kcode-k1-web-bootstrap-import 0.1.0

Authority rewrite, Chromium validation, and KTO import for Loom Web bootstrap source
Documentation
#![doc = include_str!("../Documentation.md")]
#![forbid(unsafe_code)]

use kcode_k1_transaction_id::TxId;
use kcode_k1_web_bootstrap_archive::{Archive, Dependency, Package};
use kcode_k1_web_code_document::{CodeDocument, Language};
use kcode_k1_web_coding::{CheckOutcome, K1WebCoding, WebCodingConfig, WebCodingRevisions};
use kcode_k1_web_package::{AuthorityId, SourcePackage, WebFamily};
use kcode_k1_web_podman::WebPodmanConfig;
use kcode_k1_web_projection::{K1WebProjection, PublishStatus};
use semver::Version;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use std::sync::Arc;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ImporterRevisions {
    pub boot: String,
    pub schema: String,
    pub route: String,
    pub harness: String,
    pub check_policy: String,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ImportedPackage {
    name: String,
    version: Version,
    source_sha256: [u8; 32],
}

impl ImportedPackage {
    pub fn name(&self) -> &str {
        &self.name
    }

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

    pub const fn source_sha256(&self) -> [u8; 32] {
        self.source_sha256
    }
}

#[derive(Clone)]
pub struct WebBootstrapImporter {
    inner: Arc<Inner>,
}

struct Inner {
    cache_root: PathBuf,
    projection_root: PathBuf,
    revisions: ImporterRevisions,
    podman: WebPodmanConfig,
    projection: Arc<K1WebProjection>,
}

impl WebBootstrapImporter {
    pub fn new(
        cache_root: impl Into<PathBuf>,
        projection_root: impl Into<PathBuf>,
        revisions: ImporterRevisions,
        podman: WebPodmanConfig,
        projection: Arc<K1WebProjection>,
    ) -> Result<Self, String> {
        let cache_root = cache_root.into();
        let projection_root = projection_root.into();
        require_directory(&cache_root, "Web bootstrap cache")?;
        require_directory(&projection_root, "Web projection")?;
        if [
            &revisions.boot,
            &revisions.schema,
            &revisions.route,
            &revisions.harness,
            &revisions.check_policy,
        ]
        .iter()
        .any(|value| value.is_empty())
        {
            return Err("Web bootstrap importer revisions must be nonempty".to_owned());
        }
        Ok(Self {
            inner: Arc::new(Inner {
                cache_root,
                projection_root,
                revisions,
                podman,
                projection,
            }),
        })
    }

    pub fn import_all(
        &self,
        archive: &Archive,
        authority: [u8; 12],
        user: [u8; 12],
    ) -> Result<Vec<ImportedPackage>, String> {
        let authority = AuthorityId::new(TxId::from_bytes(authority));
        let prepared = prepare_all(archive, authority)?;
        let revisions = &self.inner.revisions;
        let config = WebCodingConfig::new(
            WebCodingRevisions {
                boot: revisions.boot.clone(),
                schema: revisions.schema.clone(),
                route: revisions.route.clone(),
                harness: revisions.harness.clone(),
                check_policy: revisions.check_policy.clone(),
            },
            self.inner.projection_root.clone(),
            self.inner.podman.clone(),
        )
        .map_err(|error| format!("configure Web bootstrap coding: {error}"))?;
        let mut coding = K1WebCoding::open(
            &self.inner.cache_root,
            TxId::from_bytes(user),
            config,
            Arc::clone(&self.inner.projection),
        )
        .map_err(|error| format!("open Web bootstrap coding: {error}"))?;
        let authorize = |family: &WebFamily| Ok(family.authority() == authority);
        let mut imported = Vec::with_capacity(prepared.len());
        for item in prepared {
            coding
                .write(&item.source, &authorize)
                .map_err(|error| format!("stage Web bootstrap package {}: {error}", item.name))?;
            let check = coding
                .check_fresh(item.source.id(), &authorize)
                .map_err(|error| format!("check Web bootstrap package {}: {error}", item.name))?;
            if matches!(check, CheckOutcome::Reused) {
                return Err(format!("fresh Web check was reused for {}", item.name));
            }
            match self
                .inner
                .projection
                .load(item.source.id())
                .map_err(|error| format!("inspect published Web package {}: {error}", item.name))?
            {
                Some(existing) if existing.as_ref() == &item.source => {}
                Some(_) => {
                    return Err(format!(
                        "published Web package {} {} differs from bootstrap source",
                        item.name, item.version
                    ));
                }
                None => {
                    let completion = coding
                        .publish(item.source.id(), &authorize, &|_, _| Ok(true))
                        .map_err(|error| {
                            format!("publish Web bootstrap package {}: {error}", item.name)
                        })?;
                    if completion.outcome.status == PublishStatus::Conflict {
                        return Err(format!(
                            "published Web package {} {} conflicts with bootstrap source",
                            item.name, item.version
                        ));
                    }
                }
            }
            imported.push(ImportedPackage {
                name: item.name,
                version: item.version,
                source_sha256: item.source_sha256,
            });
        }
        Ok(imported)
    }
}

struct PreparedPackage {
    name: String,
    version: Version,
    source_sha256: [u8; 32],
    source: SourcePackage,
}

fn prepare_all(archive: &Archive, authority: AuthorityId) -> Result<Vec<PreparedPackage>, String> {
    archive
        .packages()
        .iter()
        .map(|package| prepare(package, authority))
        .collect()
}

fn prepare(package: &Package, authority: AuthorityId) -> Result<PreparedPackage, String> {
    let family = WebFamily::new(authority, package.name().to_owned())
        .map_err(|error| format!("assign Web package {} authority: {error}", package.name()))?;
    let authority_text = authority.to_string();
    let documentation = documentation(package, &authority_text)?;
    let code = rewrite_code(package, &authority_text)?;
    let document = CodeDocument::new(family, documentation, Language::JavaScript, code)
        .map_err(|error| format!("generate WebCode document for {}: {error}", package.name()))?;
    let source = document
        .to_source_package(package.version().clone())
        .map_err(|error| format!("generate Web package {}: {error}", package.name()))?;
    Ok(PreparedPackage {
        name: package.name().to_owned(),
        version: package.version().clone(),
        source_sha256: source_digest(&source),
        source,
    })
}

#[derive(Serialize)]
struct Header<'a> {
    dependencies: Vec<HeaderDependency<'a>>,
}

