pub mod crates_io;
pub mod embedded;
pub mod fixture;
pub mod legacy;
use std::collections::BTreeMap;
use serde::Serialize;
use crate::domain::ownership::Sha256;
use crate::domain::projection::{DECLARATION_PATH, Declaration};
use crate::domain::version::CanonVersion;
use crate::error::AppError;
pub use semver::Version;
pub const MANIFEST_SCHEMA: &str = "sdd.payload/1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Provenance {
Native,
LegacyAdapted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Role {
Payload,
Metadata,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Artifact {
pub path: String,
pub role: Role,
pub bytes: u64,
pub sha256: Sha256,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ReleaseManifest {
pub schema: &'static str,
pub version: Version,
pub payload_schema: u32,
pub provenance: Provenance,
pub descriptor_sha256: Option<Sha256>,
pub payload_sha256: Sha256,
pub artifacts: Vec<Artifact>,
}
impl ReleaseManifest {
#[must_use]
pub fn digest_of(&self, path: &str) -> Option<&Sha256> {
self.artifacts
.iter()
.find(|artifact| artifact.path == path)
.map(|artifact| &artifact.sha256)
}
#[must_use]
pub fn payload_digest(artifacts: &[Artifact]) -> Sha256 {
let mut joined = String::new();
for artifact in artifacts {
joined.push_str(&artifact.path);
joined.push('\0');
joined.push_str(artifact.sha256.as_str());
joined.push('\n');
}
Sha256::of(joined.as_bytes())
}
}
pub trait ReleaseBundle: Send + Sync {
fn manifest(&self) -> Result<ReleaseManifest, AppError>;
fn blob(&self, digest: &Sha256) -> Result<Vec<u8>, AppError>;
fn artifact(&self, path: &str) -> Result<Vec<u8>, AppError> {
let manifest = self.manifest()?;
let digest = manifest.digest_of(path).ok_or_else(|| {
AppError::Refused(format!("release {} carries no {path}", manifest.version))
})?;
self.blob(digest)
}
fn declaration(&self) -> Result<Declaration, AppError> {
let bytes = self.artifact(DECLARATION_PATH)?;
Declaration::parse(&bytes).map_err(|source| AppError::Refused(source.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Selector {
Embedded,
Latest,
Exact(Version),
}
impl std::fmt::Display for Selector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Embedded => f.write_str("embedded"),
Self::Latest => f.write_str("latest"),
Self::Exact(version) => write!(f, "{version}"),
}
}
}
pub struct ResolvedRelease {
pub selector: Selector,
pub version: Version,
pub registry_checksum: Option<Sha256>,
pub payload_sha256: Sha256,
pub yanked: bool,
pub bundle: Box<dyn ReleaseBundle>,
}
impl std::fmt::Debug for ResolvedRelease {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResolvedRelease")
.field("selector", &self.selector)
.field("version", &self.version)
.field("registry_checksum", &self.registry_checksum)
.field("payload_sha256", &self.payload_sha256)
.field("yanked", &self.yanked)
.finish_non_exhaustive()
}
}
impl ResolvedRelease {
pub fn canon_version(&self) -> Result<CanonVersion, AppError> {
self.version.to_string().parse().map_err(|_| {
AppError::Refused(format!(
"{} is not a released triple, so no instance record can name it",
self.version
))
})
}
}
pub trait ReleaseResolver {
fn resolve(&self, selector: &Selector) -> Result<ResolvedRelease, AppError>;
}
#[must_use]
pub fn manifest_from(
version: Version,
payload_schema: u32,
provenance: Provenance,
descriptor_sha256: Option<Sha256>,
files: &BTreeMap<String, Vec<u8>>,
metadata: &BTreeMap<String, Vec<u8>>,
) -> ReleaseManifest {
let mut artifacts: Vec<Artifact> = files
.iter()
.map(|(path, bytes)| artifact_of(path, bytes, Role::Payload))
.chain(
metadata
.iter()
.map(|(path, bytes)| artifact_of(path, bytes, Role::Metadata)),
)
.collect();
artifacts.sort_by(|left, right| left.path.cmp(&right.path));
ReleaseManifest {
schema: MANIFEST_SCHEMA,
version,
payload_schema,
provenance,
descriptor_sha256,
payload_sha256: ReleaseManifest::payload_digest(&artifacts),
artifacts,
}
}
fn artifact_of(path: &str, bytes: &[u8], role: Role) -> Artifact {
Artifact {
path: path.to_string(),
role,
bytes: bytes.len() as u64,
sha256: Sha256::of(bytes),
}
}
pub fn blob_from(
files: &BTreeMap<String, Vec<u8>>,
metadata: &BTreeMap<String, Vec<u8>>,
digest: &Sha256,
) -> Result<Vec<u8>, AppError> {
files
.values()
.chain(metadata.values())
.find(|bytes| &Sha256::of(bytes) == digest)
.cloned()
.ok_or_else(|| AppError::Refused(format!("the bundle carries no blob {digest}")))
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
reason = "a test panics as its failure signal, not as control flow"
)]
use super::*;
fn current() -> Version {
CanonVersion::current().to_string().parse().unwrap()
}
fn files() -> BTreeMap<String, Vec<u8>> {
BTreeMap::from([
("b.md".to_string(), b"two".to_vec()),
("a.md".to_string(), b"one".to_vec()),
])
}
#[test]
fn a_manifest_lists_every_artifact_in_path_order() {
let manifest = manifest_from(
current(),
1,
Provenance::Native,
None,
&files(),
&BTreeMap::new(),
);
let paths: Vec<&str> = manifest
.artifacts
.iter()
.map(|artifact| artifact.path.as_str())
.collect();
assert_eq!(paths, ["a.md", "b.md"]);
assert_eq!(manifest.schema, MANIFEST_SCHEMA);
assert_eq!(manifest.digest_of("a.md"), Some(&Sha256::of(b"one")));
assert_eq!(manifest.digest_of("absent.md"), None);
}
#[test]
fn the_payload_digest_reads_the_content_and_not_the_order_it_was_given() {
let one = manifest_from(
current(),
1,
Provenance::Native,
None,
&files(),
&BTreeMap::new(),
);
let mut reversed = BTreeMap::new();
reversed.insert("a.md".to_string(), b"one".to_vec());
reversed.insert("b.md".to_string(), b"two".to_vec());
let two = manifest_from(
current(),
1,
Provenance::Native,
None,
&reversed,
&BTreeMap::new(),
);
assert_eq!(one.payload_sha256, two.payload_sha256);
}
#[test]
fn metadata_joins_the_manifest_as_a_second_role() {
let metadata = BTreeMap::from([("m.toml".to_string(), b"x".to_vec())]);
let manifest = manifest_from(
current(),
1,
Provenance::LegacyAdapted,
Some(Sha256::of(b"descriptor")),
&files(),
&metadata,
);
let roles: Vec<Role> = manifest
.artifacts
.iter()
.map(|artifact| artifact.role)
.collect();
assert_eq!(roles, [Role::Payload, Role::Payload, Role::Metadata]);
assert_eq!(manifest.provenance, Provenance::LegacyAdapted);
}
#[test]
fn a_blob_is_served_by_digest_and_an_unknown_digest_refuses() {
let held = files();
assert_eq!(
blob_from(&held, &BTreeMap::new(), &Sha256::of(b"one")).unwrap(),
b"one"
);
assert!(blob_from(&held, &BTreeMap::new(), &Sha256::of(b"nope")).is_err());
}
}