kcode-k1-rust-package 0.2.1

Validate authority-namespaced K1 Rust package source
Documentation
use kcode_k1_transaction_id::TxId;
use semver::{Version, VersionReq};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use toml::{Table, Value};

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct AuthorityId(TxId);

impl AuthorityId {
    pub const fn new(transaction_id: TxId) -> Self {
        Self(transaction_id)
    }

    pub const fn transaction_id(&self) -> &TxId {
        &self.0
    }
}

impl Display for AuthorityId {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        Display::fmt(&self.0, formatter)
    }
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LibraryFamily {
    authority: AuthorityId,
    logical_name: String,
}

impl LibraryFamily {
    pub fn new(
        authority: AuthorityId,
        logical_name: impl Into<String>,
    ) -> Result<Self, PackageError> {
        let logical_name = logical_name.into();
        validate_logical_name(&logical_name)?;
        Ok(Self {
            authority,
            logical_name,
        })
    }

    pub const fn authority(&self) -> AuthorityId {
        self.authority
    }

    pub fn logical_name(&self) -> &str {
        &self.logical_name
    }

    pub fn package_name(&self) -> String {
        format!("k1-{}-{}", self.authority, self.logical_name)
    }
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LibraryId {
    family: LibraryFamily,
    version: Version,
}

impl LibraryId {
    pub fn new(family: LibraryFamily, version: Version) -> Result<Self, PackageError> {
        validate_stable(&version)?;
        Ok(Self { family, version })
    }

    pub fn family(&self) -> &LibraryFamily {
        &self.family
    }

