use std::fmt;
use super::CompileError;
use super::manifest;
use super::store::{
STORE_SCHEMA, SourcePlan, StoreEntry, StoreEntryKind, StoreIndex, VirtualDocumentStore,
};
pub const MAGIC: [u8; 8] = *b"CAMELTR1";
pub const FOOTER_LEN: usize = 68;
pub const FORMAT_VERSION: u16 = 1;
pub const FOOTER_LEN_V2: usize = 76;
pub const FORMAT_VERSION_V2: u16 = 2;
const CHECKSUM_DOMAIN: &[u8] = b"rust-camel-trailer-v1";
const CHECKSUM_DOMAIN_V2: &[u8] = b"rust-camel-trailer-v2";
pub const MAX_PAYLOAD_BYTES: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrailerKind {
Route,
Job,
}
impl TrailerKind {
pub fn disc(self) -> u8 {
match self {
Self::Route => 1,
Self::Job => 2,
}
}
pub fn from_disc(disc: u8) -> Option<Self> {
match disc {
1 => Some(Self::Route),
2 => Some(Self::Job),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Route => "route",
Self::Job => "job",
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name {
"route" => Some(Self::Route),
"job" => Some(Self::Job),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TrailerError {
Truncated,
InvalidMagic,
InvalidVersion(u16),
InvalidKind(u8),
InvalidReserved(u8),
InvalidFlags(u8),
LengthOverflow,
LengthOutOfBounds,
ChecksumMismatch,
KindMismatch,
InvalidManifest,
InvalidManifestFields(String),
InvalidManifestSchema(u64),
InvalidStore(String),
}
impl fmt::Display for TrailerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Truncated => write!(
f,
"trailer is truncated (terminal magic present, footer incomplete)"
),
Self::InvalidMagic => write!(f, "trailer header magic is not {MAGIC:?}"),
Self::InvalidVersion(v) => write!(f, "unsupported trailer version {v}"),
Self::InvalidKind(k) => write!(f, "invalid trailer kind byte {k}"),
Self::InvalidReserved(r) => write!(f, "trailer reserved byte is {r}, must be zero"),
Self::InvalidFlags(flags) => {
write!(f, "trailer flags byte is {flags}, must be zero")
}
Self::LengthOverflow => write!(f, "trailer length fields overflow"),
Self::LengthOutOfBounds => write!(f, "trailer lengths exceed the artifact size"),
Self::ChecksumMismatch => write!(f, "trailer checksum mismatch"),
Self::KindMismatch => write!(f, "footer kind does not match the manifest kind"),
Self::InvalidManifest => write!(f, "trailer manifest is not valid manifest JSON"),
Self::InvalidManifestFields(reason) => {
write!(f, "manifest does not satisfy its schema: {reason}")
}
Self::InvalidManifestSchema(schema) => {
write!(f, "unsupported manifest schema {schema}")
}
Self::InvalidStore(reason) => write!(f, "embedded store is invalid: {reason}"),
}
}
}
impl std::error::Error for TrailerError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Trailer {
pub kind: TrailerKind,
pub payload: Vec<u8>,
pub manifest: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrailerV2 {
pub kind: TrailerKind,
pub content: Vec<u8>,
pub index: Vec<u8>,
pub manifest: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecodedArtifact {
V1(Trailer),
V2(TrailerV2),
}
impl Trailer {
pub fn to_single_entry_store(&self) -> Result<VirtualDocumentStore, TrailerError> {
let value: serde_json::Value =
serde_json::from_slice(&self.manifest).map_err(|_| TrailerError::InvalidManifest)?;
let source_name = value
.get("source_name")
.and_then(serde_json::Value::as_str)
.ok_or(TrailerError::InvalidManifest)?;
if source_name.is_empty() || source_name.contains('\0') {
return Err(TrailerError::InvalidManifest);
}
Ok(VirtualDocumentStore {
content: self.payload.clone(),
index: StoreIndex {
config_references: Vec::new(),
entry_point: source_name.to_string(),
entries: vec![StoreEntry {
kind: StoreEntryKind::from(self.kind),
length: self.payload.len() as u64,
offset: 0,
path: source_name.to_string(),
}],
source_plan: SourcePlan {
references: vec![source_name.to_string()],
},
store_schema: STORE_SCHEMA,
},
})
}
}
fn checksum_domain(
version: u16,
kind: u8,
payload_len: u64,
manifest_len: u64,
payload: &[u8],
manifest: &[u8],
) -> Vec<u8> {
let mut buf = Vec::with_capacity(
CHECKSUM_DOMAIN.len() + 1 + 2 + 1 + 8 + 8 + payload.len() + manifest.len(),
);
buf.extend_from_slice(CHECKSUM_DOMAIN);
buf.push(0x00);
buf.extend_from_slice(&version.to_le_bytes());
buf.push(kind);
buf.extend_from_slice(&payload_len.to_le_bytes());
buf.extend_from_slice(&manifest_len.to_le_bytes());
buf.extend_from_slice(payload);
buf.extend_from_slice(manifest);
buf
}
pub fn encode(trailer: &Trailer) -> Vec<u8> {
let mut out = Vec::with_capacity(trailer.payload.len() + trailer.manifest.len() + FOOTER_LEN);
out.extend_from_slice(&trailer.payload);
out.extend_from_slice(&trailer.manifest);
let mut footer = [0u8; FOOTER_LEN];
footer[0..8].copy_from_slice(&MAGIC);
footer[8..10].copy_from_slice(&FORMAT_VERSION.to_le_bytes());
footer[10] = trailer.kind.disc();
footer[12..20].copy_from_slice(&(trailer.payload.len() as u64).to_le_bytes());
footer[20..28].copy_from_slice(&(trailer.manifest.len() as u64).to_le_bytes());
let checksum = blake3::hash(&checksum_domain(
FORMAT_VERSION,
trailer.kind.disc(),
trailer.payload.len() as u64,
trailer.manifest.len() as u64,
&trailer.payload,
&trailer.manifest,
));
footer[28..60].copy_from_slice(checksum.as_bytes());
footer[60..68].copy_from_slice(&MAGIC);
out.extend_from_slice(&footer);
out
}
pub fn decode(bytes: &[u8]) -> Result<Option<Trailer>, TrailerError> {
if bytes.len() < MAGIC.len() || bytes[bytes.len() - MAGIC.len()..] != MAGIC {
return Ok(None);
}
if bytes.len() < FOOTER_LEN {
return Err(TrailerError::Truncated);
}
let footer = &bytes[bytes.len() - FOOTER_LEN..];
if footer[0..8] != MAGIC {
return Err(TrailerError::InvalidMagic);
}
let mut field2 = [0u8; 2];
field2.copy_from_slice(&footer[8..10]);
let version = u16::from_le_bytes(field2);
if version != FORMAT_VERSION {
return Err(TrailerError::InvalidVersion(version));
}
let kind_disc = footer[10];
let kind = TrailerKind::from_disc(kind_disc).ok_or(TrailerError::InvalidKind(kind_disc))?;
let reserved = footer[11];
if reserved != 0 {
return Err(TrailerError::InvalidReserved(reserved));
}
let mut field8 = [0u8; 8];
field8.copy_from_slice(&footer[12..20]);
let payload_len = u64::from_le_bytes(field8);
field8.copy_from_slice(&footer[20..28]);
let manifest_len = u64::from_le_bytes(field8);
let total_len = payload_len
.checked_add(manifest_len)
.ok_or(TrailerError::LengthOverflow)?;
let data_end = bytes.len() - FOOTER_LEN;
if total_len > data_end as u64 {
return Err(TrailerError::LengthOutOfBounds);
}
let payload_start = data_end - total_len as usize;
let payload_end = payload_start + payload_len as usize;
let payload = &bytes[payload_start..payload_end];
let manifest = &bytes[payload_end..data_end];
let checksum = blake3::hash(&checksum_domain(
version,
kind_disc,
payload_len,
manifest_len,
payload,
manifest,
));
if checksum.as_bytes() != &footer[28..60] {
return Err(TrailerError::ChecksumMismatch);
}
let manifest_kind = manifest::validate_manifest(manifest, FORMAT_VERSION)?;
if manifest_kind != kind {
return Err(TrailerError::KindMismatch);
}
Ok(Some(Trailer {
kind,
payload: payload.to_vec(),
manifest: manifest.to_vec(),
}))
}
fn checksum_domain_v2(
version: u16,
kind: u8,
content: &[u8],
index: &[u8],
manifest: &[u8],
) -> Vec<u8> {
let mut buf = Vec::with_capacity(
CHECKSUM_DOMAIN_V2.len()
+ 1
+ 2
+ 1
+ 8
+ 8
+ 8
+ content.len()
+ index.len()
+ manifest.len(),
);
buf.extend_from_slice(CHECKSUM_DOMAIN_V2);
buf.push(0x00);
buf.extend_from_slice(&version.to_le_bytes());
buf.push(kind);
buf.extend_from_slice(&(content.len() as u64).to_le_bytes());
buf.extend_from_slice(&(index.len() as u64).to_le_bytes());
buf.extend_from_slice(&(manifest.len() as u64).to_le_bytes());
buf.extend_from_slice(content);
buf.extend_from_slice(index);
buf.extend_from_slice(manifest);
buf
}
pub fn encode_v2(trailer: &TrailerV2) -> Vec<u8> {
let mut out = Vec::with_capacity(
MAGIC.len()
+ trailer.content.len()
+ trailer.index.len()
+ trailer.manifest.len()
+ FOOTER_LEN_V2,
);
out.extend_from_slice(&MAGIC);
out.extend_from_slice(&trailer.content);
out.extend_from_slice(&trailer.index);
out.extend_from_slice(&trailer.manifest);
let mut footer = [0u8; FOOTER_LEN_V2];
footer[0..8].copy_from_slice(&MAGIC);
footer[8..10].copy_from_slice(&FORMAT_VERSION_V2.to_le_bytes());
footer[10] = trailer.kind.disc();
footer[12..20].copy_from_slice(&(trailer.content.len() as u64).to_le_bytes());
footer[20..28].copy_from_slice(&(trailer.index.len() as u64).to_le_bytes());
footer[28..36].copy_from_slice(&(trailer.manifest.len() as u64).to_le_bytes());
let checksum = blake3::hash(&checksum_domain_v2(
FORMAT_VERSION_V2,
trailer.kind.disc(),
&trailer.content,
&trailer.index,
&trailer.manifest,
));
footer[36..68].copy_from_slice(checksum.as_bytes());
footer[68..76].copy_from_slice(&MAGIC);
out.extend_from_slice(&footer);
out
}
fn decode_v2_marked(bytes: &[u8]) -> Result<TrailerV2, TrailerError> {
if bytes.len() < FOOTER_LEN_V2 {
return Err(TrailerError::Truncated);
}
let footer = &bytes[bytes.len() - FOOTER_LEN_V2..];
if footer[0..8] != MAGIC {
return Err(TrailerError::InvalidMagic);
}
let mut field2 = [0u8; 2];
field2.copy_from_slice(&footer[8..10]);
let version = u16::from_le_bytes(field2);
if version != FORMAT_VERSION_V2 {
return Err(TrailerError::InvalidVersion(version));
}
let kind_disc = footer[10];
let kind = TrailerKind::from_disc(kind_disc).ok_or(TrailerError::InvalidKind(kind_disc))?;
let flags = footer[11];
if flags != 0 {
return Err(TrailerError::InvalidFlags(flags));
}
let mut field8 = [0u8; 8];
field8.copy_from_slice(&footer[12..20]);
let content_len = u64::from_le_bytes(field8);
field8.copy_from_slice(&footer[20..28]);
let index_len = u64::from_le_bytes(field8);
field8.copy_from_slice(&footer[28..36]);
let manifest_len = u64::from_le_bytes(field8);
let total_len = content_len
.checked_add(index_len)
.and_then(|len| len.checked_add(manifest_len))
.ok_or(TrailerError::LengthOverflow)?;
let data_end = bytes.len() - FOOTER_LEN_V2;
if total_len > data_end as u64 {
return Err(TrailerError::LengthOutOfBounds);
}
let content_start = data_end - total_len as usize;
let content_end = content_start + content_len as usize;
let index_end = content_end + index_len as usize;
let content = &bytes[content_start..content_end];
let index = &bytes[content_end..index_end];
let manifest = &bytes[index_end..data_end];
if content_start < MAGIC.len() || bytes[content_start - MAGIC.len()..content_start] != MAGIC {
return Err(TrailerError::InvalidMagic);
}
let checksum = blake3::hash(&checksum_domain_v2(
version, kind_disc, content, index, manifest,
));
if checksum.as_bytes() != &footer[36..68] {
return Err(TrailerError::ChecksumMismatch);
}
let manifest_kind = manifest::validate_manifest(manifest, FORMAT_VERSION_V2)?;
if manifest_kind != kind {
return Err(TrailerError::KindMismatch);
}
let decoded_index = StoreIndex::decode(index, content.len())
.map_err(|e| TrailerError::InvalidStore(e.to_string()))?;
super::store::validate_typed_references(&decoded_index, kind)
.map_err(|e| TrailerError::InvalidStore(e.to_string()))?;
validate_manifest_store_agreement(manifest, content, &decoded_index)?;
Ok(TrailerV2 {
kind,
content: content.to_vec(),
index: index.to_vec(),
manifest: manifest.to_vec(),
})
}
fn validate_manifest_store_agreement(
manifest: &[u8],
content: &[u8],
index: &StoreIndex,
) -> Result<(), TrailerError> {
let parsed = manifest::Manifest::from_canonical_json(manifest)
.map_err(|e| TrailerError::InvalidManifestFields(e.to_string()))?;
if parsed.source_name != index.entry_point {
return Err(TrailerError::InvalidManifestFields(format!(
"manifest source_name {:?} is not the store entry point {:?}",
parsed.source_name, index.entry_point
)));
}
if parsed.embedded_files.len() != index.entries.len() {
return Err(TrailerError::InvalidManifestFields(format!(
"embedded_files lists {} entries, the store index carries {}",
parsed.embedded_files.len(),
index.entries.len()
)));
}
for (file, entry) in parsed.embedded_files.iter().zip(&index.entries) {
if file.path != entry.path || file.kind != entry.kind || file.length != entry.length {
return Err(TrailerError::InvalidManifestFields(format!(
"embedded_files entry for {:?} disagrees with the store entry path/kind/length",
file.path
)));
}
let range = entry.offset as usize..(entry.offset + entry.length) as usize;
let Some(bytes) = content.get(range) else {
return Err(TrailerError::InvalidStore(
"entry range is out of content bounds".to_string(),
));
};
if file.digest != blake3::hash(bytes).to_hex().to_string() {
return Err(TrailerError::InvalidManifestFields(format!(
"embedded_files digest for {:?} does not match the store content",
file.path
)));
}
}
Ok(())
}
pub fn decode_artifact(bytes: &[u8]) -> Result<Option<DecodedArtifact>, TrailerError> {
if bytes.len() < MAGIC.len() || bytes[bytes.len() - MAGIC.len()..] != MAGIC {
return Ok(None);
}
if bytes.len() >= FOOTER_LEN_V2 {
let window = &bytes[bytes.len() - FOOTER_LEN_V2..];
let mut field2 = [0u8; 2];
field2.copy_from_slice(&window[8..10]);
let version = u16::from_le_bytes(field2);
if version == FORMAT_VERSION_V2 {
return decode_v2_marked(bytes).map(DecodedArtifact::V2).map(Some);
}
let footer_start = bytes.len() - FOOTER_LEN;
if bytes[footer_start..footer_start + MAGIC.len()] != MAGIC {
return Err(TrailerError::InvalidVersion(version));
}
}
decode(bytes).map(|trailer| trailer.map(DecodedArtifact::V1))
}
pub fn normalize_document(bytes: &[u8]) -> Result<String, CompileError> {
let text = std::str::from_utf8(bytes).map_err(|_| CompileError::InvalidUtf8)?;
let text = text.strip_prefix('\u{feff}').unwrap_or(text);
let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
if normalized.len() > MAX_PAYLOAD_BYTES {
return Err(CompileError::PayloadTooLarge);
}
Ok(normalized)
}
pub fn normalize_documents(documents: &[&[u8]]) -> Result<Vec<String>, CompileError> {
let mut normalized = Vec::with_capacity(documents.len());
let mut total = 0usize;
for document in documents {
let text = normalize_document(document)?;
total = total
.checked_add(text.len())
.ok_or(CompileError::PayloadTooLarge)?;
normalized.push(text);
}
if total > MAX_PAYLOAD_BYTES {
return Err(CompileError::PayloadTooLarge);
}
Ok(normalized)
}
#[cfg(test)]
const PAYLOAD: &[u8] = b"routes:\n- id: r\n from: direct:start\n";
#[cfg(test)]
const MANIFEST: &[u8] = br#"{"kind":"route","source_name":"routes/route.yaml"}"#;
#[cfg(test)]
fn sample(kind: TrailerKind) -> Trailer {
Trailer {
kind,
payload: PAYLOAD.to_vec(),
manifest: MANIFEST.to_vec(),
}
}
#[cfg(test)]
fn reseal(mut encoded: Vec<u8>) -> Vec<u8> {
let f = encoded.len() - FOOTER_LEN;
let mut b2 = [0u8; 2];
b2.copy_from_slice(&encoded[f + 8..f + 10]);
let version = u16::from_le_bytes(b2);
let kind = encoded[f + 10];
let mut b8 = [0u8; 8];
b8.copy_from_slice(&encoded[f + 12..f + 20]);
let payload_len = u64::from_le_bytes(b8);
b8.copy_from_slice(&encoded[f + 20..f + 28]);
let manifest_len = u64::from_le_bytes(b8);
let data_end = f;
let payload_start = data_end - (payload_len + manifest_len) as usize;
let payload = &encoded[payload_start..payload_start + payload_len as usize];
let manifest = &encoded[payload_start + payload_len as usize..data_end];
let sum = blake3::hash(&checksum_domain(
version,
kind,
payload_len,
manifest_len,
payload,
manifest,
));
encoded[f + 28..f + 60].copy_from_slice(sum.as_bytes());
encoded
}
#[test]
fn trailer_round_trip_preserves_payload_and_manifest() {
let trailer = sample(TrailerKind::Route);
let encoded = encode(&trailer);
let decoded = decode(&encoded)
.expect("valid encoding must decode")
.expect("terminal magic must mark the trailer present");
assert_eq!(decoded.payload, PAYLOAD);
assert_eq!(decoded.manifest, MANIFEST);
assert_eq!(decoded.kind, TrailerKind::Route);
let footer = &encoded[encoded.len() - FOOTER_LEN..];
let expected = blake3::hash(&checksum_domain(
FORMAT_VERSION,
TrailerKind::Route.disc(),
PAYLOAD.len() as u64,
MANIFEST.len() as u64,
PAYLOAD,
MANIFEST,
));
assert_eq!(&footer[28..60], expected.as_bytes());
}
#[test]
fn trailer_encoding_uses_exact_68_byte_footer() {
let payload = b"payload";
let manifest: &[u8] = br#"{"kind":"job"}"#;
let encoded = encode(&Trailer {
kind: TrailerKind::Job,
payload: payload.to_vec(),
manifest: manifest.to_vec(),
});
assert_eq!(encoded.len(), payload.len() + manifest.len() + 68);
assert_eq!(&encoded[..payload.len()], payload);
assert_eq!(
&encoded[payload.len()..payload.len() + manifest.len()],
manifest
);
let footer = &encoded[encoded.len() - 68..];
assert_eq!(&footer[0..8], &MAGIC, "leading magic");
assert_eq!(&footer[8..10], &1u16.to_le_bytes(), "version u16 LE");
assert_eq!(footer[10], 2, "kind 2=job");
assert_eq!(footer[11], 0, "reserved byte zero");
assert_eq!(&footer[12..20], &(payload.len() as u64).to_le_bytes());
assert_eq!(&footer[20..28], &(manifest.len() as u64).to_le_bytes());
assert_eq!(&footer[60..68], &MAGIC, "terminal magic");
let expected = blake3::hash(&checksum_domain(
FORMAT_VERSION,
TrailerKind::Job.disc(),
payload.len() as u64,
manifest.len() as u64,
payload,
manifest,
));
assert_eq!(&footer[28..60], expected.as_bytes(), "BLAKE3 checksum");
}
#[test]
fn trailer_rejects_marked_corruption_invalid_version_lengths_and_kind() {
let encoded = encode(&sample(TrailerKind::Route));
let f = encoded.len() - FOOTER_LEN;
let mut corrupted = encoded.clone();
corrupted[0] ^= 0xFF;
assert_eq!(decode(&corrupted), Err(TrailerError::ChecksumMismatch));
let mut corrupted = encoded.clone();
corrupted[f + 28] ^= 0xFF;
assert_eq!(decode(&corrupted), Err(TrailerError::ChecksumMismatch));
let mut corrupted = encoded.clone();
corrupted[f + 8..f + 10].copy_from_slice(&2u16.to_le_bytes());
assert_eq!(decode(&corrupted), Err(TrailerError::InvalidVersion(2)));
let mut corrupted = encoded.clone();
corrupted[f + 12..f + 20].copy_from_slice(&u64::MAX.to_le_bytes());
corrupted[f + 20..f + 28].copy_from_slice(&1u64.to_le_bytes());
assert_eq!(decode(&corrupted), Err(TrailerError::LengthOverflow));
let mut corrupted = encoded.clone();
corrupted[f + 12..f + 20].copy_from_slice(&(1u64 << 40).to_le_bytes());
assert_eq!(decode(&corrupted), Err(TrailerError::LengthOutOfBounds));
let mut corrupted = encoded.clone();
corrupted[f + 10] = 7;
let corrupted = reseal(corrupted);
assert_eq!(decode(&corrupted), Err(TrailerError::InvalidKind(7)));
let mut corrupted = encoded.clone();
corrupted[f + 11] = 1;
let corrupted = reseal(corrupted);
assert_eq!(decode(&corrupted), Err(TrailerError::InvalidReserved(1)));
let mut corrupted = encoded.clone();
corrupted[f + 10] = TrailerKind::Job.disc();
let corrupted = reseal(corrupted);
assert_eq!(decode(&corrupted), Err(TrailerError::KindMismatch));
let no_kind = encode(&Trailer {
kind: TrailerKind::Route,
payload: PAYLOAD.to_vec(),
manifest: b"{}".to_vec(),
});
assert_eq!(decode(&no_kind), Err(TrailerError::InvalidManifest));
let mut corrupted = encoded.clone();
corrupted[f..f + 8].copy_from_slice(b"OTHERTR1");
assert_eq!(decode(&corrupted), Err(TrailerError::InvalidMagic));
let mut short = Vec::new();
short.extend_from_slice(b"ab");
short.extend_from_slice(&MAGIC);
short.extend_from_slice(&MAGIC);
assert_eq!(short.len(), 18);
assert_eq!(decode(&short), Err(TrailerError::Truncated));
}
#[test]
fn trailer_without_terminal_marker_is_absent() {
assert_eq!(decode(b"an ordinary executable image"), Ok(None));
assert_eq!(decode(b""), Ok(None));
assert_eq!(decode(b"CAMEL"), Ok(None));
let encoded = encode(&sample(TrailerKind::Route));
let truncated = &encoded[..encoded.len() - 4];
assert_eq!(decode(truncated), Ok(None));
assert_eq!(decode(b"x\x00CAMELTR"), Ok(None));
}
#[test]
fn normalization_removes_bom_and_normalizes_line_endings() {
let bom = "\u{feff}";
let input = format!("{bom}a\r\nb\rc");
assert_eq!(normalize_document(input.as_bytes()), Ok("a\nb\nc".into()));
assert_eq!(normalize_document(b"x\r\n"), Ok("x\n".into()));
assert_eq!(normalize_document(b"x"), Ok("x".into()));
assert_eq!(
normalize_document(&[0xFF, 0xFE]),
Err(CompileError::InvalidUtf8)
);
let at_cap = vec![b'a'; MAX_PAYLOAD_BYTES];
assert_eq!(
normalize_document(&at_cap),
Ok("a".repeat(MAX_PAYLOAD_BYTES))
);
let over_cap = vec![b'a'; MAX_PAYLOAD_BYTES + 1];
assert_eq!(
normalize_document(&over_cap),
Err(CompileError::PayloadTooLarge)
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trailer_round_trip_preserves_job_kind() {
let mut trailer = sample(TrailerKind::Job);
trailer.manifest = br#"{"kind":"job"}"#.to_vec();
let decoded = decode(&encode(&trailer))
.expect("valid encoding must decode")
.expect("terminal magic must mark the trailer present");
assert_eq!(decoded.kind, TrailerKind::Job);
}
}
#[cfg(test)]
use super::manifest::Manifest;
#[cfg(test)]
use super::store::StoreDocument;
#[cfg(test)]
fn digest(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
#[cfg(test)]
fn sample_documents() -> [StoreDocument; 3] {
[
StoreDocument {
path: "cfg/camel.toml".to_string(),
kind: StoreEntryKind::Config,
bytes: b"[profiles.default]\n".to_vec(),
},
StoreDocument {
path: "routes/main.yaml".to_string(),
kind: StoreEntryKind::Route,
bytes: b"routes:\n- id: main\n from: direct:in\n".to_vec(),
},
StoreDocument {
path: "routes/other.yaml".to_string(),
kind: StoreEntryKind::Route,
bytes: b"routes:\n- id: other\n from: direct:aux\n".to_vec(),
},
]
}
#[cfg(test)]
fn sample_manifest(documents: &[StoreDocument], index: &StoreIndex) -> Manifest {
Manifest {
manifest_schema: manifest::MANIFEST_SCHEMA,
source_name: "routes/main.yaml".to_string(),
runtime_version: manifest::RUNTIME_VERSION.to_string(),
kind: TrailerKind::Route,
components: vec!["direct".to_string()],
env_names: vec![],
listeners: vec![],
embedded_files: index
.entries
.iter()
.map(|entry| {
let bytes = &documents
.iter()
.find(|d| d.path == entry.path)
.expect("entry must reference a sampled document")
.bytes;
manifest::EmbeddedFile {
digest: digest(bytes),
kind: entry.kind,
length: entry.length,
path: entry.path.clone(),
}
})
.collect(),
}
}
#[test]
fn v2_trailer_round_trip_preserves_store_and_manifest() {
let documents = sample_documents();
let store = VirtualDocumentStore::build(
"routes/main.yaml",
&documents,
&["cfg/camel.toml".to_string()],
&[
"routes/main.yaml".to_string(),
"routes/other.yaml".to_string(),
],
)
.expect("store must build");
let index_bytes = store.index.encode_canonical().expect("index encodes");
let manifest_struct = sample_manifest(&documents, &store.index);
let manifest_bytes = manifest_struct.to_canonical_json().into_bytes();
let trailer = TrailerV2 {
kind: TrailerKind::Route,
content: store.content.clone(),
index: index_bytes.clone(),
manifest: manifest_bytes.clone(),
};
let encoded = encode_v2(&trailer);
let decoded = decode_artifact(&encoded)
.expect("valid v2 encoding must decode")
.expect("terminal magic must mark the trailer present");
let DecodedArtifact::V2(got) = decoded else {
panic!("v2 image must decode as DecodedArtifact::V2");
};
assert_eq!(got.kind, TrailerKind::Route);
assert_eq!(got.content, store.content);
assert_eq!(got.index, index_bytes);
assert_eq!(got.manifest, manifest_bytes);
let reparsed = VirtualDocumentStore::decode(got.content.clone(), &got.index)
.expect("decoded index must validate against the content");
assert_eq!(reparsed.index, store.index);
let paths: Vec<&str> = reparsed
.index
.entries
.iter()
.map(|e| e.path.as_str())
.collect();
assert_eq!(
paths,
["cfg/camel.toml", "routes/main.yaml", "routes/other.yaml"]
);
let kinds: Vec<StoreEntryKind> = reparsed.index.entries.iter().map(|e| e.kind).collect();
assert_eq!(
kinds,
[
StoreEntryKind::Config,
StoreEntryKind::Route,
StoreEntryKind::Route
]
);
let mut expected_offset = 0u64;
for entry in &reparsed.index.entries {
assert_eq!(entry.offset, expected_offset, "range offsets are canonical");
expected_offset += entry.length;
assert_eq!(
reparsed.read(&entry.path),
Some(
documents
.iter()
.find(|d| d.path == entry.path)
.map(|d| d.bytes.as_slice())
.expect("path must round-trip")
),
"content range must address the original bytes"
);
}
assert_eq!(
reparsed.index.source_plan.references,
["routes/main.yaml", "routes/other.yaml"]
);
let got_manifest =
Manifest::from_canonical_json(&got.manifest).expect("decoded manifest must validate");
assert_eq!(got_manifest, manifest_struct);
assert_eq!(got_manifest.manifest_schema, manifest::MANIFEST_SCHEMA);
let footer = &encoded[encoded.len() - FOOTER_LEN_V2..];
let expected = blake3::hash(&checksum_domain_v2(
FORMAT_VERSION_V2,
TrailerKind::Route.disc(),
&got.content,
&got.index,
&got.manifest,
));
assert_eq!(&footer[36..68], expected.as_bytes());
}
#[test]
fn v2_trailer_uses_exact_footer_and_checksum_domain() {
let documents = sample_documents();
let store = VirtualDocumentStore::build(
"routes/main.yaml",
&documents,
&["cfg/camel.toml".to_string()],
&[
"routes/main.yaml".to_string(),
"routes/other.yaml".to_string(),
],
)
.expect("store must build");
let content = store.content.clone();
let index = store.index.encode_canonical().expect("index encodes");
let manifest = sample_manifest(&documents, &store.index)
.to_canonical_json()
.into_bytes();
let encoded = encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: content.clone(),
index: index.clone(),
manifest: manifest.clone(),
});
assert_eq!(
encoded.len(),
MAGIC.len() + content.len() + index.len() + manifest.len() + 76
);
assert_eq!(&encoded[..8], &MAGIC, "leading CAMELTR1");
assert_eq!(&encoded[8..8 + content.len()], &content[..]);
assert_eq!(
&encoded[8 + content.len()..8 + content.len() + index.len()],
&index[..]
);
assert_eq!(
&encoded[8 + content.len() + index.len()..encoded.len() - 76],
&manifest[..]
);
let footer = &encoded[encoded.len() - 76..];
assert_eq!(&footer[0..8], &MAGIC, "footer leading magic");
assert_eq!(&footer[8..10], &2u16.to_le_bytes(), "version 2");
assert_eq!(footer[10], 1, "kind 1=route");
assert_eq!(footer[11], 0, "zero flags");
assert_eq!(
&footer[12..20],
&(content.len() as u64).to_le_bytes(),
"content_len"
);
assert_eq!(
&footer[20..28],
&(index.len() as u64).to_le_bytes(),
"index_len"
);
assert_eq!(
&footer[28..36],
&(manifest.len() as u64).to_le_bytes(),
"manifest_len"
);
assert_eq!(&footer[68..76], &MAGIC, "terminal CAMELTR1");
let mut domain = Vec::new();
domain.extend_from_slice(b"rust-camel-trailer-v2");
domain.push(0x00);
domain.extend_from_slice(&2u16.to_le_bytes());
domain.push(1);
domain.extend_from_slice(&(content.len() as u64).to_le_bytes());
domain.extend_from_slice(&(index.len() as u64).to_le_bytes());
domain.extend_from_slice(&(manifest.len() as u64).to_le_bytes());
domain.extend_from_slice(&content);
domain.extend_from_slice(&index);
domain.extend_from_slice(&manifest);
assert_eq!(
&footer[36..68],
blake3::hash(&domain).as_bytes(),
"checksum domain"
);
let mut corrupted = encoded.clone();
corrupted[0] ^= 0xFF;
assert_eq!(decode_artifact(&corrupted), Err(TrailerError::InvalidMagic));
assert!(decode_artifact(&encoded).is_ok());
}
#[test]
fn v2_decoder_accepts_v1_as_single_entry_store() {
let payload = b"jobs:\n- id: nightly\n from: cron:0 0 * * *\n".to_vec();
let v1 = Trailer {
kind: TrailerKind::Job,
payload: payload.clone(),
manifest: br#"{"kind":"job","source_name":"jobs/nightly.job.yaml"}"#.to_vec(),
};
let encoded = encode(&v1);
let decoded = decode_artifact(&encoded)
.expect("valid v1 encoding must decode")
.expect("terminal magic must mark the trailer present");
let DecodedArtifact::V1(trailer) = decoded else {
panic!("v1 image must decode as DecodedArtifact::V1");
};
assert_eq!(trailer.payload, payload);
let store = trailer
.to_single_entry_store()
.expect("v1 artifact must expose a one-entry store");
assert_eq!(store.index.entries.len(), 1);
let entry = &store.index.entries[0];
assert_eq!(
entry.path, "jobs/nightly.job.yaml",
"original source identity"
);
assert_eq!(entry.kind, StoreEntryKind::Job);
assert_eq!(entry.offset, 0);
assert_eq!(entry.length, payload.len() as u64);
assert_eq!(store.index.entry_point, "jobs/nightly.job.yaml");
assert_eq!(
store.index.source_plan.references,
["jobs/nightly.job.yaml"]
);
assert_eq!(
store.read("jobs/nightly.job.yaml"),
Some(payload.as_slice())
);
let index_bytes = store.index.encode_canonical().expect("index encodes");
assert!(StoreIndex::decode(&index_bytes, store.content.len()).is_ok());
}
#[test]
fn v1_store_adaptation_preserves_legacy_source_names() {
let payload = b"routes:\n- id: a\n from: direct:a\n".to_vec();
for name in ["/abs/dir/app.yaml", "../shared/app.yaml"] {
let encoded = encode(&Trailer {
kind: TrailerKind::Route,
payload: payload.clone(),
manifest: format!(r#"{{"kind":"route","source_name":"{name}"}}"#).into_bytes(),
});
let DecodedArtifact::V1(trailer) = decode_artifact(&encoded)
.expect("valid v1 encoding must decode")
.expect("terminal magic must mark the trailer present")
else {
panic!("v1 image must decode as DecodedArtifact::V1");
};
let store = trailer
.to_single_entry_store()
.expect("legacy v1 source identity must adapt");
assert_eq!(store.index.entry_point, name);
assert_eq!(store.index.entries.len(), 1);
assert_eq!(store.index.entries[0].path, name);
assert_eq!(store.index.source_plan.references, [name]);
assert_eq!(store.read(name), Some(payload.as_slice()));
}
for manifest in [
&br#"{"kind":"route"}"#[..],
br#"{"kind":"route","source_name":""}"#,
br#"{"kind":"route","source_name":"a\u0000b.yaml"}"#,
] {
let trailer = Trailer {
kind: TrailerKind::Route,
payload: payload.clone(),
manifest: manifest.to_vec(),
};
assert_eq!(
trailer.to_single_entry_store(),
Err(TrailerError::InvalidManifest),
"manifest {manifest:?} carries no usable source identity"
);
}
}
#[test]
fn aggregate_normalization_preserves_utf8_bom_newlines_and_cap() {
let docs: Vec<&[u8]> = vec![
b"\xEF\xBB\xBFroutes:\n- id: a\r\n from: direct:a\n",
b"routes:\n- id: b\rfrom: direct:b",
];
let normalized = normalize_documents(&docs).expect("valid set must normalize");
assert_eq!(normalized[0], "routes:\n- id: a\n from: direct:a\n");
assert_eq!(normalized[1], "routes:\n- id: b\nfrom: direct:b");
let invalid: Vec<&[u8]> = vec![b"ok", &[0xFF, 0xFE]];
assert_eq!(
normalize_documents(&invalid),
Err(CompileError::InvalidUtf8)
);
let half = vec![b'a'; MAX_PAYLOAD_BYTES / 2 + 1];
let over: Vec<&[u8]> = vec![&half, &half];
assert_eq!(
normalize_documents(&over),
Err(CompileError::PayloadTooLarge)
);
let half = &half[..MAX_PAYLOAD_BYTES / 2];
let at_cap: Vec<&[u8]> = vec![half, half];
let normalized = normalize_documents(&at_cap).expect("aggregate at cap must pass");
assert_eq!(normalized[0].len() + normalized[1].len(), MAX_PAYLOAD_BYTES);
}
#[test]
fn marked_v2_family_footer_with_unsupported_version_reports_invalid_version() {
let documents = sample_documents();
let store = VirtualDocumentStore::build(
"routes/main.yaml",
&documents,
&[],
&["routes/main.yaml".to_string()],
)
.expect("store must build");
let encoded = encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content,
index: store.index.encode_canonical().expect("index encodes"),
manifest: sample_manifest(&documents, &store.index)
.to_canonical_json()
.into_bytes(),
});
let f = encoded.len() - FOOTER_LEN_V2;
for version in [3u16, 9, u16::MAX] {
let mut corrupted = encoded.clone();
corrupted[f + 8..f + 10].copy_from_slice(&version.to_le_bytes());
assert_eq!(
decode_artifact(&corrupted),
Err(TrailerError::InvalidVersion(version)),
"version {version} must be named by the v2-family dispatch"
);
}
let v1 = encode(&Trailer {
kind: TrailerKind::Route,
payload: PAYLOAD.to_vec(),
manifest: br#"{"kind":"route","source_name":"routes/route.yaml"}"#.to_vec(),
});
assert!(matches!(
decode_artifact(&v1),
Ok(Some(DecodedArtifact::V1(_)))
));
}
#[test]
fn v2_manifest_store_disagreement_fails_closed() {
let documents = sample_documents();
let store = VirtualDocumentStore::build(
"routes/main.yaml",
&documents,
&["cfg/camel.toml".to_string()],
&[
"routes/main.yaml".to_string(),
"routes/other.yaml".to_string(),
],
)
.expect("store must build");
let short_manifest = Manifest {
manifest_schema: manifest::MANIFEST_SCHEMA,
source_name: "routes/main.yaml".to_string(),
runtime_version: manifest::RUNTIME_VERSION.to_string(),
kind: TrailerKind::Route,
components: vec![],
env_names: vec![],
listeners: vec![],
embedded_files: store.index.entries[..1]
.iter()
.map(|entry| manifest::EmbeddedFile {
digest: digest(b"[profiles.default]\n"),
kind: entry.kind,
length: entry.length,
path: entry.path.clone(),
})
.collect(),
};
let encoded = encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content.clone(),
index: store.index.encode_canonical().expect("index encodes"),
manifest: short_manifest.to_canonical_json().into_bytes(),
});
assert!(matches!(
decode_artifact(&encoded),
Err(TrailerError::InvalidManifestFields(reason))
if reason.contains("lists 1 entries, the store index carries 3")
));
let mut wrong_digest = sample_manifest(&documents, &store.index);
wrong_digest.embedded_files[1].digest = digest(b"tampered");
let encoded = encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content.clone(),
index: store.index.encode_canonical().expect("index encodes"),
manifest: wrong_digest.to_canonical_json().into_bytes(),
});
assert!(matches!(
decode_artifact(&encoded),
Err(TrailerError::InvalidManifestFields(reason))
if reason.contains("digest for \"routes/main.yaml\" does not match")
));
let mut wrong_length = sample_manifest(&documents, &store.index);
wrong_length.embedded_files[2].length += 1;
let encoded = encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content,
index: store.index.encode_canonical().expect("index encodes"),
manifest: wrong_length.to_canonical_json().into_bytes(),
});
assert!(matches!(
decode_artifact(&encoded),
Err(TrailerError::InvalidManifestFields(reason))
if reason.contains("disagrees with the store entry path/kind/length")
));
}
#[test]
fn v2_manifest_source_name_entry_point_mismatch_fails_closed() {
let documents = sample_documents();
let store = VirtualDocumentStore::build(
"routes/main.yaml",
&documents,
&[],
&["routes/main.yaml".to_string()],
)
.expect("store must build");
let mut manifest = sample_manifest(&documents, &store.index);
manifest.source_name = "routes/other.yaml".to_string();
let encoded = encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content,
index: store.index.encode_canonical().expect("index encodes"),
manifest: manifest.to_canonical_json().into_bytes(),
});
assert!(matches!(
decode_artifact(&encoded),
Err(TrailerError::InvalidManifestFields(reason))
if reason.contains(
"manifest source_name \"routes/other.yaml\" is not the store entry point \
\"routes/main.yaml\""
)
));
}
#[cfg(test)]
mod v2_corruption_tests {
use super::*;
#[test]
fn v2_decoder_rejects_marked_corruption() {
let documents = sample_documents();
let store = VirtualDocumentStore::build(
"routes/main.yaml",
&documents,
&[],
&["routes/main.yaml".to_string()],
)
.expect("store must build");
let index_bytes = store.index.encode_canonical().expect("index encodes");
let manifest_bytes = sample_manifest(&documents, &store.index)
.to_canonical_json()
.into_bytes();
let encoded = encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content,
index: index_bytes,
manifest: manifest_bytes,
});
let f = encoded.len() - FOOTER_LEN_V2;
let mut corrupted = encoded.clone();
corrupted[10] ^= 0xFF;
assert_eq!(
decode_artifact(&corrupted),
Err(TrailerError::ChecksumMismatch)
);
let mut corrupted = encoded.clone();
corrupted[f + 36] ^= 0xFF;
assert_eq!(
decode_artifact(&corrupted),
Err(TrailerError::ChecksumMismatch)
);
let mut corrupted = encoded.clone();
corrupted[f + 11] = 1;
let corrupted = reseal_v2(corrupted);
assert_eq!(
decode_artifact(&corrupted),
Err(TrailerError::InvalidFlags(1))
);
let mut corrupted = encoded.clone();
corrupted[f + 10] = TrailerKind::Job.disc();
let corrupted = reseal_v2(corrupted);
assert_eq!(decode_artifact(&corrupted), Err(TrailerError::KindMismatch));
let documents = sample_documents();
let store = VirtualDocumentStore::build(
"routes/main.yaml",
&documents,
&[],
&["routes/main.yaml".to_string()],
)
.expect("store must build");
let mut bad_schema_manifest = sample_manifest(&documents, &store.index)
.to_canonical_json()
.into_bytes();
let needle = br#""manifest_schema":2"#.to_vec();
let pos = bad_schema_manifest
.windows(needle.len())
.position(|w| w == needle.as_slice())
.expect("manifest carries its schema field");
bad_schema_manifest.splice(
pos..pos + needle.len(),
br#""manifest_schema":99"#.iter().copied(),
);
let corrupted = encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content.clone(),
index: store.index.encode_canonical().expect("index encodes"),
manifest: bad_schema_manifest,
});
assert_eq!(
decode_artifact(&corrupted),
Err(TrailerError::InvalidManifestSchema(99))
);
let mut corrupted = encoded.clone();
corrupted[f + 12..f + 20].copy_from_slice(&(1u64 << 40).to_le_bytes());
assert_eq!(
decode_artifact(&corrupted),
Err(TrailerError::LengthOutOfBounds)
);
let mut short = Vec::new();
short.extend_from_slice(b"ab");
short.extend_from_slice(&MAGIC);
short.extend_from_slice(&MAGIC);
assert_eq!(decode_artifact(&short), Err(TrailerError::Truncated));
assert_eq!(decode_artifact(b"an ordinary executable image"), Ok(None));
assert!(decode(&encoded).is_err());
assert_ne!(decode(&encoded), Ok(None));
}
#[cfg(test)]
fn reseal_v2(mut encoded: Vec<u8>) -> Vec<u8> {
let f = encoded.len() - FOOTER_LEN_V2;
let mut b2 = [0u8; 2];
b2.copy_from_slice(&encoded[f + 8..f + 10]);
let version = u16::from_le_bytes(b2);
let kind = encoded[f + 10];
let mut b8 = [0u8; 8];
b8.copy_from_slice(&encoded[f + 12..f + 20]);
let content_len = u64::from_le_bytes(b8);
b8.copy_from_slice(&encoded[f + 20..f + 28]);
let index_len = u64::from_le_bytes(b8);
b8.copy_from_slice(&encoded[f + 28..f + 36]);
let manifest_len = u64::from_le_bytes(b8);
let data_end = f;
let content_start = data_end - (content_len + index_len + manifest_len) as usize;
let content = &encoded[content_start..content_start + content_len as usize];
let index = &encoded[content_start + content_len as usize
..content_start + (content_len + index_len) as usize];
let manifest = &encoded[content_start + (content_len + index_len) as usize..data_end];
let sum = blake3::hash(&checksum_domain_v2(version, kind, content, index, manifest));
encoded[f + 36..f + 68].copy_from_slice(sum.as_bytes());
encoded
}
#[test]
fn v2_resealed_invalid_index_fails_closed() {
let documents = sample_documents();
let store = VirtualDocumentStore::build(
"routes/main.yaml",
&documents,
&["cfg/camel.toml".to_string()],
&[
"routes/main.yaml".to_string(),
"routes/other.yaml".to_string(),
],
)
.expect("store must build");
let content_len = store.content.len();
let index_bytes = store.index.encode_canonical().expect("index encodes");
let manifest_bytes = sample_manifest(&documents, &store.index)
.to_canonical_json()
.into_bytes();
let encoded = encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content,
index: index_bytes,
manifest: manifest_bytes,
});
let index_start = MAGIC.len() + content_len;
let mut corrupted = encoded.clone();
corrupted[index_start] = b'x';
assert_eq!(
decode_artifact(&corrupted),
Err(TrailerError::ChecksumMismatch)
);
let corrupted = reseal_v2(corrupted);
assert!(matches!(
decode_artifact(&corrupted),
Err(TrailerError::InvalidStore(_))
));
}
#[test]
fn v2_resealed_typed_reference_mismatch_fails_closed() {
let documents = sample_documents();
let cfg = "cfg/camel.toml".to_string();
let main = "routes/main.yaml".to_string();
let other = "routes/other.yaml".to_string();
let cases = [
(
cfg.clone(),
vec![cfg.clone()],
vec![main.clone()],
"expected route",
),
(
main.clone(),
vec![other.clone()],
vec![main.clone()],
"config, include, or profile",
),
(main.clone(), vec![cfg.clone()], vec![cfg], "route or job"),
];
for (entry_point, config_references, source_plan, reason) in cases {
let store = VirtualDocumentStore::build(
&entry_point,
&documents,
&config_references,
&source_plan,
)
.expect("references name existing entries");
let encoded = encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content,
index: store.index.encode_canonical().expect("index encodes"),
manifest: sample_manifest(&documents, &store.index)
.to_canonical_json()
.into_bytes(),
});
let err =
decode_artifact(&encoded).expect_err("typed-reference mismatch must fail closed");
assert!(
matches!(&err, TrailerError::InvalidStore(msg) if msg.contains(reason)),
"unexpected error for entry point {entry_point:?}: {err:?}"
);
}
}
#[test]
fn v2_manifest_strictness_rejects_incomplete_and_legacy_forms() {
let documents = sample_documents();
let store = VirtualDocumentStore::build(
"routes/main.yaml",
&documents,
&["cfg/camel.toml".to_string()],
&[
"routes/main.yaml".to_string(),
"routes/other.yaml".to_string(),
],
)
.expect("store must build");
let encode_with = |manifest: Vec<u8>| {
encode_v2(&TrailerV2 {
kind: TrailerKind::Route,
content: store.content.clone(),
index: store.index.encode_canonical().expect("index encodes"),
manifest,
})
};
let incomplete = format!(
r#"{{"components":["direct"],"env_names":[],"kind":"route","listeners":[],"manifest_schema":2,"runtime_version":"{rt}","source_name":"routes/main.yaml"}}"#,
rt = manifest::RUNTIME_VERSION,
);
assert_eq!(
decode_artifact(&encode_with(incomplete.into_bytes())),
Err(TrailerError::InvalidManifestFields(
"schema-2 manifest carries no embedded_files array".to_string()
))
);
let no_runtime = br#"{"components":[],"embedded_files":[],"env_names":[],"kind":"route","listeners":[],"manifest_schema":2,"source_name":"routes/main.yaml"}"#.to_vec();
assert_eq!(
decode_artifact(&encode_with(no_runtime)),
Err(TrailerError::InvalidManifestFields(
"schema-2 manifest carries no runtime_version".to_string()
))
);
let legacy = br#"{"kind":"route","source_name":"routes/main.yaml"}"#.to_vec();
assert_eq!(
decode_artifact(&encode_with(legacy.clone())),
Err(TrailerError::InvalidManifestSchema(
manifest::MANIFEST_SCHEMA_LEGACY
))
);
let v1 = encode(&Trailer {
kind: TrailerKind::Route,
payload: b"routes:\n- id: main\n from: direct:in\n".to_vec(),
manifest: legacy,
});
assert!(decode_artifact(&v1).is_ok());
let v1_schema2_manifest = format!(
r#"{{"components":[],"embedded_files":[],"env_names":[],"kind":"route","listeners":[],"manifest_schema":2,"runtime_version":"{rt}","source_name":"routes/main.yaml"}}"#,
rt = manifest::RUNTIME_VERSION,
);
let v1_schema2 = encode(&Trailer {
kind: TrailerKind::Route,
payload: b"routes:\n- id: main\n from: direct:in\n".to_vec(),
manifest: v1_schema2_manifest.into_bytes(),
});
assert_eq!(
decode_artifact(&v1_schema2),
Err(TrailerError::InvalidManifestSchema(
manifest::MANIFEST_SCHEMA
))
);
}
}