@ 261027.7
current
//! Host-side release verification. Decode before extracting or replacing an
//! artifact; replacement never falls back to copying over the raw addon.

mod carrier;

use std::{
    env, fs,
    io::Write,
    path::{Path, PathBuf},
};

use sha2::{Digest, Sha512};
use swc_native_addon::{
    format::{NativeTarget, HEADER_LEN, MAX_SIZE},
    platform,
};

fn output_path(path: &Path) -> carrier::Result<PathBuf> {
    if path.extension().and_then(|extension| extension.to_str()) != Some("node") {
        return Err("destination must be a .node file, never a CLI executable".into());
    }
    let parent = path
        .parent()
        .filter(|p| !p.as_os_str().is_empty())
        .unwrap_or(Path::new("."));
    Ok(fs::canonicalize(parent)?.join(path.file_name().ok_or("destination filename is required")?))
}

fn run() -> carrier::Result<()> {
    let mut args = env::args_os().skip(1);
    let mut input = None;
    let mut target = None;
    let mut raw = None;
    let mut extract = None;
    let mut replace = None;
    while let Some(arg) = args.next() {
        let destination = match arg.to_str() {
            Some("--input") => &mut input,
            Some("--target") => &mut target,
            Some("--raw") => &mut raw,
            Some("--extract") => &mut extract,
            Some("--replace") => &mut replace,
            _ => return Err("expected --input, --target, --raw, --extract, or --replace".into()),
        };
        if destination.is_some() {
            return Err("duplicate verifier argument".into());
        }
        *destination = Some(args.next().ok_or("missing verifier argument value")?);
    }
    let input = fs::canonicalize(input.ok_or("--input is required")?)?;
    let target = target.ok_or("--target is required")?;
    let target = NativeTarget::parse(target.to_str().ok_or("target must be UTF-8")?)?;
    if fs::metadata(&input)?.len() > MAX_SIZE {
        return Err("carrier exceeds 2 GiB".into());
    }
    let bytes = fs::read(&input)?;
    let payload = carrier::payload(&bytes, target)?;
    if bytes.len() as u64 >= payload.header.raw_len {
        return Err("carrier must be smaller than the stripped raw addon".into());
    }
    let mut decoded = tempfile::tempfile()?;
    payload.decode_into(&mut decoded)?;
    // The final artifact must agree with both its runtime BLAKE3 metadata and
    // the unchanged SHA-512 release identity in the v1 payload header.
    payload.header.verify(&mut decoded)?;
    payload.verify_target(&mut decoded)?;
    let raw = raw.map(fs::canonicalize).transpose()?;
    if let Some(path) = &raw {
        payload.header.verify(&mut fs::File::open(path)?)?;
    }
    if extract.is_some() && replace.is_some() {
        return Err("--extract and --replace are mutually exclusive".into());
    }
    if let Some(path) = extract {
        let path = output_path(Path::new(&path))?;
        if path.exists() {
            return Err("extraction destination already exists".into());
        }
        let mut stage = tempfile::NamedTempFile::new_in(path.parent().unwrap())?;
        use std::io::{Seek, SeekFrom};
        decoded.seek(SeekFrom::Start(0))?;
        std::io::copy(&mut decoded, &mut stage)?;
        stage.as_file().sync_all()?;
        stage.persist_noclobber(&path)?;
        platform::sync_directory(path.parent().unwrap())?;
    }
    if let Some(path) = replace {
        let path = output_path(Path::new(&path))?;
        if raw.as_ref() != Some(&fs::canonicalize(&path)?) || path == input {
            return Err(
                "--replace must name the verified --raw addon, distinct from the carrier".into(),
            );
        }
        let parent = path.parent().unwrap();
        let mut stage = tempfile::Builder::new()
            .prefix(".native-carrier-")
            .tempfile_in(parent)?;
        stage
            .as_file()
            .set_permissions(fs::metadata(&path)?.permissions())?;
        stage.write_all(&bytes)?;
        stage.as_file().sync_all()?;
        // Close all writable handles before MoveFileExW on Windows.
        let stage = stage.into_temp_path();
        platform::replace_file(&stage, &path)?;
        platform::sync_directory(parent)?;
    }
    println!(
        "{}",
        serde_json::json!({
            "kind": "carrier",
            "target": target.as_triple(),
            "rawSize": payload.header.raw_len,
            "rawSha512": payload.header.cache_key(),
            "compressedSize": payload.header.compressed_len,
            "payloadSize": HEADER_LEN as u64 + payload.header.compressed_len,
            "carrierSize": bytes.len(),
            "carrierSha512": format!("{:x}", Sha512::digest(&bytes)),
            "reduction": 1.0 - bytes.len() as f64 / payload.header.raw_len as f64,
        })
    );
    Ok(())
}

fn main() {
    if let Err(error) = run() {
        eprintln!("swc-native-addon-verify: {error}");
        std::process::exit(1);
    }
}