deno/cli/npm/tarball.rs
David Sherret cbb3f85433
feat(unstable/npm): support peer dependencies (#16561)
This adds support for peer dependencies in npm packages.

1. If not found higher in the tree (ancestor and ancestor siblings),
peer dependencies are resolved like a dependency similar to npm 7.
2. Optional peer dependencies are only resolved if found higher in the
tree.
3. This creates "copy packages" or duplicates of a package when a
package has different resolution due to peer dependency resolution—see
https://pnpm.io/how-peers-are-resolved. Unlike pnpm though, duplicates
of packages will have `_1`, `_2`, etc. added to the end of the package
version in the directory in order to minimize the chance of hitting the
max file path limit on Windows. This is done for both the local
"node_modules" directory and also the global npm cache. The files are
hard linked in this case to reduce hard drive space.

This is a first pass and the code is definitely more inefficient than it
could be.

Closes #15823
2022-11-08 14:17:24 -05:00

166 lines
4.8 KiB
Rust

// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use flate2::read::GzDecoder;
use tar::Archive;
use tar::EntryType;
use super::cache::with_folder_sync_lock;
use super::registry::NpmPackageVersionDistInfo;
use super::semver::NpmVersion;
pub fn verify_and_extract_tarball(
package: (&str, &NpmVersion),
data: &[u8],
dist_info: &NpmPackageVersionDistInfo,
output_folder: &Path,
) -> Result<(), AnyError> {
if let Some(integrity) = &dist_info.integrity {
verify_tarball_integrity(package, data, integrity)?;
} else {
// todo(dsherret): check shasum here
bail!(
"Errored on '{}@{}': npm packages with no integrity are not implemented.",
package.0,
package.1,
);
}
with_folder_sync_lock(package, output_folder, || {
extract_tarball(data, output_folder)
})
}
fn verify_tarball_integrity(
package: (&str, &NpmVersion),
data: &[u8],
npm_integrity: &str,
) -> Result<(), AnyError> {
use ring::digest::Context;
use ring::digest::SHA512;
let (algo, expected_checksum) = match npm_integrity.split_once('-') {
Some((hash_kind, checksum)) => {
let algo = match hash_kind {
"sha512" => &SHA512,
hash_kind => bail!(
"Not implemented hash function for {}@{}: {}",
package.0,
package.1,
hash_kind
),
};
(algo, checksum.to_lowercase())
}
None => bail!(
"Not implemented integrity kind for {}@{}: {}",
package.0,
package.1,
npm_integrity
),
};
let mut hash_ctx = Context::new(algo);
hash_ctx.update(data);
let digest = hash_ctx.finish();
let tarball_checksum = base64::encode(digest.as_ref()).to_lowercase();
if tarball_checksum != expected_checksum {
bail!(
"Tarball checksum did not match what was provided by npm registry for {}@{}.\n\nExpected: {}\nActual: {}",
package.0,
package.1,
expected_checksum,
tarball_checksum,
)
}
Ok(())
}
fn extract_tarball(data: &[u8], output_folder: &Path) -> Result<(), AnyError> {
fs::create_dir_all(output_folder)?;
let output_folder = fs::canonicalize(output_folder)?;
let tar = GzDecoder::new(data);
let mut archive = Archive::new(tar);
archive.set_overwrite(true);
archive.set_preserve_permissions(true);
let mut created_dirs = HashSet::new();
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
let entry_type = entry.header().entry_type();
// Some package tarballs contain "pax_global_header", these entries
// should be skipped.
if entry_type == EntryType::XGlobalHeader {
continue;
}
// skip the first component which will be either "package" or the name of the package
let relative_path = path.components().skip(1).collect::<PathBuf>();
let absolute_path = output_folder.join(relative_path);
let dir_path = if entry_type == EntryType::Directory {
absolute_path.as_path()
} else {
absolute_path.parent().unwrap()
};
if created_dirs.insert(dir_path.to_path_buf()) {
fs::create_dir_all(&dir_path)?;
let canonicalized_dir = fs::canonicalize(&dir_path)?;
if !canonicalized_dir.starts_with(&output_folder) {
bail!(
"Extracted directory '{}' of npm tarball was not in output directory.",
canonicalized_dir.display()
)
}
}
if entry.header().entry_type() == EntryType::Regular {
entry.unpack(&absolute_path)?;
}
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::npm::semver::NpmVersion;
#[test]
pub fn test_verify_tarball() {
let package_name = "package".to_string();
let package_version = NpmVersion::parse("1.0.0").unwrap();
let package = (package_name.as_str(), &package_version);
let actual_checksum =
"z4phnx7vul3xvchq1m2ab9yg5aulvxxcg/spidns6c5h0ne8xyxysp+dgnkhfuwvy7kxvudbeoglodj6+sfapg==";
assert_eq!(
verify_tarball_integrity(package, &Vec::new(), "test")
.unwrap_err()
.to_string(),
"Not implemented integrity kind for package@1.0.0: test",
);
assert_eq!(
verify_tarball_integrity(package, &Vec::new(), "sha1-test")
.unwrap_err()
.to_string(),
"Not implemented hash function for package@1.0.0: sha1",
);
assert_eq!(
verify_tarball_integrity(package, &Vec::new(), "sha512-test")
.unwrap_err()
.to_string(),
format!("Tarball checksum did not match what was provided by npm registry for package@1.0.0.\n\nExpected: test\nActual: {}", actual_checksum),
);
assert!(verify_tarball_integrity(
package,
&Vec::new(),
&format!("sha512-{}", actual_checksum)
)
.is_ok());
}
}