kcode_k1_web_bootstrap_import/
lib.rs1#![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}