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 resources: BTreeMap<String, PathBuf>,
174 pub bytecode: Option<PackageBytecode>,
175 pub schema_catalog: Option<PackageCatalogDescriptor>,
176 pub wasm_imports: BTreeMap<String, PackageVariant>,
177 pub flavors: BTreeMap<String, PackageVariant>,
178 canonical_edn: String,
179}
180
181impl PackageManifest {
182 pub fn read(path: &Path) -> Result<Self, PackageManifestError> {
183 let source = fs::read_to_string(path).map_err(|error| {
184 PackageManifestError::new(
185 "package/invalid-manifest",
186 format!("cannot read {}: {error}", path.display()),
187 )
188 })?;
189 Self::parse(&source)
190 }
191
192 pub fn read_archive(path: &Path) -> Result<Self, PackageManifestError> {
196 archive::read_archive(path)
197 }
198
199 pub fn select_archive(
203 path: &Path,
204 runtime: PackageRuntime,
205 requirements: &PackageRuntimeRequirements,
206 ) -> Result<VerifiedPackageSelection, PackageManifestError> {
207 let manifest = Self::read_archive(path)?;
208 let selection = manifest.select_variant(runtime, requirements)?;
209 Ok(VerifiedPackageSelection {
210 manifest,
211 selection,
212 })
213 }
214
215 pub fn select_wasm_import_archive(
216 path: &Path,
217 module: &str,
218 requirements: &PackageRuntimeRequirements,
219 ) -> Result<VerifiedPackageSelection, PackageManifestError> {
220 let manifest = Self::read_archive(path)?;
221 let selection = manifest.select_wasm_import(module, requirements)?;
222 Ok(VerifiedPackageSelection {
223 manifest,
224 selection,
225 })
226 }
227
228 pub fn select_hta_require_archive(
229 path: &Path,
230 module: &str,
231 requirements: &PackageRuntimeRequirements,
232 ) -> Result<VerifiedPackageSelection, PackageManifestError> {
233 let manifest = Self::read_archive(path)?;
234 let selection = manifest.select_hta_require(module, requirements)?;
235 Ok(VerifiedPackageSelection {
236 manifest,
237 selection,
238 })
239 }
240
241 pub fn parse(source: &str) -> Result<Self, PackageManifestError> {
242 parse::parse_manifest(source)
243 }
244
245 pub fn canonical_edn(&self) -> &str {
246 &self.canonical_edn
247 }
248
249 pub fn unsupported_host_flavors_warning(&self) -> Option<String> {
253 if self.flavors.is_empty() {
254 return None;
255 }
256 let flavors = self
257 .flavors
258 .keys()
259 .map(|flavor| format!(":{flavor}"))
260 .collect::<Vec<_>>()
261 .join(", ");
262 Some(format!(
263 "package/host-flavors-ignored: {} {} ({flavors}) are unavailable on the Rust/Wasm runtime",
264 self.identity, self.version
265 ))
266 }
267
268 pub fn admit_catalog_bytes(
269 &self,
270 bytes: &[u8],
271 ) -> Result<PackageCatalogAdmission, PackageManifestError> {
272 let descriptor = self.schema_catalog.as_ref().ok_or_else(|| {
273 PackageManifestError::new(
274 "package/catalog-missing",
275 "package does not declare :schema/catalog",
276 )
277 })?;
278 self.verify_file_bytes(&descriptor.path, bytes)?;
279 let source = std::str::from_utf8(bytes).map_err(|error| {
280 PackageManifestError::new(
281 "package/catalog-invalid",
282 format!(
283 "catalog {} is not UTF-8 JSON: {error}",
284 descriptor.path.display()
285 ),
286 )
287 })?;
288 catalog::admit(&descriptor.format, source)
289 }
290
291 pub fn admit_catalog_at(
292 &self,
293 root: &Path,
294 ) -> Result<Option<PackageCatalogAdmission>, PackageManifestError> {
295 let Some(descriptor) = &self.schema_catalog else {
296 return Ok(None);
297 };
298 let bytes = fs::read(root.join(&descriptor.path)).map_err(|error| {
299 PackageManifestError::new(
300 "package/catalog-missing",
301 format!("cannot read catalog {}: {error}", descriptor.path.display()),
302 )
303 })?;
304 self.admit_catalog_bytes(&bytes).map(Some)
305 }
306
307 pub fn select_variant(
308 &self,
309 runtime: PackageRuntime,
310 requirements: &PackageRuntimeRequirements,
311 ) -> Result<PackageSelection, PackageManifestError> {
312 let flavor = match runtime {
313 PackageRuntime::Jvm => "jvm",
314 };
315 self.select_flavor(flavor, requirements)
316 }
317
318 pub fn select_flavor(
319 &self,
320 flavor: &str,
321 requirements: &PackageRuntimeRequirements,
322 ) -> Result<PackageSelection, PackageManifestError> {
323 if self.flavors.is_empty() {
324 if self.wasm_imports.is_empty() {
325 return Ok(PackageSelection::Portable);
326 }
327 return Err(PackageManifestError::new(
328 "package/missing-flavor",
329 format!(
330 "{} {} has no :{} host flavor",
331 self.identity, self.version, flavor
332 ),
333 ));
334 }
335 let variant = self.flavors.get(flavor).ok_or_else(|| {
336 PackageManifestError::new(
337 "package/missing-flavor",
338 format!(
339 "{} {} has no :{} host flavor",
340 self.identity, self.version, flavor
341 ),
342 )
343 })?;
344 if variant.artifact.artifact_type != PackageArtifactType::Jar {
345 return Err(PackageManifestError::new(
346 "package/artifact-type-mismatch",
347 format!(
348 ":{} flavor must select :jar, got :{}",
349 flavor,
350 variant.artifact.artifact_type.keyword()
351 ),
352 ));
353 }
354 self.preflight_variant(&format!(":{} flavor", flavor), variant, requirements)?;
355 Ok(PackageSelection::Variant(variant.clone()))
356 }
357
358 pub fn select_hta_require(
359 &self,
360 module: &str,
361 requirements: &PackageRuntimeRequirements,
362 ) -> Result<PackageSelection, PackageManifestError> {
363 if self.wasm_imports.is_empty() && self.flavors.is_empty() {
364 return Ok(PackageSelection::Portable);
365 }
366 let variant = self.wasm_imports.get(module).ok_or_else(|| {
367 PackageManifestError::new(
368 "package/missing-require-artifact",
369 format!(
370 "{} {} has no HTA artifact for :require {module}",
371 self.identity, self.version
372 ),
373 )
374 })?;
375 if variant.artifact.artifact_type != PackageArtifactType::Hta {
376 return Err(PackageManifestError::new(
377 "package/artifact-type-mismatch",
378 format!(
379 ":require {module} must select :hta, got :{}",
380 variant.artifact.artifact_type.keyword()
381 ),
382 ));
383 }
384 self.preflight_variant(&format!(":require {module}"), variant, requirements)?;
385 Ok(PackageSelection::Variant(variant.clone()))
386 }
387
388 fn preflight_variant(
389 &self,
390 route: &str,
391 variant: &PackageVariant,
392 requirements: &PackageRuntimeRequirements,
393 ) -> Result<(), PackageManifestError> {
394 if !requirements
395 .supported_targets
396 .contains(&variant.artifact.target)
397 {
398 return Err(PackageManifestError::new(
399 "package/target-mismatch",
400 format!(
401 "{route} artifact target {} is not supported",
402 variant.artifact.target
403 ),
404 ));
405 }
406 if !requirements.supported_abis.contains(&variant.artifact.abi) {
407 return Err(PackageManifestError::new(
408 "package/abi-mismatch",
409 format!(
410 "{route} artifact ABI {} is not supported",
411 variant.artifact.abi
412 ),
413 ));
414 }
415
416 let missing_capabilities = difference(
417 &variant.required_capabilities,
418 &requirements.available_capabilities,
419 );
420 if !missing_capabilities.is_empty() {
421 return Err(PackageManifestError::new(
422 "package/capability-denied",
423 format!("missing capabilities: {}", missing_capabilities.join(", ")),
424 ));
425 }
426 let denied_host_calls = difference(&variant.host_calls, &requirements.allowed_host_calls);
427 if !denied_host_calls.is_empty() {
428 return Err(PackageManifestError::new(
429 "package/host-call-denied",
430 format!("denied host calls: {}", denied_host_calls.join(", ")),
431 ));
432 }
433 Ok(())
434 }
435
436 pub fn select_wasm_import(
437 &self,
438 module: &str,
439 requirements: &PackageRuntimeRequirements,
440 ) -> Result<PackageSelection, PackageManifestError> {
441 let variant = self.wasm_imports.get(module).ok_or_else(|| {
442 PackageManifestError::new(
443 "package/missing-wasm-import",
444 format!(
445 "{} {} has no Wasm import {module}",
446 self.identity, self.version
447 ),
448 )
449 })?;
450 if variant.artifact.artifact_type != PackageArtifactType::Wasm {
451 return Err(PackageManifestError::new(
452 "package/artifact-type-mismatch",
453 format!(
454 ":import {module} must select :wasm, got :{}",
455 variant.artifact.artifact_type.keyword()
456 ),
457 ));
458 }
459 self.preflight_variant(&format!(":import {module}"), variant, requirements)?;
460 Ok(PackageSelection::Variant(variant.clone()))
461 }
462
463 pub fn verify_artifact_bytes(
464 &self,
465 selection: &PackageSelection,
466 bytes: &[u8],
467 ) -> Result<(), PackageManifestError> {
468 let PackageSelection::Variant(variant) = selection else {
469 return Err(PackageManifestError::new(
470 "package/missing-artifact",
471 "portable package selection has no runtime artifact",
472 ));
473 };
474 self.verify_file_bytes(&variant.artifact.path, bytes)
475 }
476
477 pub fn verify_file_reader<R: Read>(
480 &self,
481 relative: &Path,
482 reader: &mut R,
483 ) -> Result<(), PackageManifestError> {
484 let expected = self.files.get(relative).ok_or_else(|| {
485 PackageManifestError::new(
486 "package/missing-artifact",
487 format!("file is not declared in :files: {}", relative.display()),
488 )
489 })?;
490 verify_reader(relative, expected, reader)
491 }
492
493 pub fn verify_file_bytes(
494 &self,
495 relative: &Path,
496 bytes: &[u8],
497 ) -> Result<(), PackageManifestError> {
498 self.verify_file_reader(relative, &mut std::io::Cursor::new(bytes))
499 }
500
501 pub fn verify_files_at(&self, root: &Path) -> Result<(), PackageManifestError> {
502 for (relative, expected) in &self.files {
503 let path = root.join(relative);
504 let metadata = fs::symlink_metadata(&path).map_err(|error| {
505 PackageManifestError::new(
506 "package/missing-artifact",
507 format!("cannot inspect {}: {error}", relative.display()),
508 )
509 })?;
510 if metadata.file_type().is_symlink() || !metadata.is_file() {
511 return Err(PackageManifestError::new(
512 "package/missing-artifact",
513 format!(
514 "declared package file is not a regular file: {}",
515 relative.display()
516 ),
517 ));
518 }
519 if metadata.len() != expected.size {
520 return Err(PackageManifestError::new(
521 "package/size-mismatch",
522 format!(
523 "{} has {} bytes, expected {}",
524 relative.display(),
525 metadata.len(),
526 expected.size
527 ),
528 ));
529 }
530 let mut file = fs::File::open(&path).map_err(|error| {
531 PackageManifestError::new(
532 "package/missing-artifact",
533 format!("cannot read {}: {error}", relative.display()),
534 )
535 })?;
536 verify_reader(relative, expected, &mut file)?;
537 }
538 Ok(())
539 }
540
541 pub fn verify_selection_at(
544 &self,
545 root: &Path,
546 runtime: PackageRuntime,
547 requirements: &PackageRuntimeRequirements,
548 ) -> Result<PackageSelection, PackageManifestError> {
549 self.verify_files_at(root)?;
550 self.select_variant(runtime, requirements)
551 }
552}
553
554fn verify_reader<R: Read>(
555 relative: &Path,
556 expected: &PackageFile,
557 reader: &mut R,
558) -> Result<(), PackageManifestError> {
559 let mut hasher = Sha256::new();
560 let mut size = 0_u64;
561 let mut buffer = [0_u8; 64 * 1024];
562 loop {
563 let read = reader.read(&mut buffer).map_err(|error| {
564 PackageManifestError::new(
565 "package/missing-artifact",
566 format!("cannot read {}: {error}", relative.display()),
567 )
568 })?;
569 if read == 0 {
570 break;
571 }
572 size = size.checked_add(read as u64).ok_or_else(|| {
573 PackageManifestError::new(
574 "package/size-mismatch",
575 format!("{} is too large to verify", relative.display()),
576 )
577 })?;
578 hasher.update(&buffer[..read]);
579 }
580 if size != expected.size {
581 return Err(PackageManifestError::new(
582 "package/size-mismatch",
583 format!(
584 "{} has {} bytes, expected {}",
585 relative.display(),
586 size,
587 expected.size
588 ),
589 ));
590 }
591 let actual = digest_string(&hasher.finalize());
592 if actual != expected.sha256 {
593 return Err(PackageManifestError::new(
594 "package/digest-mismatch",
595 format!(
596 "{} has digest {}, expected {}",
597 relative.display(),
598 actual,
599 expected.sha256
600 ),
601 ));
602 }
603 Ok(())
604}
605
606fn digest_string(bytes: &[u8]) -> String {
607 let hexadecimal = bytes
608 .iter()
609 .map(|byte| format!("{byte:02x}"))
610 .collect::<String>();
611 format!("sha256:{hexadecimal}")
612}
613
614fn difference(required: &BTreeSet<String>, available: &BTreeSet<String>) -> Vec<String> {
615 required.difference(available).cloned().collect()
616}