pax-compiler 0.39.0

Compiler APIs for parsing and building Pax projects into application executables
Documentation
use flate2::read::GzDecoder;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::fs;
use std::io::{Cursor, Read};
use std::path::{Component, Path};

const BUNDLE: &[u8] = include_bytes!("../files/new-project/bundled-examples.paxbundle");
const MAGIC: &[u8] = b"PAXBUNDLE1\n";

#[derive(Clone, Debug, Deserialize)]
pub struct BundledExample {
    pub name: String,
    pub description: String,
    pub caveats: String,
    pub sha256: String,
}

#[derive(Debug, Deserialize)]
struct BundleManifest {
    default: String,
    examples: Vec<BundledExample>,
}

struct BundleFile {
    path: String,
    data: Vec<u8>,
    mode: u32,
}

struct ParsedBundle {
    manifest: BundleManifest,
    files: Vec<BundleFile>,
}

pub fn selected_example_name(requested: Option<&str>) -> Result<String, String> {
    let bundle = parse_bundle()?;
    let selected = requested.unwrap_or(&bundle.manifest.default);
    if bundle
        .manifest
        .examples
        .iter()
        .any(|entry| entry.name == selected)
    {
        Ok(selected.to_string())
    } else {
        Err(unknown_example_message(selected, &bundle.manifest.examples))
    }
}

pub fn extract_example(name: Option<&str>, destination: &Path) -> Result<String, String> {
    let bundle = parse_bundle()?;
    let selected_name = name.unwrap_or(&bundle.manifest.default);
    let selected = bundle
        .manifest
        .examples
        .iter()
        .find(|entry| entry.name == selected_name)
        .ok_or_else(|| unknown_example_message(selected_name, &bundle.manifest.examples))?;

    let prefix = format!("{selected_name}/");
    let mut selected_files = Vec::new();
    for file in bundle.files {
        if let Some(relative) = file.path.strip_prefix(&prefix) {
            validate_relative_path(relative)?;
            selected_files.push((relative.to_string(), file.data, file.mode));
        }
    }
    if selected_files.is_empty() {
        return Err(format!(
            "Bundled example `{selected_name}` contains no files"
        ));
    }
    validate_digest(selected, &selected_files)?;

    for (relative, data, mode) in selected_files {
        let output = destination.join(relative);
        if let Some(parent) = output.parent() {
            fs::create_dir_all(parent).map_err(|error| error.to_string())?;
        }
        fs::write(&output, data).map_err(|error| error.to_string())?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            fs::set_permissions(&output, fs::Permissions::from_mode(mode))
                .map_err(|error| error.to_string())?;
        }
    }
    Ok(selected_name.to_string())
}

fn parse_bundle() -> Result<ParsedBundle, String> {
    let mut decoded = Vec::new();
    GzDecoder::new(BUNDLE)
        .read_to_end(&mut decoded)
        .map_err(|error| format!("Failed to decompress bundled examples: {error}"))?;
    let mut cursor = Cursor::new(decoded.as_slice());
    let mut magic = vec![0; MAGIC.len()];
    cursor
        .read_exact(&mut magic)
        .map_err(|error| error.to_string())?;
    if magic != MAGIC {
        return Err("Invalid bundled-example header".to_string());
    }
    let manifest_len = read_u32(&mut cursor)? as usize;
    let mut manifest_bytes = vec![0; manifest_len];
    cursor
        .read_exact(&mut manifest_bytes)
        .map_err(|error| error.to_string())?;
    let manifest: BundleManifest = serde_json::from_slice(&manifest_bytes)
        .map_err(|error| format!("Invalid bundled-example registry: {error}"))?;
    validate_manifest(&manifest)?;

    let file_count = read_u32(&mut cursor)? as usize;
    let mut files = Vec::with_capacity(file_count);
    for _ in 0..file_count {
        let path_len = read_u32(&mut cursor)? as usize;
        let data_len = read_u64(&mut cursor)? as usize;
        let mode = read_u32(&mut cursor)?;
        let mut path = vec![0; path_len];
        cursor
            .read_exact(&mut path)
            .map_err(|error| error.to_string())?;
        let path = String::from_utf8(path).map_err(|error| error.to_string())?;
        validate_relative_path(&path)?;
        let mut data = vec![0; data_len];
        cursor
            .read_exact(&mut data)
            .map_err(|error| error.to_string())?;
        files.push(BundleFile { path, data, mode });
    }
    if cursor.position() != decoded.len() as u64 {
        return Err("Bundled examples contain trailing data".to_string());
    }
    Ok(ParsedBundle { manifest, files })
}

fn validate_manifest(manifest: &BundleManifest) -> Result<(), String> {
    if !manifest
        .examples
        .iter()
        .any(|entry| entry.name == manifest.default)
    {
        return Err("Bundled-example default is unavailable".to_string());
    }
    let mut names = std::collections::HashSet::new();
    for example in &manifest.examples {
        if example.name.is_empty()
            || !example
                .name
                .chars()
                .all(|value| value.is_ascii_lowercase() || value.is_ascii_digit() || value == '-')
            || !names.insert(&example.name)
        {
            return Err(format!("Invalid bundled example name `{}`", example.name));
        }
    }
    Ok(())
}