    pub fn version(&self) -> &Version {
        &self.version
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourceFile {
    path: String,
    bytes: Vec<u8>,
}

impl SourceFile {
    pub fn new(path: impl Into<String>, bytes: Vec<u8>) -> Self {
        Self {
            path: path.into(),
            bytes,
        }
    }

    pub fn path(&self) -> &str {
        &self.path
    }

    pub fn bytes(&self) -> &[u8] {
        &self.bytes
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct K1Dependency {
    alias: String,
    family: LibraryFamily,
    requirement: VersionReq,
}

impl K1Dependency {
    pub fn alias(&self) -> &str {
        &self.alias
    }

    pub fn family(&self) -> &LibraryFamily {
        &self.family
    }

    pub fn requirement(&self) -> &VersionReq {
        &self.requirement
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourcePackage {
    id: LibraryId,
    files: Vec<SourceFile>,
    dependencies: Vec<K1Dependency>,
}

impl SourcePackage {
    pub fn new(id: LibraryId, mut files: Vec<SourceFile>) -> Result<Self, PackageError> {
        files
            .iter()
            .try_for_each(|file| validate_path(file.path()))?;
        files.sort_by(|left, right| left.path.cmp(&right.path));
        if files.windows(2).any(|pair| pair[0].path == pair[1].path) {
            return fail("duplicate source path");
        }
        if files.iter().any(|file| has_ancestor(&files, file.path())) {
            return fail("source path collides with an ancestor");
        }
        let manifest = required_utf8(&files, "Cargo.toml")?;
        required_utf8(&files, "Documentation.md")?;
        let dependencies = validate_manifest(&id, manifest)?;
        Ok(Self {
            id,
            files,
            dependencies,
        })
    }

    pub fn id(&self) -> &LibraryId {
        &self.id
    }

    pub fn files(&self) -> &[SourceFile] {
        &self.files
    }

    pub fn dependencies(&self) -> &[K1Dependency] {
        &self.dependencies
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PackageError(String);

impl PackageError {
    pub fn message(&self) -> &str {
        &self.0
    }
}

impl Display for PackageError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(&self.0)
    }
}

impl std::error::Error for PackageError {}

fn fail<T>(message: impl Into<String>) -> Result<T, PackageError> {
    Err(PackageError(message.into()))
}

fn validate_logical_name(name: &str) -> Result<(), PackageError> {
    let valid = !name.is_empty()
        && name.len() <= 36
        && name.split('-').all(|part| {
            !part.is_empty()
                && part
                    .bytes()
                    .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
        });
    if !valid {
        return fail("logical name must be 1-36 bytes of lowercase kebab case");
    }
    Ok(())
}

fn validate_stable(version: &Version) -> Result<(), PackageError> {
    if !version.pre.is_empty() || !version.build.is_empty() {
        return fail("version must be stable");
    }
    Ok(())
}

fn validate_path(path: &str) -> Result<(), PackageError> {
    if path.is_empty()
        || path.len() > 4096
        || path.starts_with('/')
        || path.contains(['\\', '\0'])
        || path.split('/').any(|component| {
            component.is_empty() || matches!(component, "." | "..") || component.len() > 255
        })
    {
        return fail("invalid source path");
    }
    if path == "Cargo.lock" {
        return fail("root Cargo.lock is generated by K1");
    }
    Ok(())
}

fn has_ancestor(files: &[SourceFile], path: &str) -> bool {
    path.match_indices('/').any(|(index, _)| {
        files
            .binary_search_by(|file| file.path.as_str().cmp(&path[..index]))
            .is_ok()
    })
}

fn required_utf8<'a>(files: &'a [SourceFile], path: &str) -> Result<&'a str, PackageError> {
    let file = files
        .iter()
        .find(|file| file.path == path)
        .ok_or_else(|| PackageError(format!("missing {path}")))?;
    std::str::from_utf8(file.bytes()).map_err(|_| PackageError(format!("{path} must be UTF-8")))
}

fn validate_manifest(id: &LibraryId, source: &str) -> Result<Vec<K1Dependency>, PackageError> {
    let root: Table = source
        .parse()
        .map_err(|cause| PackageError(format!("invalid Cargo.toml: {cause}")))?;
    let package = root
        .get("package")
        .and_then(Value::as_table)
        .ok_or_else(|| PackageError("missing [package]".into()))?;
    require_text(package, "name", &id.family.package_name())?;
    require_text(package, "version", &id.version.to_string())?;

    let mut dependencies = Vec::new();
    scan_dependency_sets(&root, &mut dependencies)?;
    if let Some(targets) = root.get("target").and_then(Value::as_table) {
        for target in targets.values().filter_map(Value::as_table) {
            scan_dependency_sets(target, &mut dependencies)?;
        }
    }
    dependencies.sort_by(|left, right| {
        (&left.alias, &left.family, left.requirement.to_string()).cmp(&(
            &right.alias,
            &right.family,
            right.requirement.to_string(),
        ))
    });
    Ok(dependencies)
}

fn require_text(table: &Table, key: &str, expected: &str) -> Result<(), PackageError> {
    if table.get(key).and_then(Value::as_str) != Some(expected) {
        return fail(format!("package {key} must equal {expected}"));
    }
    Ok(())
}

fn scan_dependency_sets(
    table: &Table,
    dependencies: &mut Vec<K1Dependency>,
) -> Result<(), PackageError> {
    for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
        let Some(entries) = table.get(section).and_then(Value::as_table) else {
            continue;
        };
        for (alias, specification) in entries {
            scan_dependency(alias, specification, dependencies)?;
        }
    }
    Ok(())
}

fn scan_dependency(
    alias: &str,
    specification: &Value,
    dependencies: &mut Vec<K1Dependency>,
) -> Result<(), PackageError> {
    let Some(table) = specification.as_table() else {
        return Ok(());
    };
    if table.get("registry").and_then(Value::as_str) != Some("k1") {
        return Ok(());
    }
    let package = text(
        table,
        "package",
        format!("K1 dependency {alias} needs package"),
    )?;
    let family = parse_package_name(package)?;
    let requirement = text(
        table,
        "version",
        format!("K1 dependency {alias} needs version"),
    )?;
    dependencies.push(K1Dependency {
        alias: alias.into(),
        family,
        requirement: parse_requirement(requirement, alias)?,
    });
    Ok(())
}

fn text<'a>(table: &'a Table, key: &str, error: String) -> Result<&'a str, PackageError> {
    table
        .get(key)
        .and_then(Value::as_str)
        .ok_or(PackageError(error))
}

fn parse_requirement(requirement: &str, alias: &str) -> Result<VersionReq, PackageError> {
    VersionReq::parse(requirement)
        .map_err(|_| PackageError(format!("invalid requirement for {alias}")))
}

fn parse_package_name(name: &str) -> Result<LibraryFamily, PackageError> {
    let rest = name
        .strip_prefix("k1-")
        .ok_or_else(|| PackageError("malformed K1 package name".into()))?;
    if rest.len() < 26 || rest.as_bytes().get(24) != Some(&b'-') {
        return fail("malformed K1 package name");
    }
    let authority =
        TxId::from_str(&rest[..24]).map_err(|_| PackageError("malformed K1 authority".into()))?;
    let family = LibraryFamily::new(AuthorityId::new(authority), &rest[25..])?;
    if family.package_name() != name {
        return fail("malformed K1 package name");
    }
    Ok(family)
}