#![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());
}
}