1use crate::kernel::Form;
10use semver::Version;
11use sha2::{Digest, Sha256};
12use std::collections::{BTreeMap, BTreeSet};
13use std::fmt;
14use std::fs;
15use std::io::Read;
16use std::path::{Path, PathBuf};
17
18mod archive;
19mod catalog;
20mod parse;
21#[cfg(test)]
22mod tests;
23
24const PACKAGE_FORMAT: &str = "0.0.0-alpha";
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub enum PackageRuntime {
28 Jvm,
29}
30
31impl PackageRuntime {
32 pub const fn keyword(self) -> &'static str {
33 match self {
34 Self::Jvm => "jvm",
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum PackageArtifactType {
41 Jar,
42 Wasm,
43 Hta,
44}
45
46impl PackageArtifactType {
47 pub const fn keyword(self) -> &'static str {
48 match self {
49 Self::Jar => "jar",
50 Self::Wasm => "wasm",
51 Self::Hta => "hta",
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct PackageManifestError {
58 pub code: &'static str,
59 pub detail: String,
60}
61
62impl PackageManifestError {
63 fn new(code: &'static str, detail: impl Into<String>) -> Self {
64 Self {
65 code,
66 detail: detail.into(),
67 }
68 }
69}
70
71impl fmt::Display for PackageManifestError {
72 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(formatter, "{}: {}", self.code, self.detail)
74 }
75}
76
77impl std::error::Error for PackageManifestError {}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct PackageProvenance {
81 pub repository: String,
82 pub commit: String,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct PackageFile {
87 pub sha256: String,
88 pub size: u64,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct PackageCatalogDescriptor {
93 pub format: String,
94 pub path: PathBuf,
95 pub sha256: String,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct PackageBytecode {
100 pub format: String,
101 pub path: PathBuf,
102 pub sha256: String,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct PackageCatalogAdmission {
107 pub format: String,
108 pub report: String,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct PackageArtifact {
113 pub artifact_type: PackageArtifactType,
114 pub path: PathBuf,
115 pub sha256: String,
116 pub target: String,
117 pub abi: String,
118 pub entry_point: String,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct PackageLifecycle {
123 pub load_idempotent: bool,
124 pub close_idempotent: bool,
125 pub session_isolation: bool,
126 pub asynchronous: bool,
127 pub cancellation: bool,
128}
129
130#[derive(Debug, Clone, PartialEq)]
131pub struct PackageVariant {
132 pub artifact: PackageArtifact,
133 pub required_capabilities: BTreeSet<String>,
134 pub host_calls: BTreeSet<String>,
135 pub exports: BTreeSet<String>,
136 pub dependencies: Option<Form>,
137 pub lifecycle: Option<PackageLifecycle>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Default)]
141pub struct PackageRuntimeRequirements {
142 pub supported_targets: BTreeSet<String>,
143 pub supported_abis: BTreeSet<String>,
144 pub available_capabilities: BTreeSet<String>,
145 pub allowed_host_calls: BTreeSet<String>,
146}
147
148#[derive(Debug, Clone, PartialEq)]
149pub enum PackageSelection {
150 Portable,
151 Variant(PackageVariant),
152}
153
154#[derive(Debug, Clone, PartialEq)]
157pub struct VerifiedPackageSelection {
158 pub manifest: PackageManifest,
159 pub selection: PackageSelection,
160}
161
162#[derive(Debug, Clone, PartialEq)]
163pub struct PackageManifest {
164 pub format: String,
165 pub identity: String,
166 pub name: Option<String>,
167 pub version: Version,
168 pub provenance: Option<PackageProvenance>,
169 pub files: BTreeMap<PathBuf, PackageFile>,
170 pub bytecode: Option<PackageBytecode>,
171 pub schema_catalog: Option<PackageCatalogDescriptor>,
172 pub wasm_imports: BTreeMap<String, PackageVariant>,
173 pub flavors: BTreeMap<String, PackageVariant>,
174 canonical_edn: String,
175}
176
177impl PackageManifest {
178 pub fn read(path: &Path) -> Result<Self, PackageManifestError> {
179 let source = fs::read_to_string(path).map_err(|error| {
180 PackageManifestError::new(
181 "package/invalid-manifest",
182 format!("cannot read {}: {error}", path.display()),
183 )
184 })?;
185 Self::parse(&source)
186 }
187
188 pub fn read_archive(path: &Path) -> Result<Self, PackageManifestError> {
192 archive::read_archive(path)
193 }
194
195 pub fn select_archive(
199 path: &Path,
200 runtime: PackageRuntime,
201 requirements: &PackageRuntimeRequirements,
202 ) -> Result<VerifiedPackageSelection, PackageManifestError> {
203 let manifest = Self::read_archive(path)?;
204 let selection = manifest.select_variant(runtime, requirements)?;
205 Ok(VerifiedPackageSelection {
206 manifest,
207 selection,
208 })
209 }
210
211 pub fn select_wasm_import_archive(
212 path: &Path,
213 module: &str,
214 requirements: &PackageRuntimeRequirements,
215 ) -> Result<VerifiedPackageSelection, PackageManifestError> {
216 let manifest = Self::read_archive(path)?;
217 let selection = manifest.select_wasm_import(module, requirements)?;
218 Ok(VerifiedPackageSelection {
219 manifest,
220 selection,
221 })
222 }
223
224 pub fn select_hta_require_archive(
225 path: &Path,
226 module: &str,
227 requirements: &PackageRuntimeRequirements,
228 ) -> Result<VerifiedPackageSelection, PackageManifestError> {
229 let manifest = Self::read_archive(path)?;
230 let selection = manifest.select_hta_require(module, requirements)?;
231 Ok(VerifiedPackageSelection {
232 manifest,
233 selection,
234 })
235 }
236
237 pub fn parse(source: &str) -> Result<Self, PackageManifestError> {
238 parse::parse_manifest(source)
239 }
240
241 pub fn canonical_edn(&self) -> &str {
242 &self.canonical_edn
243 }
244
245 pub fn unsupported_host_flavors_warning(&self) -> Option<String> {
249 if self.flavors.is_empty() {
250 return None;
251 }
252 let flavors = self
253 .flavors
254 .keys()
255 .map(|flavor| format!(":{flavor}"))
256 .collect::<Vec<_>>()
257 .join(", ");
258 Some(format!(
259 "package/host-flavors-ignored: {} {} ({flavors}) are unavailable on the Rust/Wasm runtime",
260 self.identity, self.version
261 ))
262 }
263
264 pub fn admit_catalog_bytes(
265 &self,
266 bytes: &[u8],
267 ) -> Result<PackageCatalogAdmission, PackageManifestError> {
268 let descriptor = self.schema_catalog.as_ref().ok_or_else(|| {
269 PackageManifestError::new(
270 "package/catalog-missing",
271 "package does not declare :schema/catalog",
272 )
273 })?;
274 self.verify_file_bytes(&descriptor.path, bytes)?;
275 let source = std::str::from_utf8(bytes).map_err(|error| {
276 PackageManifestError::new(
277 "package/catalog-invalid",
278 format!(
279 "catalog {} is not UTF-8 JSON: {error}",
280 descriptor.path.display()
281 ),
282 )
283 })?;
284 catalog::admit(&descriptor.format, source)
285 }
286
287 pub fn admit_catalog_at(
288 &self,
289 root: &Path,
290 ) -> Result<Option<PackageCatalogAdmission>, PackageManifestError> {
291 let Some(descriptor) = &self.schema_catalog else {
292 return Ok(None);
293 };
294 let bytes = fs::read(root.join(&descriptor.path)).map_err(|error| {
295 PackageManifestError::new(
296 "package/catalog-missing",
297 format!("cannot read catalog {}: {error}", descriptor.path.display()),
298 )
299 })?;
300 self.admit_catalog_bytes(&bytes).map(Some)
301 }
302
303 pub fn select_variant(
304 &self,
305 runtime: PackageRuntime,
306 requirements: &PackageRuntimeRequirements,
307 ) -> Result<PackageSelection, PackageManifestError> {
308 let flavor = match runtime {
309 PackageRuntime::Jvm => "jvm",
310 };
311 self.select_flavor(flavor, requirements)
312 }
313
314 pub fn select_flavor(
315 &self,
316 flavor: &str,
317 requirements: &PackageRuntimeRequirements,
318 ) -> Result<PackageSelection, PackageManifestError> {
319 if self.flavors.is_empty() {
320 if self.wasm_imports.is_empty() {
321 return Ok(PackageSelection::Portable);
322 }
323 return Err(PackageManifestError::new(
324 "package/missing-flavor",
325 format!(
326 "{} {} has no :{} host flavor",
327 self.identity, self.version, flavor
328 ),
329 ));
330 }
331 let variant = self.flavors.get(flavor).ok_or_else(|| {
332 PackageManifestError::new(
333 "package/missing-flavor",
334 format!(
335 "{} {} has no :{} host flavor",
336 self.identity, self.version, flavor
337 ),
338 )
339 })?;
340 if variant.artifact.artifact_type != PackageArtifactType::Jar {
341 return Err(PackageManifestError::new(
342 "package/artifact-type-mismatch",
343 format!(
344 ":{} flavor must select :jar, got :{}",
345 flavor,
346 variant.artifact.artifact_type.keyword()
347 ),
348 ));
349 }
350 self.preflight_variant(&format!(":{} flavor", flavor), variant, requirements)?;
351 Ok(PackageSelection::Variant(variant.clone()))
352 }
353
354 pub fn select_hta_require(
355 &self,
356 module: &str,
357 requirements: &PackageRuntimeRequirements,
358 ) -> Result<PackageSelection, PackageManifestError> {
359 if self.wasm_imports.is_empty() && self.flavors.is_empty() {
360 return Ok(PackageSelection::Portable);
361 }
362 let variant = self.wasm_imports.get(module).ok_or_else(|| {
363 PackageManifestError::new(
364 "package/missing-require-artifact",
365 format!(
366 "{} {} has no HTA artifact for :require {module}",
367 self.identity, self.version
368 ),
369 )
370 })?;
371 if variant.artifact.artifact_type != PackageArtifactType::Hta {
372 return Err(PackageManifestError::new(
373 "package/artifact-type-mismatch",
374 format!(
375 ":require {module} must select :hta, got :{}",
376 variant.artifact.artifact_type.keyword()
377 ),
378 ));
379 }
380 self.preflight_variant(&format!(":require {module}"), variant, requirements)?;
381 Ok(PackageSelection::Variant(variant.clone()))
382 }
383
384 fn preflight_variant(
385 &self,
386 route: &str,
387 variant: &PackageVariant,
388 requirements: &PackageRuntimeRequirements,
389 ) -> Result<(), PackageManifestError> {
390 if !requirements
391 .supported_targets
392 .contains(&variant.artifact.target)
393 {
394 return Err(PackageManifestError::new(
395 "package/target-mismatch",
396 format!(
397 "{route} artifact target {} is not supported",
398 variant.artifact.target
399 ),
400 ));
401 }
402 if !requirements.supported_abis.contains(&variant.artifact.abi) {
403 return Err(PackageManifestError::new(
404 "package/abi-mismatch",
405 format!(
406 "{route} artifact ABI {} is not supported",
407 variant.artifact.abi
408 ),
409 ));
410 }
411
412 let missing_capabilities = difference(
413 &variant.required_capabilities,
414 &requirements.available_capabilities,
415 );
416 if !missing_capabilities.is_empty() {
417 return Err(PackageManifestError::new(
418 "package/capability-denied",
419 format!("missing capabilities: {}", missing_capabilities.join(", ")),
420 ));
421 }
422 let denied_host_calls = difference(&variant.host_calls, &requirements.allowed_host_calls);
423 if !denied_host_calls.is_empty() {
424 return Err(PackageManifestError::new(
425 "package/host-call-denied",
426 format!("denied host calls: {}", denied_host_calls.join(", ")),
427 ));
428 }
429 Ok(())
430 }
431
432 pub fn select_wasm_import(
433 &self,
434 module: &str,
435 requirements: &PackageRuntimeRequirements,
436 ) -> Result<PackageSelection, PackageManifestError> {
437 let variant = self.wasm_imports.get(module).ok_or_else(|| {
438 PackageManifestError::new(
439 "package/missing-wasm-import",
440 format!(
441 "{} {} has no Wasm import {module}",
442 self.identity, self.version
443 ),
444 )
445 })?;
446 if variant.artifact.artifact_type != PackageArtifactType::Wasm {
447 return Err(PackageManifestError::new(
448 "package/artifact-type-mismatch",
449 format!(
450 ":import {module} must select :wasm, got :{}",
451 variant.artifact.artifact_type.keyword()
452 ),
453 ));
454 }
455 self.preflight_variant(&format!(":import {module}"), variant, requirements)?;
456 Ok(PackageSelection::Variant(variant.clone()))
457 }
458
459 pub fn verify_artifact_bytes(
460 &self,
461 selection: &PackageSelection,
462 bytes: &[u8],
463 ) -> Result<(), PackageManifestError> {
464 let PackageSelection::Variant(variant) = selection else {
465 return Err(PackageManifestError::new(
466 "package/missing-artifact",
467 "portable package selection has no runtime artifact",
468 ));
469 };
470 self.verify_file_bytes(&variant.artifact.path, bytes)
471 }
472
473 pub fn verify_file_reader<R: Read>(
476 &self,
477 relative: &Path,
478 reader: &mut R,
479 ) -> Result<(), PackageManifestError> {
480 let expected = self.files.get(relative).ok_or_else(|| {
481 PackageManifestError::new(
482 "package/missing-artifact",
483 format!("file is not declared in :files: {}", relative.display()),
484 )
485 })?;
486 verify_reader(relative, expected, reader)
487 }
488
489 pub fn verify_file_bytes(
490 &self,
491 relative: &Path,
492 bytes: &[u8],
493 ) -> Result<(), PackageManifestError> {
494 self.verify_file_reader(relative, &mut std::io::Cursor::new(bytes))
495 }
496
497 pub fn verify_files_at(&self, root: &Path) -> Result<(), PackageManifestError> {
498 for (relative, expected) in &self.files {
499 let path = root.join(relative);
500 let metadata = fs::symlink_metadata(&path).map_err(|error| {
501 PackageManifestError::new(
502 "package/missing-artifact",
503 format!("cannot inspect {}: {error}", relative.display()),
504 )
505 })?;
506 if metadata.file_type().is_symlink() || !metadata.is_file() {
507 return Err(PackageManifestError::new(
508 "package/missing-artifact",
509 format!(
510 "declared package file is not a regular file: {}",
511 relative.display()
512 ),
513 ));
514 }
515 if metadata.len() != expected.size {
516 return Err(PackageManifestError::new(
517 "package/size-mismatch",
518 format!(
519 "{} has {} bytes, expected {}",
520 relative.display(),
521 metadata.len(),
522 expected.size
523 ),
524 ));
525 }
526 let mut file = fs::File::open(&path).map_err(|error| {
527 PackageManifestError::new(
528 "package/missing-artifact",
529 format!("cannot read {}: {error}", relative.display()),
530 )
531 })?;
532 verify_reader(relative, expected, &mut file)?;
533 }
534 Ok(())
535 }
536
537 pub fn verify_selection_at(
540 &self,
541 root: &Path,
542 runtime: PackageRuntime,
543 requirements: &PackageRuntimeRequirements,
544 ) -> Result<PackageSelection, PackageManifestError> {
545 self.verify_files_at(root)?;
546 self.select_variant(runtime, requirements)
547 }
548}
549
550fn verify_reader<R: Read>(
551 relative: &Path,
552 expected: &PackageFile,
553 reader: &mut R,
554) -> Result<(), PackageManifestError> {
555 let mut hasher = Sha256::new();
556 let mut size = 0_u64;
557 let mut buffer = [0_u8; 64 * 1024];
558 loop {
559 let read = reader.read(&mut buffer).map_err(|error| {
560 PackageManifestError::new(
561 "package/missing-artifact",
562 format!("cannot read {}: {error}", relative.display()),
563 )
564 })?;
565 if read == 0 {
566 break;
567 }
568 size = size.checked_add(read as u64).ok_or_else(|| {
569 PackageManifestError::new(
570 "package/size-mismatch",
571 format!("{} is too large to verify", relative.display()),
572 )
573 })?;
574 hasher.update(&buffer[..read]);
575 }
576 if size != expected.size {
577 return Err(PackageManifestError::new(
578 "package/size-mismatch",
579 format!(
580 "{} has {} bytes, expected {}",
581 relative.display(),
582 size,
583 expected.size
584 ),
585 ));
586 }
587 let actual = digest_string(&hasher.finalize());
588 if actual != expected.sha256 {
589 return Err(PackageManifestError::new(
590 "package/digest-mismatch",
591 format!(
592 "{} has digest {}, expected {}",
593 relative.display(),
594 actual,
595 expected.sha256
596 ),
597 ));
598 }
599 Ok(())
600}
601
602fn digest_string(bytes: &[u8]) -> String {
603 let hexadecimal = bytes
604 .iter()
605 .map(|byte| format!("{byte:02x}"))
606 .collect::<String>();
607 format!("sha256:{hexadecimal}")
608}
609
610fn difference(required: &BTreeSet<String>, available: &BTreeSet<String>) -> Vec<String> {
611 required.difference(available).cloned().collect()
612}