From 2e849703cd8a2eaebff094ef2a5108a1393d02ed Mon Sep 17 00:00:00 2001 From: jif-oai Date: Fri, 27 Mar 2026 09:41:47 +0100 Subject: [PATCH] chore: drop useless stuff (#15876) --- MODULE.bazel.lock | 3 - codex-rs/Cargo.lock | 52 -- codex-rs/Cargo.toml | 6 - codex-rs/package-manager/BUILD.bazel | 6 - codex-rs/package-manager/Cargo.toml | 27 - codex-rs/package-manager/README.md | 64 --- codex-rs/package-manager/src/archive.rs | 270 --------- codex-rs/package-manager/src/config.rs | 40 -- codex-rs/package-manager/src/error.rs | 54 -- codex-rs/package-manager/src/lib.rs | 17 - codex-rs/package-manager/src/manager.rs | 464 --------------- codex-rs/package-manager/src/package.rs | 69 --- codex-rs/package-manager/src/platform.rs | 48 -- codex-rs/package-manager/src/tests.rs | 700 ----------------------- 14 files changed, 1820 deletions(-) delete mode 100644 codex-rs/package-manager/BUILD.bazel delete mode 100644 codex-rs/package-manager/Cargo.toml delete mode 100644 codex-rs/package-manager/README.md delete mode 100644 codex-rs/package-manager/src/archive.rs delete mode 100644 codex-rs/package-manager/src/config.rs delete mode 100644 codex-rs/package-manager/src/error.rs delete mode 100644 codex-rs/package-manager/src/lib.rs delete mode 100644 codex-rs/package-manager/src/manager.rs delete mode 100644 codex-rs/package-manager/src/package.rs delete mode 100644 codex-rs/package-manager/src/platform.rs delete mode 100644 codex-rs/package-manager/src/tests.rs diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 7b3454a50..2333f4850 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -824,7 +824,6 @@ "fdeflate_0.3.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"miniz_oxide\",\"req\":\"^0.7.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"name\":\"simd-adler32\",\"req\":\"^0.3.4\"}],\"features\":{}}", "fiat-crypto_0.2.9": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "filedescriptor_0.8.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"features\":[\"winuser\",\"handleapi\",\"fileapi\",\"namedpipeapi\",\"processthreadsapi\",\"winsock2\",\"processenv\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{}}", - "filetime_0.2.27": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.27\",\"target\":\"cfg(unix)\"},{\"name\":\"libredox\",\"req\":\"^0.1.0\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{}}", "find-crate_0.6.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"semver\",\"req\":\"^0.11\"},{\"name\":\"toml\",\"req\":\"^0.5.2\"}],\"features\":{}}", "find-msvc-tools_0.1.9": "{\"dependencies\":[],\"features\":{}}", "findshlibs_0.10.2": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.67\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"name\":\"libc\",\"req\":\"^0.2.104\"},{\"features\":[\"psapi\",\"memoryapi\",\"libloaderapi\",\"processthreadsapi\"],\"name\":\"winapi\",\"req\":\"^0.3.9\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{}}", @@ -1326,7 +1325,6 @@ "system-configuration-sys_0.6.0": "{\"dependencies\":[{\"name\":\"core-foundation-sys\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2.149\"}],\"features\":{}}", "system-configuration_0.6.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"core-foundation\",\"req\":\"^0.9\"},{\"name\":\"system-configuration-sys\",\"req\":\"^0.6\"}],\"features\":{}}", "tagptr_0.2.0": "{\"dependencies\":[],\"features\":{}}", - "tar_0.4.45": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"astral-tokio-tar\",\"req\":\"^0.5\"},{\"name\":\"filetime\",\"req\":\"^0.2.8\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"xattr\",\"optional\":true,\"req\":\"^1.1.3\",\"target\":\"cfg(unix)\"}],\"features\":{\"default\":[\"xattr\"]}}", "tempfile_3.24.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"fastrand\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"once_cell\",\"req\":\"^1.19.0\"},{\"features\":[\"fs\"],\"name\":\"rustix\",\"req\":\"^1.1.3\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"Win32_Storage_FileSystem\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"getrandom\"],\"nightly\":[]}}", "temporal_capi_0.1.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"diplomat\",\"req\":\"^0.14.0\"},{\"default_features\":false,\"name\":\"diplomat-runtime\",\"req\":\"^0.14.0\"},{\"default_features\":false,\"features\":[\"unstable\"],\"name\":\"icu_calendar\",\"req\":\"^2.1.0\"},{\"name\":\"icu_locale\",\"req\":\"^2.1.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.19\"},{\"default_features\":false,\"name\":\"temporal_rs\",\"req\":\"^0.1.2\"},{\"name\":\"timezone_provider\",\"req\":\"^0.1.2\"},{\"name\":\"writeable\",\"req\":\"^0.6.0\"},{\"name\":\"zoneinfo64\",\"optional\":true,\"req\":\"^0.2.0\"}],\"features\":{\"compiled_data\":[\"temporal_rs/compiled_data\"],\"zoneinfo64\":[\"dep:zoneinfo64\",\"timezone_provider/zoneinfo64\"]}}", "temporal_rs_0.1.2": "{\"dependencies\":[{\"name\":\"core_maths\",\"req\":\"^0.1.1\"},{\"name\":\"iana-time-zone\",\"optional\":true,\"req\":\"^0.1.64\"},{\"default_features\":false,\"features\":[\"unstable\",\"compiled_data\"],\"name\":\"icu_calendar\",\"req\":\"^2.1.0\"},{\"name\":\"icu_locale\",\"req\":\"^2.1.0\"},{\"features\":[\"duration\"],\"name\":\"ixdtf\",\"req\":\"^0.6.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.28\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.19\"},{\"name\":\"timezone_provider\",\"req\":\"^0.1.2\"},{\"name\":\"tinystr\",\"req\":\"^0.8.0\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1.1.0\"},{\"name\":\"writeable\",\"req\":\"^0.6.0\"}],\"features\":{\"compiled_data\":[\"tzdb\"],\"default\":[\"sys\"],\"float64_representable_durations\":[],\"log\":[\"dep:log\"],\"std\":[],\"sys\":[\"std\",\"compiled_data\",\"dep:web-time\",\"dep:iana-time-zone\"],\"tzdb\":[\"std\",\"timezone_provider/tzif\"]}}", @@ -1542,7 +1540,6 @@ "x11rb_0.13.2": "{\"dependencies\":[{\"name\":\"as-raw-xcb-connection\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"gethostname\",\"req\":\"^1.0\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"libloading\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.19\"},{\"kind\":\"dev\",\"name\":\"polling\",\"req\":\"^3.4\"},{\"name\":\"raw-window-handle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"default_features\":false,\"features\":[\"std\",\"event\",\"fs\",\"net\",\"system\"],\"name\":\"rustix\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"x11rb-protocol\",\"req\":\"^0.13.2\"},{\"name\":\"xcursor\",\"optional\":true,\"req\":\"^0.3.7\"}],\"features\":{\"all-extensions\":[\"x11rb-protocol/all-extensions\",\"composite\",\"damage\",\"dbe\",\"dpms\",\"dri2\",\"dri3\",\"glx\",\"present\",\"randr\",\"record\",\"render\",\"res\",\"screensaver\",\"shape\",\"shm\",\"sync\",\"xevie\",\"xf86dri\",\"xf86vidmode\",\"xfixes\",\"xinerama\",\"xinput\",\"xkb\",\"xprint\",\"xselinux\",\"xtest\",\"xv\",\"xvmc\"],\"allow-unsafe-code\":[\"libc\",\"as-raw-xcb-connection\"],\"composite\":[\"x11rb-protocol/composite\",\"xfixes\"],\"cursor\":[\"render\",\"resource_manager\",\"xcursor\"],\"damage\":[\"x11rb-protocol/damage\",\"xfixes\"],\"dbe\":[\"x11rb-protocol/dbe\"],\"dl-libxcb\":[\"allow-unsafe-code\",\"libloading\",\"once_cell\"],\"dpms\":[\"x11rb-protocol/dpms\"],\"dri2\":[\"x11rb-protocol/dri2\"],\"dri3\":[\"x11rb-protocol/dri3\"],\"extra-traits\":[\"x11rb-protocol/extra-traits\"],\"glx\":[\"x11rb-protocol/glx\"],\"image\":[],\"present\":[\"x11rb-protocol/present\",\"randr\",\"xfixes\",\"sync\"],\"randr\":[\"x11rb-protocol/randr\",\"render\"],\"record\":[\"x11rb-protocol/record\"],\"render\":[\"x11rb-protocol/render\"],\"request-parsing\":[\"x11rb-protocol/request-parsing\"],\"res\":[\"x11rb-protocol/res\"],\"resource_manager\":[\"x11rb-protocol/resource_manager\"],\"screensaver\":[\"x11rb-protocol/screensaver\"],\"shape\":[\"x11rb-protocol/shape\"],\"shm\":[\"x11rb-protocol/shm\"],\"sync\":[\"x11rb-protocol/sync\"],\"xevie\":[\"x11rb-protocol/xevie\"],\"xf86dri\":[\"x11rb-protocol/xf86dri\"],\"xf86vidmode\":[\"x11rb-protocol/xf86vidmode\"],\"xfixes\":[\"x11rb-protocol/xfixes\",\"render\",\"shape\"],\"xinerama\":[\"x11rb-protocol/xinerama\"],\"xinput\":[\"x11rb-protocol/xinput\",\"xfixes\"],\"xkb\":[\"x11rb-protocol/xkb\"],\"xprint\":[\"x11rb-protocol/xprint\"],\"xselinux\":[\"x11rb-protocol/xselinux\"],\"xtest\":[\"x11rb-protocol/xtest\"],\"xv\":[\"x11rb-protocol/xv\",\"shm\"],\"xvmc\":[\"x11rb-protocol/xvmc\",\"xv\"]}}", "x25519-dalek_2.0.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"zeroize_derive\"],\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"curve25519-dalek/alloc\",\"serde?/alloc\",\"zeroize?/alloc\"],\"default\":[\"alloc\",\"precomputed-tables\",\"zeroize\"],\"getrandom\":[\"rand_core/getrandom\"],\"precomputed-tables\":[\"curve25519-dalek/precomputed-tables\"],\"reusable_secrets\":[],\"serde\":[\"dep:serde\",\"curve25519-dalek/serde\"],\"static_secrets\":[],\"zeroize\":[\"dep:zeroize\",\"curve25519-dalek/zeroize\"]}}", "x509-parser_0.18.1": "{\"dependencies\":[{\"features\":[\"datetime\"],\"name\":\"asn1-rs\",\"req\":\"^0.7.0\"},{\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"data-encoding\",\"req\":\"^2.2.1\"},{\"features\":[\"bigint\"],\"name\":\"der-parser\",\"req\":\"^10.0\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"nom\",\"req\":\"^7.0\"},{\"features\":[\"crypto\",\"x509\",\"x962\"],\"name\":\"oid-registry\",\"req\":\"^0.8.1\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17.12\"},{\"name\":\"rusticata-macros\",\"req\":\"^4.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.35\"}],\"features\":{\"default\":[],\"validate\":[],\"verify\":[\"ring\"],\"verify-aws\":[\"aws-lc-rs\"]}}", - "xattr_1.6.1": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.150\",\"target\":\"cfg(any(target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"req\":\"^1.0.0\",\"target\":\"cfg(any(target_os = \\\"android\\\", target_os = \\\"linux\\\", target_os = \\\"macos\\\", target_os = \\\"hurd\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"default\":[\"unsupported\"],\"unsupported\":[]}}", "xdg-home_1.3.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.59\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "xz2_0.1.7": "{\"dependencies\":[{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.1.26\"},{\"name\":\"lzma-sys\",\"req\":\"^0.1.18\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"tokio-core\",\"req\":\"^0.1.17\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.12\"}],\"features\":{\"static\":[\"lzma-sys/static\"],\"tokio\":[\"tokio-io\",\"futures\"]}}", "yaml-rust_0.4.5": "{\"dependencies\":[{\"name\":\"linked-hash-map\",\"req\":\"^0.5.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"}],\"features\":{}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index c5846fbe1..17d5c35e8 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2358,26 +2358,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "codex-package-manager" -version = "0.0.0" -dependencies = [ - "fd-lock", - "flate2", - "pretty_assertions", - "reqwest", - "serde", - "serde_json", - "sha2", - "tar", - "tempfile", - "thiserror 2.0.18", - "tokio", - "url", - "wiremock", - "zip", -] - [[package]] name = "codex-plugin" version = "0.0.0" @@ -4288,17 +4268,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - [[package]] name = "find-crate" version = "0.6.3" @@ -9873,17 +9842,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" -[[package]] -name = "tar" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "tempfile" version = "3.24.0" @@ -12010,16 +11968,6 @@ dependencies = [ "time", ] -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.3", -] - [[package]] name = "xdg-home" version = "1.3.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index f05355e39..d84429014 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -81,7 +81,6 @@ members = [ "state", "terminal-detection", "codex-experimental-api-macros", - "package-manager", "plugin", ] resolver = "2" @@ -102,7 +101,6 @@ codex-ansi-escape = { path = "ansi-escape" } codex-analytics = { path = "analytics" } codex-api = { path = "codex-api" } codex-code-mode = { path = "code-mode" } -codex-package-manager = { path = "package-manager" } codex-app-server = { path = "app-server" } codex-app-server-client = { path = "app-server-client" } codex-app-server-protocol = { path = "app-server-protocol" } @@ -213,11 +211,9 @@ dirs = "6" dotenvy = "0.15.7" dunce = "1.0.4" encoding_rs = "0.8.35" -fd-lock = "4.0.4" env-flags = "0.1.1" env_logger = "0.11.9" eventsource-stream = "0.2.3" -flate2 = "1.1.4" futures = { version = "0.3", default-features = false } gethostname = "1.1.0" globset = "0.4" @@ -309,7 +305,6 @@ supports-color = "3.0.2" syntect = "5" sys-locale = "0.3.2" tempfile = "3.23.0" -tar = "0.4.45" test-log = "0.2.19" textwrap = "0.16.2" thiserror = "2.0.17" @@ -395,7 +390,6 @@ unwrap_used = "deny" ignored = [ "icu_provider", "openssl-sys", - "codex-package-manager", "codex-utils-readiness", "codex-utils-template", "codex-v8-poc", diff --git a/codex-rs/package-manager/BUILD.bazel b/codex-rs/package-manager/BUILD.bazel deleted file mode 100644 index bbbc725cb..000000000 --- a/codex-rs/package-manager/BUILD.bazel +++ /dev/null @@ -1,6 +0,0 @@ -load("//:defs.bzl", "codex_rust_crate") - -codex_rust_crate( - name = "package-manager", - crate_name = "codex_package_manager", -) diff --git a/codex-rs/package-manager/Cargo.toml b/codex-rs/package-manager/Cargo.toml deleted file mode 100644 index 3a138a80b..000000000 --- a/codex-rs/package-manager/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "codex-package-manager" -version.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -fd-lock = { workspace = true } -flate2 = { workspace = true } -reqwest = { workspace = true, features = ["json", "stream"] } -serde = { workspace = true, features = ["derive"] } -sha2 = { workspace = true } -tar = { workspace = true } -tempfile = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["fs", "rt", "sync", "time"] } -url = { workspace = true } -zip = { workspace = true } - -[lints] -workspace = true - -[dev-dependencies] -pretty_assertions = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["fs", "macros", "rt", "rt-multi-thread"] } -wiremock = { workspace = true } diff --git a/codex-rs/package-manager/README.md b/codex-rs/package-manager/README.md deleted file mode 100644 index c761bcdd6..000000000 --- a/codex-rs/package-manager/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# codex-package-manager - -`codex-package-manager` is the shared installer used for versioned runtime bundles and other cached artifacts in `codex-rs`. - -It owns the generic parts of package installation: - -- current-platform detection -- manifest and archive fetches -- checksum and archive-size validation -- archive extraction for `.zip` and `.tar.gz` -- staging and promotion into a versioned cache directory -- cross-process install locking - -Package-specific code stays behind the `ManagedPackage` trait. - -## Model - -The package manager is intentionally small: - -1. A `ManagedPackage` implementation describes how to fetch a manifest, choose an archive for a `PackagePlatform`, and load a validated installed package from disk. -2. `PackageManager::resolve_cached()` returns a cached install for the current platform if `load_installed()` succeeds and the version matches. -3. `PackageManager::ensure_installed()` acquires a per-install lock, downloads the archive into a staging directory, extracts it, validates the staged package, and promotes it into the cache. - -The default cache root is: - -```text -/ -``` - -Callers can override that root with `PackageManagerConfig::with_cache_root(...)`. - -## ManagedPackage Contract - -The trait is small, but the invariants matter: - -- `install_dir()` should be unique per package version and platform. If two versions or two platforms share a directory, promotion and cleanup become unsafe. -- `load_installed()` must fully validate the installed package, not just deserialize a manifest. `resolve_cached()` trusts a successful load as a valid cache hit. -- The default `detect_extracted_root()` looks for `manifest.json` at the extraction root or inside a single top-level directory. Override it if your package layout differs. -- `archive_url()` should be derived from manifest data, not recomputed from unrelated caller state, so manifest selection and download stay aligned. - -## Consumer Guidance - -- If your feature can install on demand, do not gate feature registration on a preinstalled-cache check alone. `resolve_cached()` only answers "is it already present?" while `ensure_installed()` is the bootstrap path. -- Keep cache-root overrides inside your manager/config surface. Separate helpers that reconstruct install paths can drift from `PackageManagerConfig`. -- Prefer surfacing package-specific validation failures from `load_installed()` when debugging. The generic manager treats failed cache loads as cache misses today. - -## Security and Extraction Rules - -- `.zip` extraction rejects entries that escape the extraction root and preserves Unix executable bits when the archive carries them. -- `.tar.gz` extraction rejects symlinks, hard links, sparse files, device files, and FIFOs. Only regular files and directories are promoted. -- The archive SHA-256 is always verified, and `size_bytes` is enforced when present in the manifest. - -## Extending It - -Typical usage looks like this: - -```rust,ignore -let config = PackageManagerConfig::new(codex_home, MyPackage::new(...)); -let manager = PackageManager::new(config); - -let package = manager.ensure_installed().await?; -``` - -In practice, most packages should expose their own small wrapper config/manager types over the generic crate so the rest of the codebase does not depend on `ManagedPackage` details directly. diff --git a/codex-rs/package-manager/src/archive.rs b/codex-rs/package-manager/src/archive.rs deleted file mode 100644 index 80de96c84..000000000 --- a/codex-rs/package-manager/src/archive.rs +++ /dev/null @@ -1,270 +0,0 @@ -use crate::PackageManagerError; -use flate2::read::GzDecoder; -use sha2::Digest; -use sha2::Sha256; -use std::fs::File; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; -use std::path::Component; -use std::path::Path; -use std::path::PathBuf; -use tar::Archive; -use zip::ZipArchive; - -/// Archive metadata for a platform entry in a release manifest. -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] -pub struct PackageReleaseArchive { - /// Archive file name relative to the package release location. - pub archive: String, - /// Expected SHA-256 of the downloaded archive body. - pub sha256: String, - /// Archive format used by the download. - pub format: ArchiveFormat, - /// Expected archive length in bytes, when the manifest provides it. - pub size_bytes: Option, -} - -/// Archive formats supported by the generic extractor. -#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] -pub enum ArchiveFormat { - /// A `.zip` archive. - #[serde(rename = "zip")] - Zip, - /// A `.tar.gz` archive. - #[serde(rename = "tar.gz")] - TarGz, -} - -/// Detects a package root with a `manifest.json` in an extraction directory. -pub(crate) fn detect_single_package_root( - extraction_root: &Path, -) -> Result { - let direct_manifest = extraction_root.join("manifest.json"); - if direct_manifest.exists() { - return Ok(extraction_root.to_path_buf()); - } - - let mut directory_candidates = Vec::new(); - for entry in std::fs::read_dir(extraction_root).map_err(|source| PackageManagerError::Io { - context: format!("failed to read {}", extraction_root.display()), - source, - })? { - let entry = entry.map_err(|source| PackageManagerError::Io { - context: format!("failed to read entry in {}", extraction_root.display()), - source, - })?; - let path = entry.path(); - if path.is_dir() { - directory_candidates.push(path); - } - } - - if directory_candidates.len() == 1 { - let candidate = &directory_candidates[0]; - if candidate.join("manifest.json").exists() { - return Ok(candidate.clone()); - } - } - - Err(PackageManagerError::MissingPackageRoot( - extraction_root.to_path_buf(), - )) -} - -pub(crate) fn verify_archive_size( - bytes: &[u8], - expected: Option, -) -> Result<(), PackageManagerError> { - let Some(expected) = expected else { - return Ok(()); - }; - let actual = bytes.len() as u64; - if actual == expected { - return Ok(()); - } - Err(PackageManagerError::UnexpectedArchiveSize { expected, actual }) -} - -pub(crate) fn verify_sha256(bytes: &[u8], expected: &str) -> Result<(), PackageManagerError> { - let actual = format!("{:x}", Sha256::digest(bytes)); - if actual == expected.to_ascii_lowercase() { - return Ok(()); - } - Err(PackageManagerError::ChecksumMismatch { - expected: expected.to_string(), - actual, - }) -} - -pub(crate) fn extract_archive( - archive_path: &Path, - destination: &Path, - format: ArchiveFormat, -) -> Result<(), PackageManagerError> { - match format { - ArchiveFormat::Zip => extract_zip_archive(archive_path, destination), - ArchiveFormat::TarGz => extract_tar_gz_archive(archive_path, destination), - } -} - -fn extract_zip_archive(archive_path: &Path, destination: &Path) -> Result<(), PackageManagerError> { - let file = File::open(archive_path).map_err(|source| PackageManagerError::Io { - context: format!("failed to open {}", archive_path.display()), - source, - })?; - let mut archive = ZipArchive::new(file) - .map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?; - for index in 0..archive.len() { - let mut entry = archive - .by_index(index) - .map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?; - let Some(relative_path) = entry.enclosed_name() else { - return Err(PackageManagerError::ArchiveExtraction(format!( - "zip entry `{}` escapes extraction root", - entry.name() - ))); - }; - let output_path = destination.join(relative_path); - if entry.is_dir() { - std::fs::create_dir_all(&output_path).map_err(|source| PackageManagerError::Io { - context: format!("failed to create {}", output_path.display()), - source, - })?; - continue; - } - if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent).map_err(|source| PackageManagerError::Io { - context: format!("failed to create {}", parent.display()), - source, - })?; - } - let mut output = File::create(&output_path).map_err(|source| PackageManagerError::Io { - context: format!("failed to create {}", output_path.display()), - source, - })?; - std::io::copy(&mut entry, &mut output).map_err(|source| PackageManagerError::Io { - context: format!("failed to write {}", output_path.display()), - source, - })?; - apply_zip_permissions(&entry, &output_path)?; - } - Ok(()) -} - -#[cfg(unix)] -fn apply_zip_permissions( - entry: &zip::read::ZipFile<'_>, - output_path: &Path, -) -> Result<(), PackageManagerError> { - let Some(mode) = entry.unix_mode() else { - return Ok(()); - }; - std::fs::set_permissions(output_path, std::fs::Permissions::from_mode(mode)).map_err(|source| { - PackageManagerError::Io { - context: format!("failed to set permissions on {}", output_path.display()), - source, - } - }) -} - -#[cfg(not(unix))] -fn apply_zip_permissions( - _entry: &zip::read::ZipFile<'_>, - _output_path: &Path, -) -> Result<(), PackageManagerError> { - Ok(()) -} - -fn extract_tar_gz_archive( - archive_path: &Path, - destination: &Path, -) -> Result<(), PackageManagerError> { - let file = File::open(archive_path).map_err(|source| PackageManagerError::Io { - context: format!("failed to open {}", archive_path.display()), - source, - })?; - let decoder = GzDecoder::new(file); - let mut archive = Archive::new(decoder); - for entry in archive - .entries() - .map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))? - { - let mut entry = - entry.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?; - let path = entry - .path() - .map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?; - let output_path = safe_extract_path(destination, path.as_ref())?; - let entry_type = entry.header().entry_type(); - - if entry_type.is_symlink() - || entry_type.is_hard_link() - || entry_type.is_block_special() - || entry_type.is_character_special() - || entry_type.is_fifo() - || entry_type.is_gnu_sparse() - { - return Err(PackageManagerError::ArchiveExtraction(format!( - "tar entry `{}` has unsupported type", - path.display() - ))); - } - - if entry_type.is_pax_global_extensions() - || entry_type.is_pax_local_extensions() - || entry_type.is_gnu_longname() - || entry_type.is_gnu_longlink() - { - continue; - } - - if entry_type.is_dir() { - std::fs::create_dir_all(&output_path).map_err(|source| PackageManagerError::Io { - context: format!("failed to create {}", output_path.display()), - source, - })?; - continue; - } - - if !entry_type.is_file() && !entry_type.is_contiguous() { - return Err(PackageManagerError::ArchiveExtraction(format!( - "tar entry `{}` has unsupported type", - path.display() - ))); - } - - if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent).map_err(|source| PackageManagerError::Io { - context: format!("failed to create {}", parent.display()), - source, - })?; - } - entry - .unpack(&output_path) - .map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?; - } - Ok(()) -} - -fn safe_extract_path(root: &Path, relative_path: &Path) -> Result { - let mut clean_relative = PathBuf::new(); - for component in relative_path.components() { - match component { - Component::Normal(segment) => clean_relative.push(segment), - Component::CurDir => {} - Component::ParentDir | Component::RootDir | Component::Prefix(_) => { - return Err(PackageManagerError::ArchiveExtraction(format!( - "entry `{}` escapes extraction root", - relative_path.display() - ))); - } - } - } - - if clean_relative.as_os_str().is_empty() { - return Err(PackageManagerError::ArchiveExtraction( - "archive entry had an empty path".to_string(), - )); - } - Ok(root.join(clean_relative)) -} diff --git a/codex-rs/package-manager/src/config.rs b/codex-rs/package-manager/src/config.rs deleted file mode 100644 index 819c5f6dd..000000000 --- a/codex-rs/package-manager/src/config.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::ManagedPackage; -use std::path::PathBuf; - -/// Immutable configuration for a [`crate::PackageManager`] instance. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PackageManagerConfig

{ - pub(crate) codex_home: PathBuf, - pub(crate) package: P, - cache_root: Option, -} - -impl

PackageManagerConfig

{ - /// Creates a config rooted at the provided Codex home directory. - pub fn new(codex_home: PathBuf, package: P) -> Self { - Self { - codex_home, - package, - cache_root: None, - } - } - - /// Overrides the package cache root instead of deriving it from `codex_home`. - pub fn with_cache_root(mut self, cache_root: PathBuf) -> Self { - self.cache_root = Some(cache_root); - self - } -} - -impl PackageManagerConfig

{ - /// Returns the effective cache root for the package. - pub fn cache_root(&self) -> PathBuf { - self.cache_root.clone().unwrap_or_else(|| { - self.codex_home.join( - self.package - .default_cache_root_relative() - .replace('/', std::path::MAIN_SEPARATOR_STR), - ) - }) - } -} diff --git a/codex-rs/package-manager/src/error.rs b/codex-rs/package-manager/src/error.rs deleted file mode 100644 index 13883ff71..000000000 --- a/codex-rs/package-manager/src/error.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::path::PathBuf; -use thiserror::Error; - -/// Errors returned by the generic package manager. -#[derive(Debug, Error)] -pub enum PackageManagerError { - /// The current machine OS/architecture pair is not supported by the package. - #[error("unsupported platform: {os}-{arch}")] - UnsupportedPlatform { os: String, arch: String }, - - /// The configured release base URL could not be joined with a package-specific path. - #[error("invalid release base url")] - InvalidBaseUrl(#[source] url::ParseError), - - /// An HTTP request failed while fetching the manifest or archive. - #[error("{context}")] - Http { - context: String, - #[source] - source: reqwest::Error, - }, - - /// A filesystem operation failed while reading, staging, or promoting a package. - #[error("{context}")] - Io { - context: String, - #[source] - source: std::io::Error, - }, - - /// The release manifest did not contain an archive for the current platform. - #[error("missing platform entry `{0}` in release manifest")] - MissingPlatform(String), - - /// The release manifest or installed package reported a different version than requested. - #[error("unexpected package version: expected `{expected}`, got `{actual}`")] - UnexpectedPackageVersion { expected: String, actual: String }, - - /// The downloaded archive length did not match the manifest metadata. - #[error("unexpected archive size: expected `{expected}`, got `{actual}`")] - UnexpectedArchiveSize { expected: u64, actual: u64 }, - - /// The downloaded archive checksum did not match the manifest metadata. - #[error("checksum mismatch: expected `{expected}`, got `{actual}`")] - ChecksumMismatch { expected: String, actual: String }, - - /// Archive extraction failed or the archive contents violated extraction rules. - #[error("archive extraction failed: {0}")] - ArchiveExtraction(String), - - /// The extracted archive layout did not contain a detectable package root. - #[error("archive did not contain a package root with manifest.json under {0}")] - MissingPackageRoot(PathBuf), -} diff --git a/codex-rs/package-manager/src/lib.rs b/codex-rs/package-manager/src/lib.rs deleted file mode 100644 index 5491d28af..000000000 --- a/codex-rs/package-manager/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod archive; -mod config; -mod error; -mod manager; -mod package; -mod platform; - -#[cfg(test)] -mod tests; - -pub use archive::ArchiveFormat; -pub use archive::PackageReleaseArchive; -pub use config::PackageManagerConfig; -pub use error::PackageManagerError; -pub use manager::PackageManager; -pub use package::ManagedPackage; -pub use platform::PackagePlatform; diff --git a/codex-rs/package-manager/src/manager.rs b/codex-rs/package-manager/src/manager.rs deleted file mode 100644 index 7b9aa193e..000000000 --- a/codex-rs/package-manager/src/manager.rs +++ /dev/null @@ -1,464 +0,0 @@ -use crate::ManagedPackage; -use crate::PackageManagerConfig; -use crate::PackageManagerError; -use crate::PackagePlatform; -use crate::archive::extract_archive; -use crate::archive::verify_archive_size; -use crate::archive::verify_sha256; -use fd_lock::RwLock as FileRwLock; -use reqwest::Client; -use std::fs::OpenOptions; -use std::path::Path; -use std::path::PathBuf; -use std::time::Duration; -use tempfile::tempdir_in; -use tokio::fs; -use tokio::time::sleep; -use url::Url; - -const INSTALL_LOCK_POLL_INTERVAL: Duration = Duration::from_millis(50); - -/// Fetches and installs a versioned package into a shared cache directory. -#[derive(Clone, Debug)] -pub struct PackageManager

{ - client: Client, - config: PackageManagerConfig

, -} - -impl

PackageManager

{ - /// Creates a manager with a default `reqwest` client. - pub fn new(config: PackageManagerConfig

) -> Self { - Self { - client: Client::new(), - config, - } - } - - /// Creates a manager with a caller-provided HTTP client. - pub fn with_client(config: PackageManagerConfig

, client: Client) -> Self { - Self { client, config } - } -} - -impl PackageManager

{ - /// Resolves a valid cached install for the current platform, if one exists. - pub async fn resolve_cached(&self) -> Result, P::Error> { - let platform = PackagePlatform::detect_current().map_err(P::Error::from)?; - let install_dir = self - .config - .package - .install_dir(&self.config.cache_root(), platform); - self.resolve_cached_at(platform, install_dir).await - } - - /// Ensures the requested package is installed for the current platform. - pub async fn ensure_installed(&self) -> Result { - // Fast path: most calls should resolve an already validated cache entry - // without touching the network or the install lock. - if let Some(package) = self.resolve_cached().await? { - return Ok(package); - } - - let platform = PackagePlatform::detect_current().map_err(P::Error::from)?; - let cache_root = self.config.cache_root(); - let install_dir = self.config.package.install_dir(&cache_root, platform); - if let Some(package) = self - .resolve_cached_at(platform, install_dir.clone()) - .await? - { - return Ok(package); - } - - if let Some(parent) = install_dir.parent() { - fs::create_dir_all(parent) - .await - .map_err(|source| PackageManagerError::Io { - context: format!("failed to create {}", parent.display()), - source, - }) - .map_err(P::Error::from)?; - } - - let lock_path = install_dir.with_extension("lock"); - let lock_file = OpenOptions::new() - .create(true) - .read(true) - .write(true) - .truncate(false) - .open(&lock_path) - .map_err(|source| PackageManagerError::Io { - context: format!("failed to open {}", lock_path.display()), - source, - }) - .map_err(P::Error::from)?; - let mut install_lock = FileRwLock::new(lock_file); - let _install_guard = loop { - match install_lock.try_write() { - Ok(guard) => break guard, - Err(source) if source.kind() == std::io::ErrorKind::WouldBlock => { - sleep(INSTALL_LOCK_POLL_INTERVAL).await; - } - Err(source) => { - return Err(PackageManagerError::Io { - context: format!("failed to lock {}", lock_path.display()), - source, - } - .into()); - } - } - }; - - // Another process may have finished the install while we were waiting - // on the lock, so re-check before doing any download or extraction work. - if let Some(package) = self - .resolve_cached_at(platform, install_dir.clone()) - .await? - { - return Ok(package); - } - - let manifest = self.fetch_release_manifest().await?; - if self.config.package.release_version(&manifest) != self.config.package.version() { - return Err(PackageManagerError::UnexpectedPackageVersion { - expected: self.config.package.version().to_string(), - actual: self.config.package.release_version(&manifest).to_string(), - } - .into()); - } - - fs::create_dir_all(&cache_root) - .await - .map_err(|source| PackageManagerError::Io { - context: format!("failed to create {}", cache_root.display()), - source, - }) - .map_err(P::Error::from)?; - let staging_root = cache_root.join(".staging"); - fs::create_dir_all(&staging_root) - .await - .map_err(|source| PackageManagerError::Io { - context: format!("failed to create {}", staging_root.display()), - source, - }) - .map_err(P::Error::from)?; - - // Everything below happens in a disposable staging area until the - // extracted package has passed package-specific validation. - let platform_archive = self.config.package.platform_archive(&manifest, platform)?; - let archive_url = self - .config - .package - .archive_url(&platform_archive) - .map_err(P::Error::from)?; - let archive_bytes = self.download_bytes(&archive_url).await?; - verify_archive_size(&archive_bytes, platform_archive.size_bytes).map_err(P::Error::from)?; - verify_sha256(&archive_bytes, &platform_archive.sha256).map_err(P::Error::from)?; - - let staging_dir = tempdir_in(&staging_root) - .map_err(|source| PackageManagerError::Io { - context: format!( - "failed to create staging directory in {}", - staging_root.display() - ), - source, - }) - .map_err(P::Error::from)?; - let archive_path = staging_dir.path().join(&platform_archive.archive); - fs::write(&archive_path, &archive_bytes) - .await - .map_err(|source| PackageManagerError::Io { - context: format!("failed to write {}", archive_path.display()), - source, - }) - .map_err(P::Error::from)?; - let extraction_root = staging_dir.path().join("extract"); - fs::create_dir_all(&extraction_root) - .await - .map_err(|source| PackageManagerError::Io { - context: format!("failed to create {}", extraction_root.display()), - source, - }) - .map_err(P::Error::from)?; - - extract_archive(&archive_path, &extraction_root, platform_archive.format) - .map_err(P::Error::from)?; - let extracted_root = self - .config - .package - .detect_extracted_root(&extraction_root)?; - let package = self - .config - .package - .load_installed(extracted_root.clone(), platform)?; - if self.config.package.installed_version(&package) != self.config.package.version() { - return Err(PackageManagerError::UnexpectedPackageVersion { - expected: self.config.package.version().to_string(), - actual: self.config.package.installed_version(&package).to_string(), - } - .into()); - } - - if let Some(parent) = install_dir.parent() { - fs::create_dir_all(parent) - .await - .map_err(|source| PackageManagerError::Io { - context: format!("failed to create {}", parent.display()), - source, - }) - .map_err(P::Error::from)?; - } - - // Promotion is intentionally two-phase: move the old install aside, - // then attempt an atomic rename of the staged tree into place. - // If promotion fails, restore the previous install before returning. - let replaced_install_dir = quarantine_existing_install(&install_dir) - .await - .map_err(P::Error::from)?; - let promotion = promote_staged_install(&extracted_root, &install_dir).await; - if let Err(error) = promotion { - // If another process won the race after we staged our copy, prefer - // the now-installed cache entry and clean up our quarantined copy. - if matches!( - &error, - PackageManagerError::Io { source, .. } - if matches!( - source.kind(), - std::io::ErrorKind::AlreadyExists - | std::io::ErrorKind::DirectoryNotEmpty - ) - ) && let Some(package) = self - .resolve_cached_at(platform, install_dir.clone()) - .await? - { - if let Some(replaced_install_dir) = replaced_install_dir { - let _ = fs::remove_dir_all(replaced_install_dir).await; - } - return Ok(package); - } - - restore_quarantined_install(&install_dir, replaced_install_dir.as_deref(), &error) - .await - .map_err(P::Error::from)?; - return Err(error.into()); - } - - // Validate from the final install path before deleting the quarantined - // previous install. Some packages may only fully validate once the - // promoted tree is in place at its real cache location. - let package = match self - .config - .package - .load_installed(install_dir.clone(), platform) - { - Ok(package) => package, - Err(error) => { - if let Some(replaced_install_dir) = replaced_install_dir.as_deref() { - // Final validation failed after promotion, so discard the - // broken install and restore the last known-good copy. - if fs::try_exists(&install_dir) - .await - .map_err(|source| PackageManagerError::Io { - context: format!("failed to read {}", install_dir.display()), - source, - }) - .map_err(P::Error::from)? - { - fs::remove_dir_all(&install_dir) - .await - .map_err(|source| PackageManagerError::Io { - context: format!( - "failed to remove invalid install {} after final validation failed", - install_dir.display() - ), - source, - }) - .map_err(P::Error::from)?; - } - fs::rename(replaced_install_dir, &install_dir) - .await - .map_err(|source| PackageManagerError::Io { - context: format!( - "failed to restore {} from {} after final validation failed", - install_dir.display(), - replaced_install_dir.display() - ), - source, - }) - .map_err(P::Error::from)?; - } - return Err(error); - } - }; - - if let Some(replaced_install_dir) = replaced_install_dir { - let _ = fs::remove_dir_all(replaced_install_dir).await; - } - - Ok(package) - } - - async fn resolve_cached_at( - &self, - platform: PackagePlatform, - install_dir: PathBuf, - ) -> Result, P::Error> { - if !fs::try_exists(&install_dir) - .await - .map_err(|source| PackageManagerError::Io { - context: format!("failed to read {}", install_dir.display()), - source, - }) - .map_err(P::Error::from)? - { - return Ok(None); - } - - let package = match self.config.package.load_installed(install_dir, platform) { - Ok(package) => package, - Err(_) => return Ok(None), - }; - if self.config.package.installed_version(&package) != self.config.package.version() { - return Ok(None); - } - Ok(Some(package)) - } - - async fn fetch_release_manifest(&self) -> Result { - let manifest_url = self.config.package.manifest_url().map_err(P::Error::from)?; - let response = self - .client - .get(manifest_url.clone()) - .send() - .await - .map_err(|source| PackageManagerError::Http { - context: format!("failed to fetch {manifest_url}"), - source, - }) - .map_err(P::Error::from)? - .error_for_status() - .map_err(|source| PackageManagerError::Http { - context: format!("manifest request failed for {manifest_url}"), - source, - }) - .map_err(P::Error::from)?; - - response - .json::() - .await - .map_err(|source| PackageManagerError::Http { - context: format!("failed to decode manifest from {manifest_url}"), - source, - }) - .map_err(P::Error::from) - } - - async fn download_bytes(&self, url: &Url) -> Result, P::Error> { - let response = self - .client - .get(url.clone()) - .send() - .await - .map_err(|source| PackageManagerError::Http { - context: format!("failed to download {url}"), - source, - }) - .map_err(P::Error::from)? - .error_for_status() - .map_err(|source| PackageManagerError::Http { - context: format!("archive request failed for {url}"), - source, - }) - .map_err(P::Error::from)?; - let bytes = response - .bytes() - .await - .map_err(|source| PackageManagerError::Http { - context: format!("failed to read response body for {url}"), - source, - }) - .map_err(P::Error::from)?; - Ok(bytes.to_vec()) - } -} - -pub(crate) async fn quarantine_existing_install( - install_dir: &Path, -) -> Result, PackageManagerError> { - if !fs::try_exists(install_dir) - .await - .map_err(|source| PackageManagerError::Io { - context: format!("failed to read {}", install_dir.display()), - source, - })? - { - return Ok(None); - } - - let install_name = install_dir.file_name().ok_or_else(|| { - PackageManagerError::ArchiveExtraction(format!( - "install path `{}` has no terminal component", - install_dir.display() - )) - })?; - let install_name = install_name.to_string_lossy(); - let mut suffix = 0u32; - loop { - let quarantined_path = install_dir.with_file_name(format!( - ".{install_name}.replaced-{}-{suffix}", - std::process::id() - )); - match fs::rename(install_dir, &quarantined_path).await { - Ok(()) => return Ok(Some(quarantined_path)), - Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => { - suffix += 1; - } - Err(source) => { - return Err(PackageManagerError::Io { - context: format!( - "failed to quarantine {} to {}", - install_dir.display(), - quarantined_path.display() - ), - source, - }); - } - } - } -} - -pub(crate) async fn promote_staged_install( - extracted_root: &Path, - install_dir: &Path, -) -> Result<(), PackageManagerError> { - fs::rename(extracted_root, install_dir) - .await - .map_err(|source| PackageManagerError::Io { - context: format!( - "failed to move {} to {}", - extracted_root.display(), - install_dir.display() - ), - source, - }) -} - -pub(crate) async fn restore_quarantined_install( - install_dir: &Path, - quarantined_install_dir: Option<&Path>, - promotion_error: &PackageManagerError, -) -> Result<(), PackageManagerError> { - let Some(quarantined_install_dir) = quarantined_install_dir else { - return Ok(()); - }; - - fs::rename(quarantined_install_dir, install_dir) - .await - .map_err(|source| PackageManagerError::Io { - context: format!( - "{promotion_error}; failed to restore {} from {}", - install_dir.display(), - quarantined_install_dir.display() - ), - source, - }) -} diff --git a/codex-rs/package-manager/src/package.rs b/codex-rs/package-manager/src/package.rs deleted file mode 100644 index 51be14608..000000000 --- a/codex-rs/package-manager/src/package.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::PackageManagerError; -use crate::PackagePlatform; -use crate::PackageReleaseArchive; -use crate::archive::detect_single_package_root; -use serde::de::DeserializeOwned; -use std::path::Path; -use std::path::PathBuf; -use url::Url; - -/// Describes how a specific package is located, validated, and loaded. -/// -/// Implementations should treat this trait as the package manager contract: -/// -/// - [`Self::install_dir`] should resolve to a directory unique to the package version and -/// platform so concurrent versions never overwrite each other. -/// - [`Self::load_installed`] should fully validate whatever "installed" means for the package, -/// because cache resolution trusts a successful load as a valid install. -/// - The default [`Self::detect_extracted_root`] implementation expects the extracted archive to -/// contain a `manifest.json` at the package root or a single top-level directory that does. -pub trait ManagedPackage: Clone { - /// Error type surfaced by package-specific loading and validation. - type Error: From; - - /// The fully loaded package instance returned to callers. - type Installed: Clone; - - /// The decoded release manifest fetched from the remote source. - type ReleaseManifest: DeserializeOwned; - - /// Returns the default cache root relative to Codex home. - fn default_cache_root_relative(&self) -> &str; - - /// Returns the requested package version. - fn version(&self) -> &str; - - /// Returns the manifest URL for the requested version. - fn manifest_url(&self) -> Result; - - /// Returns the archive download URL for a platform-specific manifest entry. - fn archive_url(&self, archive: &PackageReleaseArchive) -> Result; - - /// Returns the version string stored in the fetched release manifest. - fn release_version<'a>(&self, manifest: &'a Self::ReleaseManifest) -> &'a str; - - /// Selects the archive to download for the current platform. - fn platform_archive( - &self, - manifest: &Self::ReleaseManifest, - platform: PackagePlatform, - ) -> Result; - - /// Returns the final install directory for the package version and platform. - fn install_dir(&self, cache_root: &Path, platform: PackagePlatform) -> PathBuf; - - /// Returns the version string encoded in a fully loaded installed package. - fn installed_version<'a>(&self, package: &'a Self::Installed) -> &'a str; - - /// Loads and validates an installed package from disk. - fn load_installed( - &self, - root_dir: PathBuf, - platform: PackagePlatform, - ) -> Result; - - /// Resolves the extracted package root before the staged install is promoted. - fn detect_extracted_root(&self, extraction_root: &Path) -> Result { - detect_single_package_root(extraction_root).map_err(Self::Error::from) - } -} diff --git a/codex-rs/package-manager/src/platform.rs b/codex-rs/package-manager/src/platform.rs deleted file mode 100644 index c179b803e..000000000 --- a/codex-rs/package-manager/src/platform.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::PackageManagerError; - -/// Supported OS and CPU combinations for managed packages. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum PackagePlatform { - /// macOS on Apple Silicon. - DarwinArm64, - /// macOS on x86_64. - DarwinX64, - /// Linux on AArch64. - LinuxArm64, - /// Linux on x86_64. - LinuxX64, - /// Windows on AArch64. - WindowsArm64, - /// Windows on x86_64. - WindowsX64, -} - -impl PackagePlatform { - /// Detects the current process platform. - pub fn detect_current() -> Result { - match (std::env::consts::OS, std::env::consts::ARCH) { - ("macos", "aarch64") | ("macos", "arm64") => Ok(Self::DarwinArm64), - ("macos", "x86_64") => Ok(Self::DarwinX64), - ("linux", "aarch64") | ("linux", "arm64") => Ok(Self::LinuxArm64), - ("linux", "x86_64") => Ok(Self::LinuxX64), - ("windows", "aarch64") | ("windows", "arm64") => Ok(Self::WindowsArm64), - ("windows", "x86_64") => Ok(Self::WindowsX64), - (os, arch) => Err(PackageManagerError::UnsupportedPlatform { - os: os.to_string(), - arch: arch.to_string(), - }), - } - } - - /// Returns the manifest/cache string for this platform. - pub fn as_str(self) -> &'static str { - match self { - Self::DarwinArm64 => "darwin-arm64", - Self::DarwinX64 => "darwin-x64", - Self::LinuxArm64 => "linux-arm64", - Self::LinuxX64 => "linux-x64", - Self::WindowsArm64 => "windows-arm64", - Self::WindowsX64 => "windows-x64", - } - } -} diff --git a/codex-rs/package-manager/src/tests.rs b/codex-rs/package-manager/src/tests.rs deleted file mode 100644 index a80e98ae7..000000000 --- a/codex-rs/package-manager/src/tests.rs +++ /dev/null @@ -1,700 +0,0 @@ -use crate::ArchiveFormat; -use crate::ManagedPackage; -use crate::PackageManager; -use crate::PackageManagerConfig; -use crate::PackageManagerError; -use crate::PackagePlatform; -use crate::PackageReleaseArchive; -use crate::archive::detect_single_package_root; -use crate::archive::extract_archive; -use crate::manager::promote_staged_install; -use crate::manager::quarantine_existing_install; -use pretty_assertions::assert_eq; -use serde::Deserialize; -use sha2::Digest; -use sha2::Sha256; -use std::collections::BTreeMap; -use std::fs::File; -use std::io::Cursor; -use std::io::Write; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; -use tar::Builder; -use tar::EntryType; -use tempfile::TempDir; -use tokio::sync::Barrier; -use url::Url; -use wiremock::Mock; -use wiremock::MockServer; -use wiremock::ResponseTemplate; -use wiremock::matchers::method; -use wiremock::matchers::path; -use zip::ZipWriter; -use zip::write::SimpleFileOptions; - -#[derive(Clone, Debug)] -struct TestPackage { - base_url: Url, - version: String, - fail_on_final_install_dir: bool, -} - -#[derive(Clone, Debug, Deserialize)] -struct TestReleaseManifest { - package_version: String, - platforms: BTreeMap, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct TestInstalledPackage { - version: String, - platform: PackagePlatform, - root_dir: PathBuf, -} - -impl ManagedPackage for TestPackage { - type Error = PackageManagerError; - type Installed = TestInstalledPackage; - type ReleaseManifest = TestReleaseManifest; - - fn default_cache_root_relative(&self) -> &str { - "packages/test-package" - } - - fn version(&self) -> &str { - &self.version - } - - fn manifest_url(&self) -> Result { - self.base_url - .join(&format!("test-package-v{}-manifest.json", self.version)) - .map_err(PackageManagerError::InvalidBaseUrl) - } - - fn archive_url(&self, archive: &PackageReleaseArchive) -> Result { - self.base_url - .join(&archive.archive) - .map_err(PackageManagerError::InvalidBaseUrl) - } - - fn release_version<'a>(&self, manifest: &'a Self::ReleaseManifest) -> &'a str { - &manifest.package_version - } - - fn platform_archive( - &self, - manifest: &Self::ReleaseManifest, - platform: PackagePlatform, - ) -> Result { - manifest - .platforms - .get(platform.as_str()) - .cloned() - .ok_or_else(|| PackageManagerError::MissingPlatform(platform.as_str().to_string())) - } - - fn install_dir(&self, cache_root: &Path, platform: PackagePlatform) -> PathBuf { - cache_root.join(self.version()).join(platform.as_str()) - } - - fn installed_version<'a>(&self, package: &'a Self::Installed) -> &'a str { - &package.version - } - - fn load_installed( - &self, - root_dir: PathBuf, - platform: PackagePlatform, - ) -> Result { - if self.fail_on_final_install_dir - && root_dir - .file_name() - .is_some_and(|name| name == platform.as_str()) - { - return Err(PackageManagerError::ArchiveExtraction(format!( - "refusing final install dir {}", - root_dir.display() - ))); - } - let manifest_path = root_dir.join("manifest.json"); - let version = - std::fs::read_to_string(&manifest_path).map_err(|source| PackageManagerError::Io { - context: format!("failed to read {}", manifest_path.display()), - source, - })?; - Ok(TestInstalledPackage { - version: version.trim().to_string(), - platform, - root_dir, - }) - } -} - -#[tokio::test] -async fn ensure_installed_downloads_and_extracts_zip_package() { - let server = MockServer::start().await; - let version = "0.1.0"; - let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let archive_name = format!("test-package-v{version}-{}.zip", platform.as_str()); - let archive_bytes = build_zip_archive(version); - let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes)); - let manifest = serde_json::json!({ - "package_version": version, - "platforms": { - platform.as_str(): { - "archive": archive_name, - "sha256": archive_sha, - "format": "zip", - "size_bytes": archive_bytes.len(), - } - } - }); - Mock::given(method("GET")) - .and(path(format!("/test-package-v{version}-manifest.json"))) - .respond_with(ResponseTemplate::new(200).set_body_json(&manifest)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!("/{archive_name}"))) - .respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes)) - .mount(&server) - .await; - - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let package = TestPackage { - base_url: Url::parse(&format!("{}/", server.uri())) - .unwrap_or_else(|error| panic!("{error}")), - version: version.to_string(), - fail_on_final_install_dir: false, - }; - let manager = PackageManager::new(PackageManagerConfig::new( - codex_home.path().to_path_buf(), - package, - )); - - let installed = manager - .ensure_installed() - .await - .unwrap_or_else(|error| panic!("{error}")); - - assert_eq!( - installed, - TestInstalledPackage { - version: version.to_string(), - platform, - root_dir: codex_home - .path() - .join("packages") - .join("test-package") - .join(version) - .join(platform.as_str()), - } - ); - - #[cfg(unix)] - { - let executable_mode = std::fs::metadata(installed.root_dir.join("bin/tool")) - .unwrap_or_else(|error| panic!("{error}")) - .permissions() - .mode(); - assert_eq!(executable_mode & 0o111, 0o111); - } -} - -#[tokio::test] -async fn resolve_cached_uses_custom_cache_root() { - let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let cache_root = codex_home.path().join("custom-cache"); - let install_dir = cache_root.join("0.1.0").join(platform.as_str()); - std::fs::create_dir_all(&install_dir).unwrap_or_else(|error| panic!("{error}")); - std::fs::write(install_dir.join("manifest.json"), "0.1.0") - .unwrap_or_else(|error| panic!("{error}")); - - let manager = PackageManager::new( - PackageManagerConfig::new( - codex_home.path().to_path_buf(), - TestPackage { - base_url: Url::parse("https://example.test/") - .unwrap_or_else(|error| panic!("{error}")), - version: "0.1.0".to_string(), - fail_on_final_install_dir: false, - }, - ) - .with_cache_root(cache_root.clone()), - ); - - let installed = manager - .resolve_cached() - .await - .unwrap_or_else(|error| panic!("{error}")); - - assert_eq!( - installed, - Some(TestInstalledPackage { - version: "0.1.0".to_string(), - platform, - root_dir: cache_root.join("0.1.0").join(platform.as_str()), - }) - ); -} - -#[tokio::test] -async fn ensure_installed_replaces_invalid_cached_install() { - let server = MockServer::start().await; - let version = "0.1.0"; - let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let archive_name = format!("test-package-v{version}-{}.zip", platform.as_str()); - let archive_bytes = build_zip_archive(version); - let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes)); - let manifest = serde_json::json!({ - "package_version": version, - "platforms": { - platform.as_str(): { - "archive": archive_name, - "sha256": archive_sha, - "format": "zip", - "size_bytes": archive_bytes.len(), - } - } - }); - Mock::given(method("GET")) - .and(path(format!("/test-package-v{version}-manifest.json"))) - .respond_with(ResponseTemplate::new(200).set_body_json(&manifest)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!("/{archive_name}"))) - .respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes)) - .mount(&server) - .await; - - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let install_dir = codex_home - .path() - .join("packages") - .join("test-package") - .join(version) - .join(platform.as_str()); - std::fs::create_dir_all(&install_dir).unwrap_or_else(|error| panic!("{error}")); - std::fs::write(install_dir.join("broken.txt"), "stale") - .unwrap_or_else(|error| panic!("{error}")); - - let manager = PackageManager::new(PackageManagerConfig::new( - codex_home.path().to_path_buf(), - TestPackage { - base_url: Url::parse(&format!("{}/", server.uri())) - .unwrap_or_else(|error| panic!("{error}")), - version: version.to_string(), - fail_on_final_install_dir: false, - }, - )); - - let installed = manager - .ensure_installed() - .await - .unwrap_or_else(|error| panic!("{error}")); - - assert_eq!(installed.version, version); - assert!(installed.root_dir.join("manifest.json").exists()); - assert!(!installed.root_dir.join("broken.txt").exists()); -} - -#[tokio::test] -async fn ensure_installed_rejects_manifest_version_mismatch() { - let server = MockServer::start().await; - let version = "0.1.0"; - let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let archive_name = format!("test-package-v{version}-{}.zip", platform.as_str()); - let manifest = serde_json::json!({ - "package_version": "0.2.0", - "platforms": { - platform.as_str(): { - "archive": archive_name, - "sha256": "deadbeef", - "format": "zip", - "size_bytes": 1, - } - } - }); - Mock::given(method("GET")) - .and(path(format!("/test-package-v{version}-manifest.json"))) - .respond_with(ResponseTemplate::new(200).set_body_json(&manifest)) - .mount(&server) - .await; - - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let manager = PackageManager::new(PackageManagerConfig::new( - codex_home.path().to_path_buf(), - TestPackage { - base_url: Url::parse(&format!("{}/", server.uri())) - .unwrap_or_else(|error| panic!("{error}")), - version: version.to_string(), - fail_on_final_install_dir: false, - }, - )); - - let error = manager - .ensure_installed() - .await - .expect_err("manifest version mismatch should fail"); - assert!(matches!( - error, - PackageManagerError::UnexpectedPackageVersion { expected, actual } - if expected == "0.1.0" && actual == "0.2.0" - )); -} - -#[tokio::test] -async fn ensure_installed_serializes_concurrent_installs() { - let server = MockServer::start().await; - let version = "0.1.0"; - let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let archive_name = format!("test-package-v{version}-{}.zip", platform.as_str()); - let archive_bytes = build_zip_archive(version); - let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes)); - let manifest = serde_json::json!({ - "package_version": version, - "platforms": { - platform.as_str(): { - "archive": archive_name, - "sha256": archive_sha, - "format": "zip", - "size_bytes": archive_bytes.len(), - } - } - }); - Mock::given(method("GET")) - .and(path(format!("/test-package-v{version}-manifest.json"))) - .respond_with(ResponseTemplate::new(200).set_body_json(&manifest)) - .expect(1) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!("/{archive_name}"))) - .respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes)) - .expect(1) - .mount(&server) - .await; - - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let config = PackageManagerConfig::new( - codex_home.path().to_path_buf(), - TestPackage { - base_url: Url::parse(&format!("{}/", server.uri())) - .unwrap_or_else(|error| panic!("{error}")), - version: version.to_string(), - fail_on_final_install_dir: false, - }, - ); - let manager_one = PackageManager::new(config.clone()); - let manager_two = PackageManager::new(config); - let barrier = Arc::new(Barrier::new(2)); - let barrier_one = Arc::clone(&barrier); - let barrier_two = Arc::clone(&barrier); - - let (first, second) = tokio::join!( - async { - barrier_one.wait().await; - manager_one.ensure_installed().await - }, - async { - barrier_two.wait().await; - manager_two.ensure_installed().await - } - ); - - let first = first.unwrap_or_else(|error| panic!("{error}")); - let second = second.unwrap_or_else(|error| panic!("{error}")); - assert_eq!(first, second); -} - -#[tokio::test] -async fn ensure_installed_rejects_unexpected_archive_size() { - let server = MockServer::start().await; - let version = "0.1.0"; - let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let archive_name = format!("test-package-v{version}-{}.zip", platform.as_str()); - let archive_bytes = build_zip_archive(version); - let actual_size = archive_bytes.len() as u64; - let expected_size = (archive_bytes.len() + 1) as u64; - let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes)); - let manifest = serde_json::json!({ - "package_version": version, - "platforms": { - platform.as_str(): { - "archive": archive_name, - "sha256": archive_sha, - "format": "zip", - "size_bytes": expected_size, - } - } - }); - Mock::given(method("GET")) - .and(path(format!("/test-package-v{version}-manifest.json"))) - .respond_with(ResponseTemplate::new(200).set_body_json(&manifest)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!("/{archive_name}"))) - .respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes)) - .mount(&server) - .await; - - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let manager = PackageManager::new(PackageManagerConfig::new( - codex_home.path().to_path_buf(), - TestPackage { - base_url: Url::parse(&format!("{}/", server.uri())) - .unwrap_or_else(|error| panic!("{error}")), - version: version.to_string(), - fail_on_final_install_dir: false, - }, - )); - - let error = manager - .ensure_installed() - .await - .expect_err("archive size mismatch should fail"); - assert!(matches!( - error, - PackageManagerError::UnexpectedArchiveSize { expected, actual } - if expected == expected_size && actual == actual_size - )); -} - -#[tokio::test] -async fn staged_install_restore_keeps_previous_install_on_failed_promotion() { - let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let install_dir = temp.path().join("install"); - let staged_dir = temp.path().join("missing-staged"); - std::fs::create_dir_all(&install_dir).unwrap_or_else(|error| panic!("{error}")); - std::fs::write(install_dir.join("manifest.json"), "0.1.0") - .unwrap_or_else(|error| panic!("{error}")); - - let quarantined = quarantine_existing_install(&install_dir) - .await - .unwrap_or_else(|error| panic!("{error}")); - let promotion_error = promote_staged_install(&staged_dir, &install_dir) - .await - .expect_err("promotion should fail"); - crate::manager::restore_quarantined_install( - &install_dir, - quarantined.as_deref(), - &promotion_error, - ) - .await - .unwrap_or_else(|error| panic!("{error}")); - - assert!(install_dir.join("manifest.json").exists()); - assert_eq!( - std::fs::read_to_string(install_dir.join("manifest.json")) - .unwrap_or_else(|error| panic!("{error}")), - "0.1.0" - ); -} - -#[tokio::test] -async fn ensure_installed_restores_previous_install_when_final_validation_fails() { - let server = MockServer::start().await; - let version = "0.1.0"; - let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let archive_name = format!("test-package-v{version}-{}.zip", platform.as_str()); - let archive_bytes = build_zip_archive(version); - let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes)); - let manifest = serde_json::json!({ - "package_version": version, - "platforms": { - platform.as_str(): { - "archive": archive_name, - "sha256": archive_sha, - "format": "zip", - "size_bytes": archive_bytes.len(), - } - } - }); - Mock::given(method("GET")) - .and(path(format!("/test-package-v{version}-manifest.json"))) - .respond_with(ResponseTemplate::new(200).set_body_json(&manifest)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!("/{archive_name}"))) - .respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes)) - .mount(&server) - .await; - - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let install_dir = codex_home - .path() - .join("packages") - .join("test-package") - .join(version) - .join(platform.as_str()); - std::fs::create_dir_all(&install_dir).unwrap_or_else(|error| panic!("{error}")); - std::fs::write(install_dir.join("manifest.json"), "0.0.9") - .unwrap_or_else(|error| panic!("{error}")); - - let error = PackageManager::new(PackageManagerConfig::new( - codex_home.path().to_path_buf(), - TestPackage { - base_url: Url::parse(&format!("{}/", server.uri())) - .unwrap_or_else(|error| panic!("{error}")), - version: version.to_string(), - fail_on_final_install_dir: true, - }, - )) - .ensure_installed() - .await - .expect_err("final validation should fail"); - - assert!( - matches!(error, PackageManagerError::ArchiveExtraction(message) if message.contains("refusing final install dir")) - ); - assert_eq!( - std::fs::read_to_string(install_dir.join("manifest.json")) - .unwrap_or_else(|error| panic!("{error}")), - "0.0.9" - ); - assert!( - !install_dir - .parent() - .unwrap_or_else(|| panic!("install dir should have a parent")) - .read_dir() - .unwrap_or_else(|error| panic!("{error}")) - .any(|entry| { - entry - .unwrap_or_else(|error| panic!("{error}")) - .file_name() - .to_string_lossy() - .contains(".replaced-") - }) - ); -} - -#[test] -fn tar_gz_extraction_supports_default_package_root_detection() { - let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let archive_path = temp.path().join("package.tar.gz"); - let extraction_root = temp.path().join("extract"); - std::fs::create_dir_all(&extraction_root).unwrap_or_else(|error| panic!("{error}")); - write_tar_gz_archive(&archive_path, "0.2.0"); - - extract_archive(&archive_path, &extraction_root, ArchiveFormat::TarGz) - .unwrap_or_else(|error| panic!("{error}")); - let package_root = - detect_single_package_root(&extraction_root).unwrap_or_else(|error| panic!("{error}")); - - assert!(package_root.join("manifest.json").exists()); -} - -#[test] -fn tar_gz_extraction_rejects_symlinks() { - let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let archive_path = temp.path().join("package.tar.gz"); - let extraction_root = temp.path().join("extract"); - std::fs::create_dir_all(&extraction_root).unwrap_or_else(|error| panic!("{error}")); - write_tar_gz_archive_with_symlink(&archive_path); - - let error = extract_archive(&archive_path, &extraction_root, ArchiveFormat::TarGz) - .expect_err("symlink entry should fail"); - assert!( - matches!(error, PackageManagerError::ArchiveExtraction(message) if message.contains("unsupported type")) - ); -} - -#[test] -fn zip_extraction_rejects_parent_paths() { - let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let archive_path = temp.path().join("package.zip"); - let extraction_root = temp.path().join("extract"); - std::fs::create_dir_all(&extraction_root).unwrap_or_else(|error| panic!("{error}")); - write_zip_archive_with_parent_path(&archive_path); - - let error = extract_archive(&archive_path, &extraction_root, ArchiveFormat::Zip) - .expect_err("parent path entry should fail"); - assert!( - matches!(error, PackageManagerError::ArchiveExtraction(message) if message.contains("escapes extraction root")) - ); -} - -fn build_zip_archive(version: &str) -> Vec { - let mut bytes = Cursor::new(Vec::new()); - { - let mut zip = ZipWriter::new(&mut bytes); - let options = SimpleFileOptions::default(); - zip.start_file("test-package/manifest.json", options) - .unwrap_or_else(|error| panic!("{error}")); - zip.write_all(version.as_bytes()) - .unwrap_or_else(|error| panic!("{error}")); - zip.start_file("test-package/bin/tool", options.unix_permissions(0o755)) - .unwrap_or_else(|error| panic!("{error}")); - zip.write_all(b"#!/bin/sh\n") - .unwrap_or_else(|error| panic!("{error}")); - zip.finish().unwrap_or_else(|error| panic!("{error}")); - } - bytes.into_inner() -} - -fn write_zip_archive_with_parent_path(archive_path: &Path) { - let file = File::create(archive_path).unwrap_or_else(|error| panic!("{error}")); - let mut zip = ZipWriter::new(file); - let options = SimpleFileOptions::default(); - zip.start_file("../escape.txt", options) - .unwrap_or_else(|error| panic!("{error}")); - zip.write_all(b"escape") - .unwrap_or_else(|error| panic!("{error}")); - zip.finish().unwrap_or_else(|error| panic!("{error}")); -} - -fn write_tar_gz_archive(archive_path: &Path, version: &str) { - let file = File::create(archive_path).unwrap_or_else(|error| panic!("{error}")); - let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default()); - let mut builder = Builder::new(encoder); - - append_tar_file( - &mut builder, - "test-package/manifest.json", - version.as_bytes(), - ); - builder.finish().unwrap_or_else(|error| panic!("{error}")); -} - -fn write_tar_gz_archive_with_symlink(archive_path: &Path) { - let file = File::create(archive_path).unwrap_or_else(|error| panic!("{error}")); - let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default()); - let mut builder = Builder::new(encoder); - - append_tar_file(&mut builder, "test-package/manifest.json", b"0.2.0"); - - let mut header = tar::Header::new_gnu(); - header.set_entry_type(EntryType::Symlink); - header.set_size(0); - header.set_mode(0o777); - header - .set_link_name("/tmp/escape") - .unwrap_or_else(|error| panic!("{error}")); - header.set_cksum(); - builder - .append_data(&mut header, "test-package/link", std::io::empty()) - .unwrap_or_else(|error| panic!("{error}")); - - builder.finish().unwrap_or_else(|error| panic!("{error}")); -} - -fn append_tar_file( - builder: &mut Builder>, - path: &str, - contents: &[u8], -) { - let mut header = tar::Header::new_gnu(); - header.set_size(contents.len() as u64); - header.set_mode(0o755); - header.set_cksum(); - builder - .append_data(&mut header, path, contents) - .unwrap_or_else(|error| panic!("{error}")); -}