Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/index-rows/src/npm.rs

//! An npm tarball's own `package.json` and its SSRI integrity (moved here from server-lib's
//! `promote::npm`, lane HOL-PERF-2): the scan worker reads them at intake and answers them as
//! an [`crate::IndexRow::Npm`]; the packument is rendered from the row.

use std::io::Read as _;

use base64::Engine as _;
use serde_json::Value;

/// The most of a `package.json` the door reads.
pub const MAX_MANIFEST_BYTES: u64 = 1 << 20;
/// The most a tarball may unpack to while the door looks for its `package.json`.
pub const MAX_UNPACKED_BYTES: u64 = 512 << 20;
/// The most tar members the door walks looking for it.
pub const MAX_MEMBERS: usize = 100_000;

/// SSRI `sha512-<base64>` of `bytes`.
pub fn integrity_of(bytes: &[u8]) -> String {
    use sha2::Digest as _;
    format!(
        "sha512-{}",
        base64::engine::general_purpose::STANDARD.encode(sha2::Sha512::digest(bytes))
    )
}

/// **The tarball's own `package.json`**: the top-level directory's (`package/` as `npm pack`
/// lays it), gunzipped and untarred with bounds on what is read.
pub fn read_manifest(tgz: &[u8]) -> Result<Value, String> {
    crate::counted();
    let gz = flate2::read::GzDecoder::new(tgz).take(MAX_UNPACKED_BYTES);
    let mut archive = tar::Archive::new(gz);
    let entries = archive
        .entries()
        .map_err(|e| format!("the tarball is not a gzipped tar: {e}"))?;
    for (n, entry) in entries.enumerate() {
        if n >= MAX_MEMBERS {
            break;
        }
        let entry = entry.map_err(|e| format!("the tarball is not a gzipped tar: {e}"))?;
        let path = entry
            .path()
            .map_err(|e| format!("a tar member's path: {e}"))?
            .into_owned();
        let parts: Vec<_> = path.components().collect();
        if parts.len() == 2 && parts[1].as_os_str() == "package.json" {
            if entry.header().size().unwrap_or(u64::MAX) > MAX_MANIFEST_BYTES {
                return Err("the tarball's package.json is larger than 1 MiB".into());
            }
            let mut text = String::new();
            entry
                .take(MAX_MANIFEST_BYTES)
                .read_to_string(&mut text)
                .map_err(|e| format!("the tarball's package.json: {e}"))?;
            let v: Value = serde_json::from_str(&text)
                .map_err(|e| format!("the tarball's package.json is not JSON: {e}"))?;
            return if v.is_object() {
                Ok(v)
            } else {
                Err("the tarball's package.json is not an object".into())
            };
        }
    }
    Err("the tarball carries no top-level package.json".into())
}