summaryrefslogtreecommitdiff
path: root/cli/npm/tarball.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2022-08-10 15:23:58 -0400
committerGitHub <noreply@github.com>2022-08-10 15:23:58 -0400
commitd9fae38d1e093fd2578c096203f1bddc18aa8ddb (patch)
treeb3dc3e4f442ffc0b1461f30a3435388a7b3f4b87 /cli/npm/tarball.rs
parentd0ffa0beb52679ddfc90ccc03e27572337db79dc (diff)
feat: add initial internal npm client and dependency resolver (#15446)
Diffstat (limited to 'cli/npm/tarball.rs')
-rw-r--r--cli/npm/tarball.rs188
1 files changed, 188 insertions, 0 deletions
diff --git a/cli/npm/tarball.rs b/cli/npm/tarball.rs
new file mode 100644
index 000000000..f6fcb27bf
--- /dev/null
+++ b/cli/npm/tarball.rs
@@ -0,0 +1,188 @@
+// 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::anyhow::Context;
+use deno_core::error::AnyError;
+use flate2::read::GzDecoder;
+use tar::Archive;
+use tar::EntryType;
+
+use super::cache::NPM_PACKAGE_SYNC_LOCK_FILENAME;
+use super::registry::NpmPackageVersionDistInfo;
+use super::NpmPackageId;
+
+pub fn verify_and_extract_tarball(
+ package: &NpmPackageId,
+ 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
+ );
+ }
+
+ fs::create_dir_all(output_folder).with_context(|| {
+ format!("Error creating '{}'.", output_folder.display())
+ })?;
+
+ // This sync lock file is a way to ensure that partially created
+ // npm package directories aren't considered valid. This could maybe
+ // be a bit smarter in the future to not bother extracting here
+ // if another process has taken the lock in the past X seconds and
+ // wait for the other process to finish (it could try to create the
+ // file with `create_new(true)` then if it exists, check the metadata
+ // then wait until the other process finishes with a timeout), but
+ // for now this is good enough.
+ let sync_lock_path = output_folder.join(NPM_PACKAGE_SYNC_LOCK_FILENAME);
+ match fs::OpenOptions::new()
+ .write(true)
+ .create(true)
+ .open(&sync_lock_path)
+ {
+ Ok(_) => {
+ extract_tarball(data, output_folder)?;
+ // extraction succeeded, so only now delete this file
+ let _ignore = std::fs::remove_file(&sync_lock_path);
+ Ok(())
+ }
+ Err(err) => {
+ bail!(
+ concat!(
+ "Error creating package sync lock file at '{}'. ",
+ "Maybe try manually deleting this folder.\n\n{:#}",
+ ),
+ output_folder.display(),
+ err
+ );
+ }
+ }
+}
+
+fn verify_tarball_integrity(
+ package: &NpmPackageId,
+ 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,
+ hash_kind
+ ),
+ };
+ (algo, checksum.to_lowercase())
+ }
+ None => bail!(
+ "Not implemented integrity kind for {}: {}",
+ package,
+ 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,
+ 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();
+ // 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::*;
+
+ #[test]
+ pub fn test_verify_tarball() {
+ let package_id = NpmPackageId {
+ name: "package".to_string(),
+ version: semver::Version::parse("1.0.0").unwrap(),
+ };
+ let actual_checksum =
+ "z4phnx7vul3xvchq1m2ab9yg5aulvxxcg/spidns6c5h0ne8xyxysp+dgnkhfuwvy7kxvudbeoglodj6+sfapg==";
+ assert_eq!(
+ verify_tarball_integrity(&package_id, &Vec::new(), "test")
+ .unwrap_err()
+ .to_string(),
+ "Not implemented integrity kind for package@1.0.0: test",
+ );
+ assert_eq!(
+ verify_tarball_integrity(&package_id, &Vec::new(), "sha1-test")
+ .unwrap_err()
+ .to_string(),
+ "Not implemented hash function for package@1.0.0: sha1",
+ );
+ assert_eq!(
+ verify_tarball_integrity(&package_id, &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_id,
+ &Vec::new(),
+ &format!("sha512-{}", actual_checksum)
+ )
+ .is_ok());
+ }
+}