use crate::error::{Error, Result};
use crate::field::provenance::json_escape;
const EXTERNAL_MAGIC: &[u8; 8] = b"VOLECTX1";
const EXTERNAL_FORMAT_VERSION: u8 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExternalOrigin {
#[default]
Harness,
Operator,
Catalog,
}
impl ExternalOrigin {
pub const fn name(self) -> &'static str {
match self {
ExternalOrigin::Harness => "harness",
ExternalOrigin::Operator => "operator",
ExternalOrigin::Catalog => "catalog",
}
}
const fn code(self) -> u8 {
match self {
ExternalOrigin::Harness => 0,
ExternalOrigin::Operator => 1,
ExternalOrigin::Catalog => 2,
}
}
fn from_code(b: u8) -> Result<Self> {
match b {
0 => Ok(ExternalOrigin::Harness),
1 => Ok(ExternalOrigin::Operator),
2 => Ok(ExternalOrigin::Catalog),
other => Err(Error::new(
crate::ErrorClass::UnsupportedVersion,
format!("external-context origin code {other} is not supported"),
)),
}
}
pub fn parse(s: &str) -> Result<Self> {
match s {
"harness" => Ok(ExternalOrigin::Harness),
"operator" => Ok(ExternalOrigin::Operator),
"catalog" => Ok(ExternalOrigin::Catalog),
other => Err(Error::usage(format!(
"--origin expects harness|operator|catalog, got {other:?}"
))),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ExternalLineage {
pub family: Option<String>,
pub member: Option<String>,
pub head: bool,
pub revision_family: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExternalContext {
pub dataset_id: Option<String>,
pub lineage: ExternalLineage,
pub origin: ExternalOrigin,
pub source: String,
}
fn put_opt_str(out: &mut Vec<u8>, s: Option<&str>) {
match s {
Some(v) => {
out.push(1);
out.extend_from_slice(&(v.len() as u32).to_le_bytes());
out.extend_from_slice(v.as_bytes());
}
None => out.push(0),
}
}
fn get_opt_str(bytes: &[u8], at: &mut usize) -> Result<Option<String>> {
let present = *bytes
.get(*at)
.ok_or_else(|| Error::usage("truncated external-context record"))?;
*at += 1;
if present == 0 {
return Ok(None);
}
let len_bytes = bytes
.get(*at..*at + 4)
.ok_or_else(|| Error::usage("truncated external-context record"))?;
let len = u32::from_le_bytes(len_bytes.try_into().unwrap()) as usize;
*at += 4;
let end = at
.checked_add(len)
.ok_or_else(|| Error::usage("external-context string overflow"))?;
let slice = bytes
.get(*at..end)
.ok_or_else(|| Error::usage("truncated external-context string"))?;
let s = core::str::from_utf8(slice)
.map_err(|_| Error::usage("external-context string is not UTF-8"))?
.to_string();
*at = end;
Ok(Some(s))
}
fn opt_json(v: Option<&str>) -> String {
match v {
Some(s) => format!("\"{}\"", json_escape(s)),
None => "null".to_string(),
}
}
impl ExternalContext {
pub fn encode_canonical(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(128);
out.extend_from_slice(EXTERNAL_MAGIC);
out.push(EXTERNAL_FORMAT_VERSION);
out.push(self.origin.code());
put_opt_str(&mut out, self.dataset_id.as_deref());
put_opt_str(&mut out, self.lineage.family.as_deref());
put_opt_str(&mut out, self.lineage.member.as_deref());
out.push(u8::from(self.lineage.head));
put_opt_str(&mut out, self.lineage.revision_family.as_deref());
put_opt_str(&mut out, Some(self.source.as_str()));
out
}
pub fn decode_canonical(bytes: &[u8]) -> Result<ExternalContext> {
if bytes.len() < 10 || &bytes[0..8] != EXTERNAL_MAGIC {
return Err(Error::unsupported_version(
"external-context record: bad magic",
));
}
if bytes[8] != EXTERNAL_FORMAT_VERSION {
return Err(Error::unsupported_version(format!(
"external-context record version {} is not supported",
bytes[8]
)));
}
let origin = ExternalOrigin::from_code(bytes[9])?;
let mut at = 10;
let dataset_id = get_opt_str(bytes, &mut at)?;
let family = get_opt_str(bytes, &mut at)?;
let member = get_opt_str(bytes, &mut at)?;
let head = match bytes.get(at) {
Some(0) => false,
Some(1) => true,
Some(_) => return Err(Error::usage("external-context head flag is not boolean")),
None => return Err(Error::usage("truncated external-context record")),
};
at += 1;
let revision_family = get_opt_str(bytes, &mut at)?;
let source = get_opt_str(bytes, &mut at)?.unwrap_or_default();
if at != bytes.len() {
return Err(Error::usage("external-context record has trailing bytes"));
}
Ok(ExternalContext {
dataset_id,
lineage: ExternalLineage {
family,
member,
head,
revision_family,
},
origin,
source,
})
}
pub fn answer_json(&self) -> String {
format!(
concat!(
"{{",
"\"family_id\":{},",
"\"member_id\":{},",
"\"is_head\":{},",
"\"dataset_id\":{},",
"\"revision_family\":{},",
"\"origin\":\"{}\",",
"\"source\":\"{}\"",
"}}"
),
opt_json(self.lineage.family.as_deref()),
opt_json(self.lineage.member.as_deref()),
self.lineage.head,
opt_json(self.dataset_id.as_deref()),
opt_json(self.lineage.revision_family.as_deref()),
self.origin.name(),
json_escape(&self.source),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> ExternalContext {
ExternalContext {
dataset_id: Some("real100-v1".to_string()),
lineage: ExternalLineage {
family: Some("rev-nist-fips-140".to_string()),
member: Some("nist-pdf-0017".to_string()),
head: true,
revision_family: None,
},
origin: ExternalOrigin::Harness,
source: "real100-v1/manifest.tsv".to_string(),
}
}
#[test]
fn round_trips_canonically() {
let ctx = sample();
let bytes = ctx.encode_canonical();
assert_eq!(ExternalContext::decode_canonical(&bytes).unwrap(), ctx);
assert_eq!(ctx.encode_canonical(), bytes);
}
#[test]
fn round_trips_with_absent_fields() {
let ctx = ExternalContext {
dataset_id: None,
lineage: ExternalLineage::default(),
origin: ExternalOrigin::Operator,
source: String::new(),
};
let bytes = ctx.encode_canonical();
assert_eq!(ExternalContext::decode_canonical(&bytes).unwrap(), ctx);
}
#[test]
fn rejects_bad_magic_version_and_trailing_bytes() {
let mut bytes = sample().encode_canonical();
bytes[0] ^= 0xff;
assert_eq!(
ExternalContext::decode_canonical(&bytes)
.unwrap_err()
.class(),
crate::ErrorClass::UnsupportedVersion
);
let mut bytes = sample().encode_canonical();
bytes[8] = 9;
assert_eq!(
ExternalContext::decode_canonical(&bytes)
.unwrap_err()
.class(),
crate::ErrorClass::UnsupportedVersion
);
let mut bytes = sample().encode_canonical();
bytes.push(0);
assert_eq!(
ExternalContext::decode_canonical(&bytes)
.unwrap_err()
.class(),
crate::ErrorClass::Usage
);
assert!(ExternalContext::decode_canonical(b"short").is_err());
}
#[test]
fn origin_parses_typed_and_rejects_unknown() {
assert_eq!(ExternalOrigin::parse("harness").unwrap().name(), "harness");
assert_eq!(
ExternalOrigin::parse("operator").unwrap().name(),
"operator"
);
assert_eq!(ExternalOrigin::parse("catalog").unwrap().name(), "catalog");
assert_eq!(
ExternalOrigin::parse("guess").unwrap_err().class(),
crate::ErrorClass::Usage
);
}
#[test]
fn answer_json_names_the_c4b_tuple() {
let json = sample().answer_json();
assert!(
json.contains("\"family_id\":\"rev-nist-fips-140\""),
"{json}"
);
assert!(json.contains("\"member_id\":\"nist-pdf-0017\""), "{json}");
assert!(json.contains("\"is_head\":true"), "{json}");
assert!(json.contains("\"origin\":\"harness\""), "{json}");
}
}