pub mod crate_source;
pub mod declared;
pub mod dir;
pub mod embedded;
use serde::{Deserialize, Serialize};
use crate::diagnostic::{Diagnostic, Reason};
use crate::digest::Digest;
use crate::error::RkError;
pub use crate_source::CrateReleaseSource;
pub use dir::DirReleaseSource;
pub use embedded::EmbeddedReleaseSource;
pub const PAYLOAD_SCHEMA: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Artifact {
pub path: String,
pub sha256: Digest,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReleaseManifest {
pub release_kit_version: String,
pub payload_schema: u32,
pub payload_sha256: Digest,
pub artifacts: Vec<Artifact>,
}
impl ReleaseManifest {
#[must_use]
pub fn new(release_kit_version: String, payload_schema: u32, artifacts: Vec<Artifact>) -> Self {
Self {
release_kit_version,
payload_schema,
payload_sha256: aggregate(&artifacts),
artifacts,
}
}
#[must_use]
pub fn artifact(&self, path: &str) -> Option<&Artifact> {
self.artifacts.iter().find(|artifact| artifact.path == path)
}
pub fn under<'a>(&'a self, prefix: &'a str) -> impl Iterator<Item = (&'a str, &'a Artifact)> {
self.artifacts.iter().filter_map(move |artifact| {
artifact
.path
.strip_prefix(prefix)
.and_then(|rest| rest.strip_prefix('/'))
.map(|rest| (rest, artifact))
})
}
#[must_use]
pub fn dirs_under(&self, prefix: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for (rest, _) in self.under(prefix) {
if let Some((dir, _)) = rest.split_once('/') {
if !out.iter().any(|known| known == dir) {
out.push(dir.to_owned());
}
}
}
out
}
pub fn check_schema(&self) -> Result<(), RkError> {
check_schema(self.payload_schema, &self.release_kit_version)
}
}
pub fn check_schema(schema: u32, version: &str) -> Result<(), RkError> {
if schema <= PAYLOAD_SCHEMA {
return Ok(());
}
Err(RkError::refusal(
Diagnostic::new(
Reason::UnsupportedSchema,
format!(
"the bundle for release-kit {version} declares payload schema {schema}, and this engine reads schema {PAYLOAD_SCHEMA} at most"
),
)
.expected("a bundle whose payload schema is at or below the engine's")
.action(format!(
"install release-kit {version} or newer; an engine reads any bundle at or below its own schema, and no older engine can read this one"
)),
))
}
#[must_use]
pub fn aggregate(artifacts: &[Artifact]) -> Digest {
let mut lines = String::new();
for artifact in artifacts {
lines.push_str(&artifact.path);
lines.push('\n');
lines.push_str(&artifact.sha256.to_string());
lines.push('\n');
}
Digest::of(lines.as_bytes())
}
pub trait ReleaseSource {
fn manifest(&self) -> Result<ReleaseManifest, RkError>;
fn blob(&self, digest: &Digest) -> Result<Vec<u8>, RkError>;
}
impl ReleaseSource for &dyn ReleaseSource {
fn manifest(&self) -> Result<ReleaseManifest, RkError> {
(**self).manifest()
}
fn blob(&self, digest: &Digest) -> Result<Vec<u8>, RkError> {
(**self).blob(digest)
}
}
pub fn read(
source: &dyn ReleaseSource,
manifest: &ReleaseManifest,
path: &str,
) -> Result<Vec<u8>, RkError> {
let artifact = manifest.artifact(path).ok_or_else(|| RkError::NotFound {
kind: "artifact",
name: path.to_owned(),
})?;
source.blob(&artifact.sha256)
}
pub(crate) fn unknown_digest(digest: &Digest) -> RkError {
RkError::NotFound {
kind: "blob",
name: digest.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::{Artifact, PAYLOAD_SCHEMA, ReleaseManifest, check_schema};
use crate::digest::Digest;
fn manifest() -> ReleaseManifest {
ReleaseManifest::new(
"0.0.0".into(),
PAYLOAD_SCHEMA,
vec![
Artifact {
path: "snippets/_shared/github/SECURITY.md".into(),
sha256: Digest::of(b"a"),
},
Artifact {
path: "snippets/rust/github/release-plz.toml".into(),
sha256: Digest::of(b"b"),
},
Artifact {
path: "versions.toml".into(),
sha256: Digest::of(b"c"),
},
],
)
}
#[test]
fn an_engine_reads_a_bundle_at_or_below_its_schema() {
assert!(check_schema(PAYLOAD_SCHEMA, "9.9.9").is_ok());
assert!(check_schema(0, "0.0.1").is_ok());
assert!(manifest().check_schema().is_ok());
}
#[test]
fn an_engine_refuses_a_newer_schema_naming_the_engine_to_install() {
let err = check_schema(PAYLOAD_SCHEMA + 1, "9.9.9").expect_err("a newer schema refuses");
assert_eq!(err.exit_code(), 73);
assert_eq!(err.reason(), crate::diagnostic::Reason::UnsupportedSchema);
let text = err.to_string();
assert!(text.contains("release-kit 9.9.9"), "{text}");
assert!(
text.contains(&format!("schema {}", PAYLOAD_SCHEMA + 1)),
"{text}"
);
let action = err
.diagnostic()
.action
.expect("the refusal names the engine to install");
assert!(action.contains("install release-kit 9.9.9"), "{action}");
}
#[test]
fn the_manifest_lists_under_a_prefix() {
let manifest = manifest();
assert_eq!(manifest.dirs_under("snippets"), ["_shared", "rust"]);
assert_eq!(manifest.dirs_under("snippets/rust"), ["github"]);
let under: Vec<&str> = manifest
.under("snippets/rust/github")
.map(|(p, _)| p)
.collect();
assert_eq!(under, ["release-plz.toml"]);
assert!(manifest.artifact("versions.toml").is_some());
assert!(manifest.artifact("versions").is_none());
}
}