#[derive(Serialize)]
struct HeaderDependency<'a> {
    authority: &'a str,
    name: &'a str,
    selector: &'a str,
}

fn documentation(package: &Package, authority: &str) -> Result<Vec<u8>, String> {
    let dependencies = package
        .dependencies()
        .iter()
        .map(|dependency| HeaderDependency {
            authority,
            name: dependency.name(),
            selector: dependency.selector(),
        })
        .collect();
    let header = serde_json::to_string(&Header { dependencies })
        .map_err(|error| format!("encode Web dependencies for {}: {error}", package.name()))?;
    let mut output = format!("<!-- k1-web/v1\n{header}\n-->\n").into_bytes();
    output.extend_from_slice(package.documentation());
    Ok(output)
}

fn rewrite_code(package: &Package, authority: &str) -> Result<Vec<u8>, String> {
    let source = std::str::from_utf8(package.code())
        .map_err(|_| format!("Code.js for {} is not UTF-8", package.name()))?;
    let names = package
        .dependencies()
        .iter()
        .map(Dependency::name)
        .collect::<Vec<_>>();
    rewrite_routes(source, authority, &names)
        .map(String::into_bytes)
        .map_err(|error| format!("rewrite Web routes for {}: {error}", package.name()))
}

fn rewrite_routes(source: &str, authority: &str, dependencies: &[&str]) -> Result<String, String> {
    let mut rewritten = source.to_owned();
    for name in dependencies {
        let needle = format!("/module/{name}/");
        if !rewritten.contains(&needle) {
            return Err(format!("declared dependency {name} has no module route"));
        }
        rewritten = rewritten.replace(&needle, &format!("/module/{authority}/{name}/"));
    }
    let prefix = format!("/module/{authority}/");
    if rewritten
        .match_indices("/module/")
        .any(|(index, _)| !rewritten[index..].starts_with(&prefix))
    {
        return Err("code contains an undeclared authority-neutral module route".to_owned());
    }
    Ok(rewritten)
}

fn source_digest(source: &SourcePackage) -> [u8; 32] {
    let mut hash = Sha256::new();
    for file in source.files() {
        hash.update((file.path().len() as u64).to_le_bytes());
        hash.update(file.path().as_bytes());
        hash.update((file.bytes().len() as u64).to_le_bytes());
        hash.update(file.bytes());
    }
    hash.finalize().into()
}

fn require_directory(path: &Path, label: &str) -> Result<(), String> {
    let metadata = std::fs::symlink_metadata(path)
        .map_err(|error| format!("inspect {label} {}: {error}", path.display()))?;
    if metadata.is_dir() && !metadata.file_type().is_symlink() {
        Ok(())
    } else {
        Err(format!("{label} is not an ordinary directory"))
    }
}

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

    #[test]
    fn route_rewrite_is_exact_and_fail_closed() {
        let authority = "010101010101010101010101";
        let source = "import '/module/base/v1/Code.js';\n";
        assert_eq!(
            rewrite_routes(source, authority, &["base"]).unwrap(),
            "import '/module/010101010101010101010101/base/v1/Code.js';\n"
        );
        assert!(rewrite_routes(source, authority, &[]).is_err());
        assert!(rewrite_routes("export {};", authority, &["base"]).is_err());
    }
}