fn validate_relative_path(path: &str) -> Result<(), String> {
    if path.is_empty()
        || Path::new(path)
            .components()
            .any(|component| !matches!(component, Component::Normal(_)))
    {
        return Err(format!("Unsafe bundled-example path `{path}`"));
    }
    Ok(())
}

fn validate_digest(
    example: &BundledExample,
    files: &[(String, Vec<u8>, u32)],
) -> Result<(), String> {
    let mut digest = Sha256::new();
    for (path, data, mode) in files {
        digest.update(path.as_bytes());
        digest.update([0]);
        digest.update(mode.to_string().as_bytes());
        digest.update([0]);
        digest.update(data);
    }
    if format!("{:x}", digest.finalize()) != example.sha256 {
        return Err(format!(
            "Bundled example `{}` failed validation",
            example.name
        ));
    }
    Ok(())
}

fn unknown_example_message(name: &str, examples: &[BundledExample]) -> String {
    let available = examples
        .iter()
        .map(|entry| {
            format!(
                "  {} — {}\n    Note: {}",
                entry.name, entry.description, entry.caveats
            )
        })
        .collect::<Vec<_>>()
        .join("\n");
    format!("Unknown bundled example `{name}`. Available examples:\n{available}")
}

fn read_u32(cursor: &mut Cursor<&[u8]>) -> Result<u32, String> {
    let mut bytes = [0; 4];
    cursor
        .read_exact(&mut bytes)
        .map_err(|error| error.to_string())?;
    Ok(u32::from_be_bytes(bytes))
}

fn read_u64(cursor: &mut Cursor<&[u8]>) -> Result<u64, String> {
    let mut bytes = [0; 8];
    cursor
        .read_exact(&mut bytes)
        .map_err(|error| error.to_string())?;
    Ok(u64::from_be_bytes(bytes))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn registry_is_valid_and_has_expected_default() {
        let bundle = parse_bundle().unwrap();
        assert_eq!(bundle.manifest.default, "living-quilt");
        assert!(bundle
            .manifest
            .examples
            .iter()
            .any(|entry| entry.name == "ink-and-light"));
        assert!(bundle
            .manifest
            .examples
            .iter()
            .any(|entry| entry.name == "increment"));
    }

    #[test]
    fn unsafe_paths_are_rejected() {
        assert!(validate_relative_path("src/lib.rs").is_ok());
        assert!(validate_relative_path("../Cargo.toml").is_err());
        assert!(validate_relative_path("/tmp/project").is_err());
    }

    #[test]
    fn filtered_build_outputs_are_absent() {
        let bundle = parse_bundle().unwrap();
        assert!(bundle.files.iter().all(|file| {
            !file.path.split('/').any(|part| {
                matches!(
                    part,
                    ".git" | ".pax" | "target" | "node_modules" | "__pycache__"
                )
            }) && !file.path.ends_with("Cargo.lock")
        }));
        assert!(bundle
            .files
            .iter()
            .any(|file| file.path == "living-quilt/src/animated_pax_logo_banner.rs"));
        assert!(bundle
            .files
            .iter()
            .any(|file| file.path == "ink-and-light/assets/moon-postage.png"));
    }

    #[test]
    fn canonical_examples_use_selective_debug_profiles() {
        let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
        let examples = workspace.join("examples/src");
        if !examples.is_dir() {
            return;
        }
        let mut checked = 0;
        for entry in fs::read_dir(examples).unwrap() {
            let manifest = entry.unwrap().path().join("Cargo.toml");
            if !manifest.is_file() {
                continue;
            }
            let doc = fs::read_to_string(&manifest)
                .unwrap()
                .parse::<toml_edit::Document>()
                .unwrap();
            let name = doc["package"]["name"].as_str().unwrap();
            assert_eq!(
                doc["profile"]["dev"]["opt-level"].as_integer(),
                Some(1),
                "{}",
                manifest.display()
            );
            assert_eq!(
                doc["profile"]["dev"]["package"][name]["opt-level"].as_integer(),
                Some(0),
                "{}",
                manifest.display()
            );
            assert!(
                !doc["profile"]["dev"]["package"]
                    .as_table()
                    .unwrap()
                    .contains_key("*"),
                "{} must not optimize build dependencies through a wildcard",
                manifest.display()
            );
            checked += 1;
        }
        assert!(checked > 0);
    }

    #[test]
    fn checked_in_bundle_matches_canonical_examples() {
        let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
        let sync = workspace.join("scripts/sync-cli-examples.py");
        if !sync.is_file() {
            return;
        }
        let status = std::process::Command::new("python3")
            .arg(sync)
            .arg("--check")
            .current_dir(workspace)
            .status()
            .expect("failed to run bundled-example drift check");
        assert!(status.success());
    }
}