bcode_ipc 0.0.1-alpha.0

Client/server IPC protocol for bcode
use sha2::{Digest as _, Sha256};
use std::fs;
use std::path::{Path, PathBuf};

const FALLBACK_INPUT: &str = concat!(env!("CARGO_PKG_NAME"), "-", env!("CARGO_PKG_VERSION"));
const SOURCE_ROOTS: &[&str] = &["catalog", "packages", "plugins", "workers"];
const ROOT_SOURCE_FILES: &[&str] = &[
    "Cargo.lock",
    "Cargo.toml",
    "rust-toolchain.toml",
    "clippy.toml",
];

fn main() {
    println!("cargo:rerun-if-env-changed=BCODE_BUILD_FINGERPRINT");
    println!("cargo:rerun-if-env-changed=BCODE_ARTIFACT_ID");
    println!(
        "cargo:rustc-env=BCODE_ARTIFACT_ID_EXPLICIT={}",
        std::env::var_os("BCODE_ARTIFACT_ID").is_some()
    );
    let artifact_id = match std::env::var("BCODE_ARTIFACT_ID") {
        Ok(value) if is_valid_artifact_id(&value) => value,
        Ok(value) => panic!("invalid BCODE_ARTIFACT_ID {value:?}"),
        Err(std::env::VarError::NotPresent) => generated_artifact_id(),
        Err(std::env::VarError::NotUnicode(_)) => panic!("BCODE_ARTIFACT_ID is not valid Unicode"),
    };
    println!("cargo:rustc-env=BCODE_ARTIFACT_ID={artifact_id}");
    if let Ok(value) = std::env::var("BCODE_BUILD_FINGERPRINT")
        && is_valid_fingerprint(&value)
    {
        println!("cargo:rustc-env=BCODE_BUILD_FINGERPRINT={value}");
        return;
    }

    let workspace_root = workspace_root();
    let source_files = source_files(&workspace_root).unwrap_or_default();
    for path in &source_files {
        println!("cargo:rerun-if-changed={}", path.display());
    }
    for root in SOURCE_ROOTS {
        println!(
            "cargo:rerun-if-changed={}",
            workspace_root.join(root).display()
        );
    }

    let fingerprint = source_fingerprint(&workspace_root, &source_files)
        .unwrap_or_else(|| short_sha256(FALLBACK_INPUT.as_bytes()));
    println!("cargo:rustc-env=BCODE_BUILD_FINGERPRINT={fingerprint}");
}

fn workspace_root() -> PathBuf {
    // Build-script executables can be reused across worktrees sharing a target
    // directory. Cargo supplies the active manifest at execution time; baking it
    // into the executable can watch and fingerprint a different checkout.
    let manifest = PathBuf::from(
        std::env::var_os("CARGO_MANIFEST_DIR").expect("Cargo supplies the manifest directory"),
    );
    let workspace = manifest.join("../..");
    workspace.canonicalize().unwrap_or(workspace)
}

fn source_files(workspace_root: &Path) -> std::io::Result<Vec<PathBuf>> {
    let mut files = ROOT_SOURCE_FILES
        .iter()
        .map(|path| workspace_root.join(path))
        .filter(|path| path.is_file())
        .collect::<Vec<_>>();
    for root in SOURCE_ROOTS {
        collect_files(&workspace_root.join(root), &mut files)?;
    }
    files.sort_by(|left, right| {
        relative_path(workspace_root, left).cmp(&relative_path(workspace_root, right))
    });
    Ok(files)
}

fn collect_files(directory: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
    let mut entries = match fs::read_dir(directory) {
        Ok(entries) => entries.collect::<Result<Vec<_>, _>>()?,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
        Err(error) => return Err(error),
    };
    entries.sort_by_key(std::fs::DirEntry::file_name);
    for entry in entries {
        let path = entry.path();
        let file_type = entry.file_type()?;
        if file_type.is_dir() {
            collect_files(&path, files)?;
        } else if file_type.is_file() {
            files.push(path);
        }
    }
    Ok(())
}

fn source_fingerprint(workspace_root: &Path, files: &[PathBuf]) -> Option<String> {
    let mut digest = Sha256::new();
    for path in files {
        let relative = relative_path(workspace_root, path);
        let contents = fs::read(path).ok()?;
        update_length_prefixed(&mut digest, relative.as_bytes());
        update_length_prefixed(&mut digest, &contents);
    }
    Some(short_digest(digest.finalize().as_slice()))
}

fn relative_path(workspace_root: &Path, path: &Path) -> String {
    path.strip_prefix(workspace_root)
        .unwrap_or(path)
        .to_string_lossy()
        .replace('\\', "/")
}

fn update_length_prefixed(digest: &mut Sha256, value: &[u8]) {
    digest.update(u64::try_from(value.len()).unwrap_or(u64::MAX).to_le_bytes());
    digest.update(value);
}

fn short_sha256(input: &[u8]) -> String {
    short_digest(Sha256::digest(input).as_slice())
}

fn short_digest(digest: &[u8]) -> String {
    let hex = digest
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect::<String>();
    hex[..16].to_string()
}

fn generated_artifact_id() -> String {
    let generated_at = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |duration| duration.as_nanos());
    let mut digest = Sha256::new();
    let values = [
        env!("CARGO_PKG_NAME").to_owned(),
        env!("CARGO_PKG_VERSION").to_owned(),
        std::env::var("PROFILE").unwrap_or_default(),
        std::env::var("TARGET").unwrap_or_default(),
        std::process::id().to_string(),
        generated_at.to_string(),
    ];
    for value in values {
        update_length_prefixed(&mut digest, value.as_bytes());
    }
    format!("cargo-{}", short_digest(digest.finalize().as_slice()))
}

fn is_valid_artifact_id(value: &str) -> bool {
    !value.is_empty()
        && value.len() <= 128
        && value
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
}

fn is_valid_fingerprint(value: &str) -> bool {
    !value.is_empty()
        && value.len() <= 64
        && value
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
}