use serde::{Deserialize, Serialize};
pub const MANIFEST_SECTION_NAME: &str = "freenet-manifest";
pub const MANIFEST_VERSION: u16 = 1;
pub const MAX_MANIFEST_BYTES: usize = 4096;
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct DelegateManifest {
pub manifest_version: u16,
#[serde(default, deserialize_with = "lenient_list")]
pub lifecycle: Vec<LifecycleKind>,
#[serde(default, deserialize_with = "lenient_list")]
pub capabilities: Vec<Capability>,
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum LifecycleKind {
Installed,
NodeStarted,
#[serde(other)]
Unknown,
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
Background,
#[serde(other)]
Unknown,
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum LifecycleEvent {
Installed,
NodeStarted {
down_since_ms: Option<u64>,
},
}
impl LifecycleEvent {
pub fn kind(&self) -> LifecycleKind {
match self {
LifecycleEvent::Installed => LifecycleKind::Installed,
LifecycleEvent::NodeStarted { .. } => LifecycleKind::NodeStarted,
}
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ManifestError {
#[error("not a WASM module (bad magic or version)")]
NotWasm,
#[error("truncated or malformed WASM section structure")]
Malformed,
#[error("more than one `{MANIFEST_SECTION_NAME}` custom section")]
Duplicate,
#[error("manifest is {0} bytes, over the {MAX_MANIFEST_BYTES}-byte limit")]
TooLarge(usize),
#[error("manifest is not valid JSON for this schema: {0}")]
Decode(String),
#[error("manifest_version 0 is not a valid version")]
BadVersion,
}
impl DelegateManifest {
pub fn new(lifecycle: Vec<LifecycleKind>, capabilities: Vec<Capability>) -> Self {
Self {
manifest_version: MANIFEST_VERSION,
lifecycle,
capabilities,
}
}
pub fn wants_lifecycle(&self, kind: LifecycleKind) -> bool {
kind != LifecycleKind::Unknown && self.lifecycle.contains(&kind)
}
pub fn wants_capability(&self, cap: Capability) -> bool {
cap != Capability::Unknown && self.capabilities.contains(&cap)
}
pub fn known_capabilities(&self) -> Vec<Capability> {
let mut out = Vec::new();
for c in &self.capabilities {
if *c != Capability::Unknown && !out.contains(c) {
out.push(*c);
}
}
out
}
pub fn to_bytes(&self) -> Vec<u8> {
serde_json::to_vec(self).expect("a manifest always serializes")
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ManifestError> {
if bytes.len() > MAX_MANIFEST_BYTES {
return Err(ManifestError::TooLarge(bytes.len()));
}
let m: DelegateManifest =
serde_json::from_slice(bytes).map_err(|e| ManifestError::Decode(e.to_string()))?;
if m.manifest_version == 0 {
return Err(ManifestError::BadVersion);
}
Ok(m)
}
pub fn from_wasm(module: &[u8]) -> Result<Option<Self>, ManifestError> {
let mut found: Option<&[u8]> = None;
for section in custom_sections(module)? {
let (name, payload) = section?;
if name == MANIFEST_SECTION_NAME.as_bytes() {
if found.is_some() {
return Err(ManifestError::Duplicate);
}
found = Some(payload);
}
}
found.map(Self::from_bytes).transpose()
}
}
fn lenient_list<'de, D, T>(d: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: serde::de::DeserializeOwned + Unknownable,
{
let serde_json::Value::Array(raw) = serde_json::Value::deserialize(d)? else {
return Ok(Vec::new());
};
Ok(raw
.into_iter()
.map(|v| serde_json::from_value(v).unwrap_or_else(|_| T::unknown()))
.collect())
}
trait Unknownable {
fn unknown() -> Self;
}
impl Unknownable for LifecycleKind {
fn unknown() -> Self {
LifecycleKind::Unknown
}
}
impl Unknownable for Capability {
fn unknown() -> Self {
Capability::Unknown
}
}
#[doc(hidden)]
pub const fn __manifest_macro_agrees(section: &str, version: u16) -> bool {
let (a, b) = (section.as_bytes(), MANIFEST_SECTION_NAME.as_bytes());
if a.len() != b.len() || version != MANIFEST_VERSION {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
type CustomSection<'a> = (&'a [u8], &'a [u8]);
fn custom_sections(
module: &[u8],
) -> Result<impl Iterator<Item = Result<CustomSection<'_>, ManifestError>>, ManifestError> {
const HEADER: [u8; 8] = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
if module.len() < HEADER.len() || module[..HEADER.len()] != HEADER {
return Err(ManifestError::NotWasm);
}
let mut pos = HEADER.len();
let mut failed = false;
Ok(std::iter::from_fn(move || loop {
if failed || pos >= module.len() {
return None;
}
let parsed = (|| {
let id = module[pos];
let mut p = pos + 1;
let size = read_leb_u32(module, &mut p)? as usize;
let end = p.checked_add(size).ok_or(ManifestError::Malformed)?;
if end > module.len() {
return Err(ManifestError::Malformed);
}
let custom = if id == 0 {
let name_len = read_leb_u32(module, &mut p)? as usize;
let name_end = p.checked_add(name_len).ok_or(ManifestError::Malformed)?;
if name_end > end {
return Err(ManifestError::Malformed);
}
Some((&module[p..name_end], &module[name_end..end]))
} else {
None
};
Ok((end, custom))
})();
match parsed {
Ok((end, custom)) => {
pos = end;
if let Some(c) = custom {
return Some(Ok(c));
}
}
Err(e) => {
failed = true;
return Some(Err(e));
}
}
}))
}
fn read_leb_u32(buf: &[u8], pos: &mut usize) -> Result<u32, ManifestError> {
let mut result: u32 = 0;
for i in 0..5 {
let byte = *buf.get(*pos).ok_or(ManifestError::Malformed)?;
*pos += 1;
if i == 4 && byte & 0xf0 != 0 {
return Err(ManifestError::Malformed);
}
result |= u32::from(byte & 0x7f) << (7 * i);
if byte & 0x80 == 0 {
return Ok(result);
}
}
Err(ManifestError::Malformed)
}
#[cfg(test)]
mod tests {
use super::*;
fn leb(mut v: u32) -> Vec<u8> {
let mut out = Vec::new();
loop {
let mut b = (v & 0x7f) as u8;
v >>= 7;
if v != 0 {
b |= 0x80;
}
out.push(b);
if v == 0 {
return out;
}
}
}
fn custom_section(name: &str, payload: &[u8]) -> Vec<u8> {
let mut body = leb(name.len() as u32);
body.extend_from_slice(name.as_bytes());
body.extend_from_slice(payload);
let mut out = vec![0u8];
out.extend(leb(body.len() as u32));
out.extend(body);
out
}
fn module(sections: &[Vec<u8>]) -> Vec<u8> {
let mut m = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
m.extend([0x01, 0x01, 0x00]);
for s in sections {
m.extend_from_slice(s);
}
m
}
fn sample() -> DelegateManifest {
DelegateManifest::new(
vec![LifecycleKind::Installed, LifecycleKind::NodeStarted],
vec![Capability::Background],
)
}
#[test]
fn round_trips_through_a_wasm_custom_section() {
let m = module(&[
custom_section("name", b"whatever"),
custom_section(MANIFEST_SECTION_NAME, &sample().to_bytes()),
]);
assert_eq!(DelegateManifest::from_wasm(&m).unwrap(), Some(sample()));
}
#[test]
fn a_module_without_the_section_has_no_manifest() {
let m = module(&[custom_section("producers", b"rustc")]);
assert_eq!(DelegateManifest::from_wasm(&m).unwrap(), None);
}
#[test]
fn json_shape_is_pinned() {
assert_eq!(
String::from_utf8(sample().to_bytes()).unwrap(),
r#"{"manifest_version":1,"lifecycle":["installed","node_started"],"capabilities":["background"]}"#
);
}
#[test]
fn a_newer_manifest_is_read_keeping_known_entries() {
let json = br#"{"manifest_version":3,"lifecycle":["installed","woke_up"],
"capabilities":["background","teleport"],"brand_new_field":{"x":1}}"#;
let m = DelegateManifest::from_bytes(json).unwrap();
assert!(m.wants_lifecycle(LifecycleKind::Installed));
assert!(!m.wants_lifecycle(LifecycleKind::NodeStarted));
assert!(!m.wants_lifecycle(LifecycleKind::Unknown));
assert!(!m.wants_capability(Capability::Unknown));
assert_eq!(m.known_capabilities(), vec![Capability::Background]);
}
#[test]
fn missing_lists_default_to_empty() {
let m = DelegateManifest::from_bytes(br#"{"manifest_version":1}"#).unwrap();
assert!(m.lifecycle.is_empty() && m.capabilities.is_empty());
let m = DelegateManifest::from_bytes(
br#"{"manifest_version":1,"lifecycle":null,"capabilities":null}"#,
)
.unwrap();
assert!(m.lifecycle.is_empty() && m.capabilities.is_empty());
let m = DelegateManifest::from_bytes(
br#"{"manifest_version":1,"lifecycle":"installed","capabilities":{"background":{}}}"#,
)
.unwrap();
assert!(m.lifecycle.is_empty() && m.capabilities.is_empty());
}
#[test]
fn a_non_string_entry_reads_as_unknown_not_as_an_error() {
let json = br#"{"manifest_version":2,
"lifecycle":[{"woke_up":{"every_s":60}},"node_started",7],
"capabilities":[{"notify":{"max_per_hour":4}},"background",null]}"#;
let m = DelegateManifest::from_bytes(json).unwrap();
assert_eq!(
m.lifecycle,
vec![
LifecycleKind::Unknown,
LifecycleKind::NodeStarted,
LifecycleKind::Unknown
]
);
assert_eq!(m.known_capabilities(), vec![Capability::Background]);
}
#[test]
fn macro_agreement_check() {
assert!(__manifest_macro_agrees(
MANIFEST_SECTION_NAME,
MANIFEST_VERSION
));
assert!(!__manifest_macro_agrees(
"freenet-manifesT",
MANIFEST_VERSION
));
assert!(!__manifest_macro_agrees(
"freenet-manifest2",
MANIFEST_VERSION
));
assert!(!__manifest_macro_agrees(
MANIFEST_SECTION_NAME,
MANIFEST_VERSION + 1
));
}
#[test]
fn rejects_version_zero_oversize_and_garbage() {
assert_eq!(
DelegateManifest::from_bytes(br#"{"manifest_version":0}"#),
Err(ManifestError::BadVersion)
);
let big = vec![b' '; MAX_MANIFEST_BYTES + 1];
assert_eq!(
DelegateManifest::from_bytes(&big),
Err(ManifestError::TooLarge(MAX_MANIFEST_BYTES + 1))
);
assert!(matches!(
DelegateManifest::from_bytes(b"not json"),
Err(ManifestError::Decode(_))
));
}
#[test]
fn rejects_a_duplicate_section() {
let payload = sample().to_bytes();
let m = module(&[
custom_section(MANIFEST_SECTION_NAME, &payload),
custom_section(MANIFEST_SECTION_NAME, &payload),
]);
assert_eq!(
DelegateManifest::from_wasm(&m),
Err(ManifestError::Duplicate)
);
}
#[test]
fn concatenated_manifests_fail_to_decode() {
let mut payload = sample().to_bytes();
payload.extend(sample().to_bytes());
let m = module(&[custom_section(MANIFEST_SECTION_NAME, &payload)]);
assert!(matches!(
DelegateManifest::from_wasm(&m),
Err(ManifestError::Decode(_))
));
}
#[test]
fn rejects_non_wasm_and_truncated_modules() {
assert_eq!(
DelegateManifest::from_wasm(b"\0asm"),
Err(ManifestError::NotWasm)
);
assert_eq!(
DelegateManifest::from_wasm(b"hello world, not wasm"),
Err(ManifestError::NotWasm)
);
let mut m = module(&[custom_section(MANIFEST_SECTION_NAME, &sample().to_bytes())]);
m.truncate(m.len() - 3);
assert_eq!(
DelegateManifest::from_wasm(&m),
Err(ManifestError::Malformed)
);
let mut m = module(&[]);
m.extend([0x00, 0xff, 0xff, 0x03]);
assert_eq!(
DelegateManifest::from_wasm(&m),
Err(ManifestError::Malformed)
);
let mut m = module(&[]);
m.extend([0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00]);
assert_eq!(
DelegateManifest::from_wasm(&m),
Err(ManifestError::Malformed)
);
}
#[test]
fn lifecycle_event_tags_are_pinned() {
fn tag(e: &LifecycleEvent) -> u32 {
match e {
LifecycleEvent::Installed => 0,
LifecycleEvent::NodeStarted { .. } => 1,
}
}
let all = [
LifecycleEvent::Installed,
LifecycleEvent::NodeStarted {
down_since_ms: Some(0x0102_0304_0506_0708),
},
];
for e in &all {
let enc = bincode::serialize(e).unwrap();
assert_eq!(u32::from_le_bytes(enc[..4].try_into().unwrap()), tag(e));
assert_eq!(&bincode::deserialize::<LifecycleEvent>(&enc).unwrap(), e);
}
assert_eq!(
bincode::serialize(&all[1]).unwrap(),
vec![1, 0, 0, 0, 1, 8, 7, 6, 5, 4, 3, 2, 1]
);
assert_eq!(all[0].kind(), LifecycleKind::Installed);
assert_eq!(all[1].kind(), LifecycleKind::NodeStarted);
}
}