From 1f452a5706aca6ad851db28c3ff29caa9f610cc9 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Sun, 21 Jan 2024 17:13:17 -0500 Subject: [PATCH] refactor: updates for smarter lockfile (#41) --- Cargo.lock | 215 ++++++++++++++++++------------------- Cargo.toml | 6 +- rust-toolchain.toml | 2 +- src/lib.rs | 52 ++++++++- src/registry.rs | 75 +++++++++---- src/resolution/mod.rs | 1 + src/resolution/snapshot.rs | 205 +++++++++++++++++++++++++++++------ 7 files changed, 385 insertions(+), 171 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db18516..86f974b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,12 +31,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bumpalo" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - [[package]] name = "bytes" version = "1.4.0" @@ -45,9 +39,9 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.80" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f1226cd9da55587234753d1245dd5b132343ea240f26b6a9003d68706141ba" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] @@ -70,9 +64,9 @@ dependencies = [ [[package]] name = "deno_lockfile" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f589768a9e492bef84acb0c5c203d766d49480a8e096aae3bca4b4c9554e90" +checksum = "dfe06eda519ed05b69da567bcba1d728c482fd553ddaa2ffe008468158da6de0" dependencies = [ "ring", "serde", @@ -100,9 +94,9 @@ dependencies = [ [[package]] name = "deno_semver" -version = "0.5.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "594fd570fecc994ef602b96214f9d6c8ae11e60e29e37e89ab90af7f6b7f9d00" +checksum = "b49e14effd9df8ed261f7a1a34ac19bbaf0fa940c59bd19a6d8313cf41525e1c" dependencies = [ "monch", "once_cell", @@ -215,6 +209,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -256,20 +261,11 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" -[[package]] -name = "js-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" -dependencies = [ - "wasm-bindgen", -] - [[package]] name = "libc" -version = "0.2.141" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "lock_api" @@ -305,14 +301,14 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] name = "monch" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4519a88847ba2d5ead3dc53f1060ec6a571de93f325d9c5c4968147382b1cbc3" +checksum = "b52c1b33ff98142aecea13138bd399b68aa7ab5d9546c300988c345004001eea" [[package]] name = "num_cpus" @@ -359,7 +355,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -421,17 +417,16 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", + "getrandom", "libc", - "once_cell", "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -514,9 +509,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.5.2" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "syn" @@ -591,7 +586,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -628,9 +623,9 @@ dependencies = [ [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" @@ -649,70 +644,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasm-bindgen" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.13", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.13", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" - -[[package]] -name = "web-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "winapi" version = "0.3.9" @@ -741,7 +672,16 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", ] [[package]] @@ -750,13 +690,28 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -765,42 +720,84 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 393e9d9..80997e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,9 @@ license = "MIT" [dependencies] anyhow = "1.0.70" async-trait = "0.1.68" -deno_semver = "0.5.0" -deno_lockfile = "0.17.0" -monch = "0.4.3" +deno_semver = "0.5.4" +deno_lockfile = "0.18.0" +monch = "0.5.0" log = "0.4" serde = { version = "1.0.130", features = ["derive", "rc"] } thiserror = "1.0.24" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e1e8b08..5666651 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.71.0" +channel = "1.75.0" components = ["clippy", "rustfmt"] profile = "minimal" diff --git a/src/lib.rs b/src/lib.rs index 8e03ee7..c79522e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ pub mod resolution; #[derive(Debug, Error)] #[error("Invalid npm package id '{text}'. {message}")] -pub struct NpmPackageNodeIdDeserializationError { +pub struct NpmPackageIdDeserializationError { message: String, text: String, } @@ -66,7 +66,7 @@ impl NpmPackageId { pub fn from_serialized( id: &str, - ) -> Result { + ) -> Result { use monch::*; fn parse_name(input: &str) -> ParseResult<&str> { @@ -90,9 +90,13 @@ impl NpmPackageId { let (input, _) = ch('@')(input)?; let at_version_input = input; let (input, version) = parse_version(input)?; + // todo: improve monch to provide the error message without source match Version::parse_from_npm(version) { Ok(version) => Ok((input, (name.to_string(), version))), - Err(err) => ParseError::fail(at_version_input, format!("{err:#}")), + Err(err) => ParseError::fail( + at_version_input, + format!("Invalid npm version. {}", err.message()), + ), } } @@ -152,7 +156,7 @@ impl NpmPackageId { } with_failure_handling(parse_id_at_level(0))(id).map_err(|err| { - NpmPackageNodeIdDeserializationError { + NpmPackageIdDeserializationError { message: format!("{err:#}"), text: id.to_string(), } @@ -387,6 +391,46 @@ mod test { assert_eq!(NpmPackageId::from_serialized(&serialized).unwrap(), id); } + #[test] + fn parse_npm_package_id() { + #[track_caller] + fn run_test(input: &str) { + let id = NpmPackageId::from_serialized(input).unwrap(); + assert_eq!(id.as_serialized(), input); + } + + run_test("pkg-a@1.2.3"); + run_test("pkg-a@1.2.3_pkg-b@3.2.1"); + run_test( + "pkg-a@1.2.3_pkg-b@3.2.1__pkg-c@1.3.2__pkg-d@2.3.4_pkg-e@2.3.1__pkg-f@2.3.1", + ); + + #[track_caller] + fn run_error_test(input: &str, message: &str) { + let err = NpmPackageId::from_serialized(input).unwrap_err(); + assert_eq!(format!("{:#}", err), message); + } + + run_error_test( + "asdf", + "Invalid npm package id 'asdf'. Unexpected character. + asdf + ~", + ); + run_error_test( + "asdf@test", + "Invalid npm package id 'asdf@test'. Invalid npm version. Unexpected character. + test + ~", + ); + run_error_test( + "pkg@1.2.3_asdf@test", + "Invalid npm package id 'pkg@1.2.3_asdf@test'. Invalid npm version. Unexpected character. + test + ~", + ); + } + #[test] fn test_matches_os_or_cpu_vec() { assert!(matches_os_or_cpu_vec(&[], "x64")); diff --git a/src/registry.rs b/src/registry.rs index 5d348d4..824f584 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -15,28 +15,25 @@ use thiserror::Error; use crate::resolution::NpmPackageVersionNotFound; -#[derive(Debug, Clone, Error)] -#[error("Could not find @ symbol in npm url '{value}'")] -pub struct PackageDepNpmSchemeValueParseError { - pub value: String, -} - /// Gets the name and raw version constraint for a registry info or /// package.json dependency entry taking into account npm package aliases. pub fn parse_dep_entry_name_and_raw_version<'a>( key: &'a str, value: &'a str, -) -> Result<(&'a str, &'a str), PackageDepNpmSchemeValueParseError> { +) -> (&'a str, &'a str) { if let Some(package_and_version) = value.strip_prefix("npm:") { if let Some((name, version)) = package_and_version.rsplit_once('@') { - Ok((name, version)) + // if empty, then the name was scoped and there's no version + if name.is_empty() { + (package_and_version, "*") + } else { + (name, version) + } } else { - Err(PackageDepNpmSchemeValueParseError { - value: value.to_string(), - }) + (package_and_version, "*") } } else { - Ok((key, value)) + (key, value) } } @@ -81,10 +78,6 @@ pub struct NpmDependencyEntryError { pub enum NpmDependencyEntryErrorSource { #[error(transparent)] NpmVersionReqParseError(#[from] NpmVersionReqParseError), - #[error(transparent)] - PackageDepNpmSchemeValueParseError( - #[from] PackageDepNpmSchemeValueParseError, - ), } #[derive(Debug, Clone, Eq, PartialEq)] @@ -178,7 +171,7 @@ impl NpmPackageVersionInfo { kind: NpmDependencyEntryKind, ) -> Result { let (name, version_req) = - parse_dep_entry_name_and_raw_version(key, value)?; + parse_dep_entry_name_and_raw_version(key, value); let version_req = VersionReq::parse_from_npm(version_req)?; Ok(NpmDependencyEntry { kind, @@ -273,6 +266,21 @@ pub enum NpmPackageVersionDistInfoIntegrity<'a> { LegacySha1Hex(&'a str), } +impl<'a> NpmPackageVersionDistInfoIntegrity<'a> { + pub fn for_lockfile(&self) -> String { + match self { + NpmPackageVersionDistInfoIntegrity::Integrity { + algorithm, + base64_hash, + } => format!("{}-{}", algorithm, base64_hash), + NpmPackageVersionDistInfoIntegrity::UnknownIntegrity(integrity) => { + integrity.to_string() + } + NpmPackageVersionDistInfoIntegrity::LegacySha1Hex(hex) => hex.to_string(), + } + } +} + #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct NpmPackageVersionDistInfo { /// URL to the tarball. @@ -387,6 +395,15 @@ impl TestNpmRegistryApi { } pub fn ensure_package_version(&self, name: &str, version: &str) { + self.ensure_package_version_with_integrity(name, version, None) + } + + pub fn ensure_package_version_with_integrity( + &self, + name: &str, + version: &str, + integrity: Option<&str>, + ) { self.ensure_package(name); let mut infos = self.package_infos.lock().unwrap(); let info = infos.get_mut(name).unwrap(); @@ -396,6 +413,10 @@ impl TestNpmRegistryApi { version.clone(), NpmPackageVersionInfo { version, + dist: NpmPackageVersionDistInfo { + integrity: integrity.map(|s| s.to_string()), + ..Default::default() + }, ..Default::default() }, ); @@ -499,9 +520,7 @@ mod test { use deno_semver::Version; use serde_json; - use super::NpmPackageVersionBinEntry; - use super::NpmPackageVersionDistInfo; - use super::NpmPackageVersionInfo; + use super::*; #[test] fn deserializes_minimal_pkg_info() { @@ -573,4 +592,20 @@ mod test { super::NpmPackageVersionDistInfoIntegrity::UnknownIntegrity("test") ); } + + #[test] + fn test_parse_dep_entry_name_and_raw_version() { + let cases = [ + ("test", "^1.2", ("test", "^1.2")), + ("test", "1.x - 2.6", ("test", "1.x - 2.6")), + ("test", "npm:package@^1.2", ("package", "^1.2")), + ("test", "npm:package", ("package", "*")), + ("test", "npm:@scope/package", ("@scope/package", "*")), + ("test", "npm:@scope/package@1", ("@scope/package", "1")), + ]; + for (key, value, expected_result) in cases { + let result = parse_dep_entry_name_and_raw_version(key, value); + assert_eq!(result, expected_result); + } + } } diff --git a/src/resolution/mod.rs b/src/resolution/mod.rs index 3a7225d..806dc3b 100644 --- a/src/resolution/mod.rs +++ b/src/resolution/mod.rs @@ -20,4 +20,5 @@ pub use snapshot::PackageReqNotFoundError; pub use snapshot::SerializedNpmResolutionSnapshot; pub use snapshot::SerializedNpmResolutionSnapshotPackage; pub use snapshot::SnapshotFromLockfileError; +pub use snapshot::SnapshotFromLockfileParams; pub use snapshot::ValidSerializedNpmResolutionSnapshot; diff --git a/src/resolution/snapshot.rs b/src/resolution/snapshot.rs index e21ba30..106f185 100644 --- a/src/resolution/snapshot.rs +++ b/src/resolution/snapshot.rs @@ -5,8 +5,10 @@ use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; +use std::path::PathBuf; use std::rc::Rc; +use deno_lockfile::IntegrityCheckFailedError; use deno_lockfile::Lockfile; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; @@ -33,7 +35,7 @@ use crate::registry::NpmRegistryApi; use crate::registry::NpmRegistryPackageInfoLoadError; use crate::NpmPackageCacheFolderId; use crate::NpmPackageId; -use crate::NpmPackageNodeIdDeserializationError; +use crate::NpmPackageIdDeserializationError; use crate::NpmResolutionPackage; use crate::NpmResolutionPackageSystemInfo; use crate::NpmSystemInfo; @@ -803,7 +805,7 @@ fn name_without_path(name: &str) -> &str { } #[derive(Debug, Error)] -pub enum SnapshotFromLockfileError { +pub enum IncompleteSnapshotFromLockfileError { #[error("Unable to parse npm specifier: {key}")] ReqParse { key: String, @@ -811,24 +813,17 @@ pub enum SnapshotFromLockfileError { source: PackageReqParseError, }, #[error(transparent)] - NodeIdDeserialization(#[from] NpmPackageNodeIdDeserializationError), - #[error(transparent)] - PackageInfoLoad(#[from] NpmRegistryPackageInfoLoadError), - #[error("Could not find '{}' specified in the lockfile.", .source.0)] - VersionNotFound { - #[from] - source: NpmPackageVersionNotFound, - }, - #[error("The lockfile is corrupt. You can recreate it with --lock-write")] - PackageIdNotFound(#[from] PackageIdNotFoundError), + PackageIdDeserialization(#[from] NpmPackageIdDeserializationError), } struct IncompletePackageInfo { id: NpmPackageId, + integrity: String, dependencies: HashMap, } pub struct IncompleteSnapshot { + lockfile_file_name: PathBuf, root_packages: HashMap, packages: Vec, } @@ -843,7 +838,7 @@ pub struct IncompleteSnapshot { /// similar, which doesn't implement [`Send`]. pub fn incomplete_snapshot_from_lockfile( lockfile: &Lockfile, -) -> Result { +) -> Result { let mut root_packages = HashMap::::with_capacity( lockfile.content.packages.specifiers.len(), ); @@ -852,7 +847,7 @@ pub fn incomplete_snapshot_from_lockfile( if let Some(key) = key.strip_prefix("npm:") { if let Some(value) = value.strip_prefix("npm:") { let package_req = PackageReq::from_str(key).map_err(|e| { - SnapshotFromLockfileError::ReqParse { + IncompleteSnapshotFromLockfileError::ReqParse { key: key.to_string(), source: e, } @@ -875,24 +870,53 @@ pub fn incomplete_snapshot_from_lockfile( dependencies.insert(name.clone(), dep_id); } - packages.push(IncompletePackageInfo { id, dependencies }); + packages.push(IncompletePackageInfo { + id, + integrity: package.integrity.clone(), + dependencies, + }); } Ok(IncompleteSnapshot { + lockfile_file_name: lockfile.filename.clone(), root_packages, packages, }) } +#[derive(Debug, Error)] +pub enum SnapshotFromLockfileError { + #[error(transparent)] + PackageInfoLoad(#[from] NpmRegistryPackageInfoLoadError), + #[error("Could not find '{}' specified in the lockfile.", .source.0)] + VersionNotFound { + #[from] + source: NpmPackageVersionNotFound, + }, + #[error("The lockfile is corrupt. You can recreate it with --lock-write")] + PackageIdNotFound(#[from] PackageIdNotFoundError), + #[error(transparent)] + IntegrityCheckFailed(#[from] IntegrityCheckFailedError), +} + +pub struct SnapshotFromLockfileParams<'a> { + pub api: &'a dyn NpmRegistryApi, + pub incomplete_snapshot: IncompleteSnapshot, + pub skip_integrity_check: bool, +} + /// Constructs [`ValidSerializedNpmResolutionSnapshot`] from the given [`Lockfile`]. /// /// You should call [`incomplete_snapshot_from_lockfile`] first to get an /// [`IncompleteSnapshot`] instance that's passed as the first argument for this /// function. -pub async fn snapshot_from_lockfile( - incomplete_snapshot: IncompleteSnapshot, - api: &dyn NpmRegistryApi, +#[allow(clippy::needless_lifetimes)] // clippy bug +pub async fn snapshot_from_lockfile<'a>( + params: SnapshotFromLockfileParams<'a>, ) -> Result { + let api = params.api; + let incomplete_snapshot = params.incomplete_snapshot; + // fetch the package version information let pkg_nvs = incomplete_snapshot .packages @@ -916,9 +940,27 @@ pub async fn snapshot_from_lockfile( while let Some(result) = version_infos.next().await { match result { Ok(version_info) => { + let snapshot_package = &incomplete_snapshot.packages[i]; + if !params.skip_integrity_check { + let registry_integrity = version_info.dist.integrity().for_lockfile(); + if registry_integrity != snapshot_package.integrity { + return Err( + IntegrityCheckFailedError { + package_display_id: snapshot_package.id.as_serialized(), + expected: snapshot_package.integrity.clone(), + actual: registry_integrity, + filename: incomplete_snapshot + .lockfile_file_name + .display() + .to_string(), + } + .into(), + ); + } + } packages.push(SerializedNpmResolutionSnapshotPackage { - id: incomplete_snapshot.packages[i].id.clone(), - dependencies: incomplete_snapshot.packages[i].dependencies.clone(), + id: snapshot_package.id.clone(), + dependencies: snapshot_package.dependencies.clone(), dist: version_info.dist, system: NpmResolutionPackageSystemInfo { cpu: version_info.cpu, @@ -1192,8 +1234,16 @@ mod tests { #[tokio::test] async fn test_snapshot_from_lockfile_v2() { let api = TestNpmRegistryApi::default(); - api.ensure_package_version("emoji-regex", "10.2.1"); - api.ensure_package_version("chalk", "5.3.0"); + api.ensure_package_version_with_integrity( + "chalk", + "5.3.0", + Some("sha512-integrity1"), + ); + api.ensure_package_version_with_integrity( + "emoji-regex", + "10.2.1", + Some("sha512-integrity2"), + ); let lockfile = Lockfile::with_lockfile_content( PathBuf::from("/deno.lock"), @@ -1207,11 +1257,11 @@ mod tests { }, "packages": { "chalk@5.3.0": { - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "integrity": "sha512-integrity1", "dependencies": {} }, "emoji-regex@10.2.1": { - "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==", + "integrity": "sha512-integrity2", "dependencies": {} } } @@ -1223,16 +1273,99 @@ mod tests { let incomplete_snapshot = incomplete_snapshot_from_lockfile(&lockfile).unwrap(); - assert!(snapshot_from_lockfile(incomplete_snapshot, &api) - .await - .is_ok()); + assert!(snapshot_from_lockfile(SnapshotFromLockfileParams { + incomplete_snapshot, + api: &api, + skip_integrity_check: false + }) + .await + .is_ok()); + } + + #[tokio::test] + async fn test_snapshot_from_lockfile_bad_integrity() { + let api = TestNpmRegistryApi::default(); + api.ensure_package_version_with_integrity( + "chalk", + "5.3.0", + Some("sha512-integrity1-bad"), + ); + api.ensure_package_version_with_integrity( + "emoji-regex", + "10.2.1", + Some("sha512-integrity2"), + ); + + let lockfile = Lockfile::with_lockfile_content( + PathBuf::from("/deno.lock"), + r#"{ + "version": "2", + "remote": {}, + "npm": { + "specifiers": { + "chalk@5": "chalk@5.3.0", + "emoji-regex": "emoji-regex@10.2.1" + }, + "packages": { + "chalk@5.3.0": { + "integrity": "sha512-integrity1", + "dependencies": {} + }, + "emoji-regex@10.2.1": { + "integrity": "sha512-integrity2", + "dependencies": {} + } + } + } + }"#, + false, + ) + .unwrap(); + + let incomplete_snapshot = + incomplete_snapshot_from_lockfile(&lockfile).unwrap(); + let err = snapshot_from_lockfile(SnapshotFromLockfileParams { + incomplete_snapshot, + api: &api, + skip_integrity_check: false, + }) + .await + .unwrap_err(); + match err { + SnapshotFromLockfileError::IntegrityCheckFailed(err) => { + assert_eq!(err.actual, "sha512-integrity1-bad"); + assert_eq!(err.expected, "sha512-integrity1"); + assert_eq!(err.filename, "/deno.lock"); + assert_eq!(err.package_display_id, "chalk@5.3.0"); + } + _ => unreachable!(), + } + + // now try with skipping the integrity check + let incomplete_snapshot = + incomplete_snapshot_from_lockfile(&lockfile).unwrap(); + assert!(snapshot_from_lockfile(SnapshotFromLockfileParams { + incomplete_snapshot, + api: &api, + skip_integrity_check: true, // will pass because ignored + }) + .await + .is_ok()); } #[tokio::test] async fn test_snapshot_from_lockfile_v3() { let api = TestNpmRegistryApi::default(); - api.ensure_package_version("emoji-regex", "10.2.1"); - api.ensure_package_version("chalk", "5.3.0"); + api.ensure_package_version_with_integrity( + "chalk", + "5.3.0", + Some("sha512-integrity1"), + ); + api.ensure_package_version_with_integrity( + "emoji-regex", + "10.2.1", + Some("sha512-integrity2"), + ); let lockfile = Lockfile::with_lockfile_content( PathBuf::from("/deno.lock"), @@ -1247,11 +1380,11 @@ mod tests { }, "npm": { "chalk@5.3.0": { - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "integrity": "sha512-integrity1", "dependencies": {} }, "emoji-regex@10.2.1": { - "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==", + "integrity": "sha512-integrity2", "dependencies": {} } } @@ -1263,9 +1396,13 @@ mod tests { let incomplete_snapshot = incomplete_snapshot_from_lockfile(&lockfile).unwrap(); - let snapshot = snapshot_from_lockfile(incomplete_snapshot, &api) - .await - .unwrap(); + let snapshot = snapshot_from_lockfile(SnapshotFromLockfileParams { + incomplete_snapshot, + api: &api, + skip_integrity_check: false, + }) + .await + .unwrap(); assert_eq!( snapshot.as_serialized().root_packages, HashMap::from([