Skip to main content

kcode_k1_web_bootstrap_import/
lib.rs

1#![doc = include_str!("../Documentation.md")]
2#![forbid(unsafe_code)]
3
4use kcode_k1_transaction_id::TxId;
5use kcode_k1_web_bootstrap_archive::{Archive, Dependency, Package};
6use kcode_k1_web_code_document::{CodeDocument, Language};
7use kcode_k1_web_coding::{CheckOutcome, K1WebCoding, WebCodingConfig, WebCodingRevisions};
8use kcode_k1_web_package::{AuthorityId, SourcePackage, WebFamily};
9use kcode_k1_web_podman::WebPodmanConfig;
10use kcode_k1_web_projection::{K1WebProjection, PublishStatus};
11use semver::Version;
12use serde::Serialize;
13use sha2::{Digest, Sha256};
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct ImporterRevisions {
19    pub boot: String,
20    pub schema: String,
21    pub route: String,
22    pub harness: String,
23    pub check_policy: String,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct ImportedPackage {
28    name: String,
29    version: Version,
30    source_sha256: [u8; 32],
31}
32
33impl ImportedPackage {
34    pub fn name(&self) -> &str {
35        &self.name
36    }
37
38    pub fn version(&self) -> &Version {
39        &self.version
40    }
41
42    pub const fn source_sha256(&self) -> [u8; 32] {
43        self.source_sha256
44    }
45}
46
47#[derive(Clone)]
48pub struct WebBootstrapImporter {
49    inner: Arc<Inner>,
50}
51
52struct Inner {
53    cache_root: PathBuf,
54    projection_root: PathBuf,
55    revisions: ImporterRevisions,
56    podman: WebPodmanConfig,
57    projection: Arc<K1WebProjection>,
58}
59
60impl WebBootstrapImporter {
61    pub fn new(
62        cache_root: impl Into<PathBuf>,
63        projection_root: impl Into<PathBuf>,
64        revisions: ImporterRevisions,
65        podman: WebPodmanConfig,
66        projection: Arc<K1WebProjection>,
67    ) -> Result<Self, String> {
68        let cache_root = cache_root.into();
69        let projection_root = projection_root.into();
70        require_directory(&cache_root, "Web bootstrap cache")?;
71        require_directory(&projection_root, "Web projection")?;
72        if [
73            &revisions.boot,
74            &revisions.schema,
75            &revisions.route,
76            &revisions.harness,
77            &revisions.check_policy,
78        ]
79        .iter()
80        .any(|value| value.is_empty())
81        {
82            return Err("Web bootstrap importer revisions must be nonempty".to_owned());
83        }
84        Ok(Self {
85            inner: Arc::new(Inner {
86                cache_root,
87                projection_root,
88                revisions,
89                podman,
90                projection,
91            }),
92        })
93    }
94
95    pub fn import_all(
96        &self,
97        archive: &Archive,
98        authority: [u8; 12],
99        user: [u8; 12],
100    ) -> Result<Vec<ImportedPackage>, String> {
101        let authority = AuthorityId::new(TxId::from_bytes(authority));
102        let prepared = prepare_all(archive, authority)?;
103        let revisions = &self.inner.revisions;
104        let config = WebCodingConfig::new(
105            WebCodingRevisions {
106                boot: revisions.boot.clone(),
107                schema: revisions.schema.clone(),
108                route: revisions.route.clone(),
109                harness: revisions.harness.clone(),
110                check_policy: revisions.check_policy.clone(),
111            },
112            self.inner.projection_root.clone(),
113            self.inner.podman.clone(),
114        )
115        .map_err(|error| format!("configure Web bootstrap coding: {error}"))?;
116        let mut coding = K1WebCoding::open(
117            &self.inner.cache_root,
118            TxId::from_bytes(user),
119            config,
120            Arc::clone(&self.inner.projection),
121        )
122        .map_err(|error| format!("open Web bootstrap coding: {error}"))?;
123        let authorize = |family: &WebFamily| Ok(family.authority() == authority);
124        let mut imported = Vec::with_capacity(prepared.len());
125        for item in prepared {
126            coding
127                .write(&item.source, &authorize)
128                .map_err(|error| format!("stage Web bootstrap package {}: {error}", item.name))?;
129            let check = coding
130                .check_fresh(item.source.id(), &authorize)
131                .map_err(|error| format!("check Web bootstrap package {}: {error}", item.name))?;
132            if matches!(check, CheckOutcome::Reused) {
133                return Err(format!("fresh Web check was reused for {}", item.name));
134            }
135            match self
136                .inner
137                .projection
138                .load(item.source.id())
139                .map_err(|error| format!("inspect published Web package {}: {error}", item.name))?
140            {
141                Some(existing) if existing.as_ref() == &item.source => {}
142                Some(_) => {
143                    return Err(format!(
144                        "published Web package {} {} differs from bootstrap source",
145                        item.name, item.version
146                    ));
147                }
148                None => {
149                    let completion = coding
150                        .publish(item.source.id(), &authorize, &|_, _| Ok(true))
151                        .map_err(|error| {
152                            format!("publish Web bootstrap package {}: {error}", item.name)
153                        })?;
154                    if completion.outcome.status == PublishStatus::Conflict {
155                        return Err(format!(
156                            "published Web package {} {} conflicts with bootstrap source",
157                            item.name, item.version
158                        ));
159                    }
160                }
161            }
162            imported.push(ImportedPackage {
163                name: item.name,
164                version: item.version,
165                source_sha256: item.source_sha256,
166            });
167        }
168        Ok(imported)
169    }
170}
171
172struct PreparedPackage {
173    name: String,
174    version: Version,
175    source_sha256: [u8; 32],
176    source: SourcePackage,
177}
178
179fn prepare_all(archive: &Archive, authority: AuthorityId) -> Result<Vec<PreparedPackage>, String> {
180    archive
181        .packages()
182        .iter()
183        .map(|package| prepare(package, authority))
184        .collect()
185}
186
187fn prepare(package: &Package, authority: AuthorityId) -> Result<PreparedPackage, String> {
188    let family = WebFamily::new(authority, package.name().to_owned())
189        .map_err(|error| format!("assign Web package {} authority: {error}", package.name()))?;
190    let authority_text = authority.to_string();
191    let documentation = documentation(package, &authority_text)?;
192    let code = rewrite_code(package, &authority_text)?;
193    let document = CodeDocument::new(family, documentation, Language::JavaScript, code)
194        .map_err(|error| format!("generate WebCode document for {}: {error}", package.name()))?;
195    let source = document
196        .to_source_package(package.version().clone())
197        .map_err(|error| format!("generate Web package {}: {error}", package.name()))?;
198    Ok(PreparedPackage {
199        name: package.name().to_owned(),
200        version: package.version().clone(),
201        source_sha256: source_digest(&source),
202        source,
203    })
204}
205
206#[derive(Serialize)]
207struct Header<'a> {
208    dependencies: Vec<HeaderDependency<'a>>,
209}
210
211#[derive(Serialize)]
212struct HeaderDependency<'a> {
213    authority: &'a str,
214    name: &'a str,
215    selector: &'a str,
216}
217
218fn documentation(package: &Package, authority: &str) -> Result<Vec<u8>, String> {
219    let dependencies = package
220        .dependencies()
221        .iter()
222        .map(|dependency| HeaderDependency {
223            authority,
224            name: dependency.name(),
225            selector: dependency.selector(),
226        })
227        .collect();
228    let header = serde_json::to_string(&Header { dependencies })
229        .map_err(|error| format!("encode Web dependencies for {}: {error}", package.name()))?;
230    let mut output = format!("<!-- k1-web/v1\n{header}\n-->\n").into_bytes();
231    output.extend_from_slice(package.documentation());
232    Ok(output)
233}
234
235fn rewrite_code(package: &Package, authority: &str) -> Result<Vec<u8>, String> {
236    let source = std::str::from_utf8(package.code())
237        .map_err(|_| format!("Code.js for {} is not UTF-8", package.name()))?;
238    let names = package
239        .dependencies()
240        .iter()
241        .map(Dependency::name)
242        .collect::<Vec<_>>();
243    rewrite_routes(source, authority, &names)
244        .map(String::into_bytes)
245        .map_err(|error| format!("rewrite Web routes for {}: {error}", package.name()))
246}
247
248fn rewrite_routes(source: &str, authority: &str, dependencies: &[&str]) -> Result<String, String> {
249    let mut rewritten = source.to_owned();
250    for name in dependencies {
251        let needle = format!("/module/{name}/");
252        if !rewritten.contains(&needle) {
253            return Err(format!("declared dependency {name} has no module route"));
254        }
255        rewritten = rewritten.replace(&needle, &format!("/module/{authority}/{name}/"));
256    }
257    let prefix = format!("/module/{authority}/");
258    if rewritten
259        .match_indices("/module/")
260        .any(|(index, _)| !rewritten[index..].starts_with(&prefix))
261    {
262        return Err("code contains an undeclared authority-neutral module route".to_owned());
263    }
264    Ok(rewritten)
265}
266
267fn source_digest(source: &SourcePackage) -> [u8; 32] {
268    let mut hash = Sha256::new();
269    for file in source.files() {
270        hash.update((file.path().len() as u64).to_le_bytes());
271        hash.update(file.path().as_bytes());
272        hash.update((file.bytes().len() as u64).to_le_bytes());
273        hash.update(file.bytes());
274    }
275    hash.finalize().into()
276}
277
278fn require_directory(path: &Path, label: &str) -> Result<(), String> {
279    let metadata = std::fs::symlink_metadata(path)
280        .map_err(|error| format!("inspect {label} {}: {error}", path.display()))?;
281    if metadata.is_dir() && !metadata.file_type().is_symlink() {
282        Ok(())
283    } else {
284        Err(format!("{label} is not an ordinary directory"))
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn route_rewrite_is_exact_and_fail_closed() {
294        let authority = "010101010101010101010101";
295        let source = "import '/module/base/v1/Code.js';\n";
296        assert_eq!(
297            rewrite_routes(source, authority, &["base"]).unwrap(),
298            "import '/module/010101010101010101010101/base/v1/Code.js';\n"
299        );
300        assert!(rewrite_routes(source, authority, &[]).is_err());
301        assert!(rewrite_routes("export {};", authority, &["base"]).is_err());
302    }
303}