use serde::Deserialize;
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::io::Write;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Licence {
Mit,
Gpl,
}
impl Licence {
const MIT: &'static str = "MIT";
const GPL: &'static str = "GPL";
fn parse(s: &str) -> Option<Self> {
match s {
Self::MIT => Some(Self::Mit),
Self::GPL => Some(Self::Gpl),
_ => None,
}
}
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Mit => Self::MIT,
Self::Gpl => Self::GPL,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LicenceProvenance {
Declared,
RuleSuggestedConfirmed,
}
impl LicenceProvenance {
const DECLARED: &'static str = "declared";
const RULE_SUGGESTED_CONFIRMED: &'static str = "rule-suggested-confirmed";
fn parse(s: &str) -> Option<Self> {
match s {
Self::DECLARED => Some(Self::Declared),
Self::RULE_SUGGESTED_CONFIRMED => Some(Self::RuleSuggestedConfirmed),
_ => None,
}
}
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Declared => Self::DECLARED,
Self::RuleSuggestedConfirmed => Self::RULE_SUGGESTED_CONFIRMED,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CustomizationStatus {
Customizable,
Fixed,
}
impl CustomizationStatus {
const CUSTOMIZABLE: &'static str = "customizable";
const FIXED: &'static str = "fixed";
fn parse(s: &str) -> Option<Self> {
match s {
Self::CUSTOMIZABLE => Some(Self::Customizable),
Self::FIXED => Some(Self::Fixed),
_ => None,
}
}
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Customizable => Self::CUSTOMIZABLE,
Self::Fixed => Self::FIXED,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContentKind {
Template,
}
impl ContentKind {
const TEMPLATE: &'static str = "template";
fn parse(s: &str) -> Option<Self> {
match s {
Self::TEMPLATE => Some(Self::Template),
_ => None,
}
}
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Template => Self::TEMPLATE,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct LogicalAddress(String);
impl LogicalAddress {
pub(crate) fn parse(raw: &str) -> Result<Self, AdmissionError> {
let rejected = || AdmissionError::TraversalRejected(raw.to_string());
if raw.is_empty() || raw.starts_with('/') || raw.contains('\\') {
return Err(rejected());
}
for segment in raw.split('/') {
if segment.is_empty() || segment == "." || segment == ".." {
return Err(rejected());
}
}
Ok(Self(raw.to_string()))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PublicationEntry {
address: LogicalAddress,
backing: String,
kind: ContentKind,
title: String,
licence: Licence,
provenance: LicenceProvenance,
customization: CustomizationStatus,
}
impl PublicationEntry {
pub(crate) fn address(&self) -> &LogicalAddress {
&self.address
}
pub(crate) fn report_line(&self) -> String {
format!(
"{addr} -> {backing} [{kind}] \"{title}\" licence={licence} provenance={provenance} customization={customization}",
addr = self.address.as_str(),
backing = self.backing,
kind = self.kind.as_str(),
title = self.title,
licence = self.licence.as_str(),
provenance = self.provenance.as_str(),
customization = self.customization.as_str(),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PublicationManifest {
entries: Vec<PublicationEntry>,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub(crate) enum AdmissionError {
#[error("malformed publication manifest: {0}")]
MalformedManifest(String),
#[error("entry {index}: missing, empty, or unknown field '{field}'")]
MissingField { index: usize, field: &'static str },
#[error("entry {index}: licence '{value}' is missing or outside the allowed set {{MIT, GPL}}")]
LicenceMissingOrOutOfSet { index: usize, value: String },
#[error("duplicate logical address '{0}'")]
DuplicateAddress(String),
#[error("address '{0}' is not a safe relative logical path")]
TraversalRejected(String),
#[error("publication manifest asset is missing from the embed")]
ManifestAssetMissing,
}
#[derive(Deserialize)]
struct RawManifest {
#[serde(default)]
entry: Vec<RawEntry>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawEntry {
address: Option<String>,
backing: Option<String>,
kind: Option<String>,
title: Option<String>,
licence: Option<String>,
provenance: Option<String>,
customization: Option<String>,
}
fn require<'a>(
value: Option<&'a String>,
index: usize,
field: &'static str,
) -> Result<&'a str, AdmissionError> {
match value {
Some(s) if !s.trim().is_empty() => Ok(s.as_str()),
_ => Err(AdmissionError::MissingField { index, field }),
}
}
impl PublicationManifest {
pub(crate) fn admit(bytes: &[u8]) -> Result<Self, AdmissionError> {
let text = std::str::from_utf8(bytes)
.map_err(|e| AdmissionError::MalformedManifest(e.to_string()))?;
let raw: RawManifest =
toml::from_str(text).map_err(|e| AdmissionError::MalformedManifest(e.to_string()))?;
let mut entries = Vec::with_capacity(raw.entry.len());
let mut seen: BTreeSet<String> = BTreeSet::new();
for (index, raw_entry) in raw.entry.iter().enumerate() {
let entry = admit_entry(raw_entry, index)?;
if !seen.insert(entry.address.as_str().to_string()) {
return Err(AdmissionError::DuplicateAddress(
entry.address.as_str().to_string(),
));
}
entries.push(entry);
}
Ok(Self { entries })
}
pub(crate) fn load() -> Result<Self, AdmissionError> {
let bytes = crate::asset_source::publication_manifest_bytes()
.ok_or(AdmissionError::ManifestAssetMissing)?;
Self::admit(&bytes)
}
pub(crate) fn entries(&self) -> &[PublicationEntry] {
&self.entries
}
}
fn admit_entry(raw: &RawEntry, index: usize) -> Result<PublicationEntry, AdmissionError> {
let address = LogicalAddress::parse(require(raw.address.as_ref(), index, "address")?)?;
let backing = require(raw.backing.as_ref(), index, "backing")?.to_string();
let kind = ContentKind::parse(require(raw.kind.as_ref(), index, "kind")?).ok_or(
AdmissionError::MissingField {
index,
field: "kind",
},
)?;
let title = require(raw.title.as_ref(), index, "title")?.to_string();
let licence_raw = require(raw.licence.as_ref(), index, "licence")?;
let licence =
Licence::parse(licence_raw).ok_or_else(|| AdmissionError::LicenceMissingOrOutOfSet {
index,
value: licence_raw.to_string(),
})?;
let provenance =
LicenceProvenance::parse(require(raw.provenance.as_ref(), index, "provenance")?).ok_or(
AdmissionError::MissingField {
index,
field: "provenance",
},
)?;
let customization =
CustomizationStatus::parse(require(raw.customization.as_ref(), index, "customization")?)
.ok_or(AdmissionError::MissingField {
index,
field: "customization",
})?;
Ok(PublicationEntry {
address,
backing,
kind,
title,
licence,
provenance,
customization,
})
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub(crate) enum ResolveError {
#[error("no published entry for logical address '{0}'")]
UnknownAddress(String),
#[error("backing source '{0}' is missing")]
BackingSourceMissing(String),
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum EmitError {
#[error(transparent)]
Resolve(#[from] ResolveError),
#[error("emit write failed: {0}")]
Io(#[from] std::io::Error),
}
pub(crate) trait SourceAdapter {
fn read(&self, backing: &str) -> Result<Cow<'static, [u8]>, ResolveError>;
}
pub(crate) struct EmbeddedAdapter;
impl SourceAdapter for EmbeddedAdapter {
fn read(&self, backing: &str) -> Result<Cow<'static, [u8]>, ResolveError> {
crate::asset_source::read_bytes(backing)
.ok_or_else(|| ResolveError::BackingSourceMissing(backing.to_string()))
}
}
pub(crate) struct Resolver<A: SourceAdapter> {
manifest: PublicationManifest,
source: A,
}
impl<A: SourceAdapter> Resolver<A> {
pub(crate) fn new(manifest: PublicationManifest, source: A) -> Self {
Self { manifest, source }
}
pub(crate) fn manifest(&self) -> &PublicationManifest {
&self.manifest
}
pub(crate) fn resolve(
&self,
addr: &LogicalAddress,
) -> Result<Cow<'static, [u8]>, ResolveError> {
let entry = self
.manifest
.entries()
.iter()
.find(|e| e.address == *addr)
.ok_or_else(|| ResolveError::UnknownAddress(addr.as_str().to_string()))?;
self.source.read(&entry.backing)
}
pub(crate) fn emit(&self, addr: &LogicalAddress, out: &mut dyn Write) -> Result<(), EmitError> {
let bytes = self.resolve(addr)?;
out.write_all(&bytes)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn valid_entry(licence: &str, provenance: &str, customization: &str) -> String {
format!(
"[[entry]]\n\
address = \"templates/slice.toml\"\n\
backing = \"templates/slice.toml\"\n\
kind = \"template\"\n\
title = \"Slice metadata template\"\n\
licence = \"{licence}\"\n\
provenance = \"{provenance}\"\n\
customization = \"{customization}\"\n"
)
}
#[test]
fn mit_and_gpl_both_admit() {
for licence in ["MIT", "GPL"] {
let body = valid_entry(licence, "declared", "customizable");
PublicationManifest::admit(body.as_bytes())
.unwrap_or_else(|e| panic!("{licence} entry should admit: {e}"));
}
}
#[test]
fn out_of_set_licence_fails_closed() {
let body = valid_entry("BSD", "declared", "customizable");
let err = PublicationManifest::admit(body.as_bytes()).expect_err("BSD is out of set");
assert_eq!(
err,
AdmissionError::LicenceMissingOrOutOfSet {
index: 0,
value: "BSD".to_string()
}
);
}
#[test]
fn missing_field_fails_closed() {
let body = "[[entry]]\n\
address = \"templates/slice.toml\"\n\
backing = \"templates/slice.toml\"\n\
kind = \"template\"\n\
licence = \"MIT\"\n\
provenance = \"declared\"\n\
customization = \"customizable\"\n";
let err = PublicationManifest::admit(body.as_bytes()).expect_err("missing title");
assert_eq!(
err,
AdmissionError::MissingField {
index: 0,
field: "title"
}
);
}
#[test]
fn malformed_toml_fails_closed() {
let err = PublicationManifest::admit(b"this is [[[ not valid").expect_err("malformed");
assert!(matches!(err, AdmissionError::MalformedManifest(_)));
}
#[test]
fn unknown_provenance_fails_closed() {
let body = valid_entry("MIT", "hearsay", "customizable");
let err = PublicationManifest::admit(body.as_bytes()).expect_err("unknown provenance");
assert_eq!(
err,
AdmissionError::MissingField {
index: 0,
field: "provenance"
}
);
}
#[test]
fn unknown_customization_fails_closed() {
let body = valid_entry("MIT", "declared", "whatever");
let err = PublicationManifest::admit(body.as_bytes()).expect_err("unknown customization");
assert_eq!(
err,
AdmissionError::MissingField {
index: 0,
field: "customization"
}
);
}
#[test]
fn duplicate_address_rejected_at_admission() {
let one = valid_entry("MIT", "declared", "customizable");
let body = format!("{one}{one}"); let err = PublicationManifest::admit(body.as_bytes()).expect_err("dup address");
assert_eq!(
err,
AdmissionError::DuplicateAddress("templates/slice.toml".to_string())
);
}
#[test]
fn traversal_address_rejected_at_construction() {
for bad in ["/abs/path", "../escape", "a/../b", "", "a\\b", "./here"] {
assert!(
matches!(
LogicalAddress::parse(bad),
Err(AdmissionError::TraversalRejected(_))
),
"{bad:?} must be rejected"
);
}
assert!(LogicalAddress::parse("templates/slice.toml").is_ok());
}
#[test]
fn shipped_manifest_admits_from_disk_source() {
let path = crate::test_support::repo_root().join("publication/manifest.toml");
let bytes = std::fs::read(&path).expect("shipped publication/manifest.toml on disk");
let manifest = PublicationManifest::admit(&bytes).expect("shipped manifest admits");
assert!(
!manifest.entries().is_empty(),
"shipped manifest declares at least one entry"
);
for entry in manifest.entries() {
assert!(entry.report_line().contains("licence=MIT"));
}
}
#[test]
fn load_admits_the_embedded_manifest() {
let manifest = PublicationManifest::load().expect("embedded manifest admits");
assert!(!manifest.entries().is_empty());
}
use std::collections::BTreeMap;
struct MapAdapter {
bytes: BTreeMap<String, Vec<u8>>,
}
impl SourceAdapter for MapAdapter {
fn read(&self, backing: &str) -> Result<Cow<'static, [u8]>, ResolveError> {
self.bytes
.get(backing)
.map(|v| Cow::Owned(v.clone()))
.ok_or_else(|| ResolveError::BackingSourceMissing(backing.to_string()))
}
}
struct FailingWriter;
impl std::io::Write for FailingWriter {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("writer boom"))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn one_entry(address: &str, backing: &str) -> PublicationManifest {
let body = format!(
"[[entry]]\n\
address = \"{address}\"\n\
backing = \"{backing}\"\n\
kind = \"template\"\n\
title = \"Fixture\"\n\
licence = \"MIT\"\n\
provenance = \"declared\"\n\
customization = \"customizable\"\n"
);
PublicationManifest::admit(body.as_bytes()).expect("fixture admits")
}
fn shipped_resolver() -> Resolver<EmbeddedAdapter> {
Resolver::new(
PublicationManifest::load().expect("shipped manifest loads"),
EmbeddedAdapter,
)
}
#[test]
fn declared_address_resolves_to_exact_bytes() {
let resolver = shipped_resolver();
let addr = LogicalAddress::parse("templates/slice.toml").expect("addr");
let got = resolver.resolve(&addr).expect("declared address resolves");
let expected =
crate::asset_source::read_bytes("templates/slice.toml").expect("backing embed present");
assert_eq!(got, expected, "resolve returns the backing bytes verbatim");
}
#[test]
fn undeclared_address_is_unknown_not_empty() {
let resolver = shipped_resolver();
let addr = LogicalAddress::parse("templates/not-published.toml").expect("addr");
assert_eq!(
resolver.resolve(&addr).expect_err("undeclared address"),
ResolveError::UnknownAddress("templates/not-published.toml".to_string())
);
}
#[test]
fn absent_backing_is_backing_source_missing() {
let manifest = one_entry("templates/ghost.toml", "templates/does-not-exist.toml");
let resolver = Resolver::new(manifest, EmbeddedAdapter);
let addr = LogicalAddress::parse("templates/ghost.toml").expect("addr");
assert_eq!(
resolver.resolve(&addr).expect_err("absent backing"),
ResolveError::BackingSourceMissing("templates/does-not-exist.toml".to_string())
);
}
#[test]
fn emit_streams_framing_free_bytes() {
let resolver = shipped_resolver();
let addr = LogicalAddress::parse("templates/slice.toml").expect("addr");
let mut buf: Vec<u8> = Vec::new();
resolver.emit(&addr, &mut buf).expect("emits");
let expected =
crate::asset_source::read_bytes("templates/slice.toml").expect("backing embed present");
assert_eq!(buf.as_slice(), expected.as_ref());
}
#[test]
fn emit_surfaces_writer_io_failure() {
let resolver = shipped_resolver();
let addr = LogicalAddress::parse("templates/slice.toml").expect("addr");
let err = resolver
.emit(&addr, &mut FailingWriter)
.expect_err("writer failure surfaces");
assert!(matches!(err, EmitError::Io(_)));
}
#[test]
fn storage_independent_resolve_through_relocated_backing() {
let manifest = one_entry("templates/slice.toml", "relocated/elsewhere.bin");
let mut bytes = BTreeMap::new();
bytes.insert("relocated/elsewhere.bin".to_string(), b"payload".to_vec());
let resolver = Resolver::new(manifest, MapAdapter { bytes });
let addr = LogicalAddress::parse("templates/slice.toml").expect("addr");
assert_eq!(
resolver
.resolve(&addr)
.expect("resolves via relocated backing"),
Cow::Borrowed(b"payload".as_slice())
);
}
#[test]
fn emit_binary_bytes_round_trip() {
let raw = vec![0xff_u8, 0x00, 0xfe, 0x80, 0x01];
assert!(
std::str::from_utf8(&raw).is_err(),
"fixture must be genuinely non-UTF-8"
);
let manifest = one_entry("blobs/raw.bin", "blobs/raw.bin");
let mut bytes = BTreeMap::new();
bytes.insert("blobs/raw.bin".to_string(), raw.clone());
let resolver = Resolver::new(manifest, MapAdapter { bytes });
let addr = LogicalAddress::parse("blobs/raw.bin").expect("addr");
let mut buf: Vec<u8> = Vec::new();
resolver.emit(&addr, &mut buf).expect("emits binary");
assert_eq!(buf, raw, "binary bytes stream byte-for-byte");
}
#[test]
fn traversal_address_rejected_before_resolve() {
for bad in ["../escape", "/abs/path", "a/../b", "a\\b", "", "./here"] {
assert!(
matches!(
LogicalAddress::parse(bad),
Err(AdmissionError::TraversalRejected(_))
),
"{bad:?} must be rejected at construction, never reach resolve"
);
}
}
#[test]
fn resolve_emit_writes_nothing_to_disk() {
let tmp = tempfile::tempdir().expect("tempdir");
let resolver = shipped_resolver();
let mut sink = std::io::sink();
for entry in resolver.manifest().entries() {
resolver.emit(entry.address(), &mut sink).expect("emits");
}
assert!(
std::fs::read_dir(tmp.path())
.expect("read temp repo")
.next()
.is_none(),
"resolve/emit must not create any path in the temp repo"
);
}
}