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;
pub const MIN_WAKEUP_INTERVAL_SECS: u64 = 60;
pub const MAX_WAKEUP_INTERVAL_SECS: u64 = 7 * 24 * 3600;
pub const MAX_WAKEUP_TAG_BYTES: usize = 64;
pub const MAX_WAKEUPS: usize = 4;
#[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>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "lenient_wakeups"
)]
pub wakeups: Vec<WakeupSchedule>,
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct WakeupSchedule {
pub tag: String,
pub every_secs: u64,
}
impl WakeupSchedule {
pub fn new(tag: impl Into<String>, every_secs: u64) -> Self {
Self {
tag: tag.into(),
every_secs,
}
}
}
#[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,
wakeups: Vec::new(),
}
}
pub fn with_wakeup(mut self, tag: impl Into<String>, every_secs: u64) -> Self {
self.wakeups.push(WakeupSchedule::new(tag, every_secs));
self
}
pub fn effective_wakeups(&self) -> Vec<(Vec<u8>, std::time::Duration)> {
let mut out: Vec<(Vec<u8>, std::time::Duration)> = Vec::new();
for w in &self.wakeups {
if out.len() >= MAX_WAKEUPS {
break;
}
let tag = w.tag.as_bytes();
if tag.is_empty() || tag.len() > MAX_WAKEUP_TAG_BYTES {
continue;
}
if out.iter().any(|(t, _)| t.as_slice() == tag) {
continue;
}
let secs = w
.every_secs
.clamp(MIN_WAKEUP_INTERVAL_SECS, MAX_WAKEUP_INTERVAL_SECS);
out.push((tag.to_vec(), std::time::Duration::from_secs(secs)));
}
out
}
pub fn wants_wakeups(&self) -> bool {
!self.effective_wakeups().is_empty()
}
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())
}
fn lenient_wakeups<'de, D>(d: D) -> Result<Vec<WakeupSchedule>, D::Error>
where
D: serde::Deserializer<'de>,
{
let serde_json::Value::Array(raw) = serde_json::Value::deserialize(d)? else {
return Ok(Vec::new());
};
Ok(raw
.into_iter()
.filter_map(|v| serde_json::from_value(v).ok())
.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 json_shape_with_wakeups_is_pinned() {
let m = sample().with_wakeup("heartbeat", 300);
assert_eq!(
String::from_utf8(m.to_bytes()).unwrap(),
r#"{"manifest_version":1,"lifecycle":["installed","node_started"],"capabilities":["background"],"wakeups":[{"tag":"heartbeat","every_secs":300}]}"#
);
assert_eq!(DelegateManifest::from_bytes(&m.to_bytes()).unwrap(), m);
}
#[test]
fn effective_wakeups_clamp_and_filter_at_the_boundaries() {
let secs = |m: &DelegateManifest| {
m.effective_wakeups()
.into_iter()
.map(|(t, d)| (String::from_utf8(t).unwrap(), d.as_secs()))
.collect::<Vec<_>>()
};
let one = |every: u64| secs(&sample().with_wakeup("t", every));
assert_eq!(one(0), vec![("t".into(), MIN_WAKEUP_INTERVAL_SECS)]);
assert_eq!(
one(MIN_WAKEUP_INTERVAL_SECS - 1),
vec![("t".into(), MIN_WAKEUP_INTERVAL_SECS)]
);
assert_eq!(
one(MIN_WAKEUP_INTERVAL_SECS),
vec![("t".into(), MIN_WAKEUP_INTERVAL_SECS)]
);
assert_eq!(
one(MIN_WAKEUP_INTERVAL_SECS + 1),
vec![("t".into(), MIN_WAKEUP_INTERVAL_SECS + 1)]
);
assert_eq!(
one(MAX_WAKEUP_INTERVAL_SECS),
vec![("t".into(), MAX_WAKEUP_INTERVAL_SECS)]
);
assert_eq!(
one(MAX_WAKEUP_INTERVAL_SECS + 1),
vec![("t".into(), MAX_WAKEUP_INTERVAL_SECS)]
);
assert_eq!(one(u64::MAX), vec![("t".into(), MAX_WAKEUP_INTERVAL_SECS)]);
let max_tag = "x".repeat(MAX_WAKEUP_TAG_BYTES);
let long_tag = "x".repeat(MAX_WAKEUP_TAG_BYTES + 1);
let m = sample()
.with_wakeup("", 120)
.with_wakeup(long_tag, 120)
.with_wakeup(max_tag.clone(), 120);
assert_eq!(secs(&m), vec![(max_tag, 120)]);
let utf8_max = "é".repeat(MAX_WAKEUP_TAG_BYTES / 2);
assert_eq!(utf8_max.len(), MAX_WAKEUP_TAG_BYTES);
let utf8_over = format!("{utf8_max}a");
let m = sample()
.with_wakeup(utf8_over, 120)
.with_wakeup(utf8_max.clone(), 120);
assert_eq!(secs(&m), vec![(utf8_max, 120)]);
let m = sample().with_wakeup("a", 120).with_wakeup("a", 600);
assert_eq!(secs(&m), vec![("a".into(), 120)]);
let mut m = sample().with_wakeup("", 60);
for i in 0..MAX_WAKEUPS + 1 {
m = m.with_wakeup(format!("w{i}"), 60);
}
let got = secs(&m);
assert_eq!(got.len(), MAX_WAKEUPS);
assert_eq!(got[0].0, "w0");
assert_eq!(got[MAX_WAKEUPS - 1].0, format!("w{}", MAX_WAKEUPS - 1));
assert!(!sample().wants_wakeups());
assert!(!sample().with_wakeup("", 60).wants_wakeups());
assert!(sample().with_wakeup("a", 60).wants_wakeups());
}
#[test]
fn a_malformed_wakeup_entry_is_dropped_not_fatal() {
let json = br#"{"manifest_version":2,"lifecycle":["node_started"],
"capabilities":["background"],
"wakeups":[{"tag":"ok","every_secs":90},{"tag":7},"junk",{"cron":"* * *"},
{"tag":"extra","every_secs":120,"jitter":5}]}"#;
let m = DelegateManifest::from_bytes(json).unwrap();
assert!(m.wants_lifecycle(LifecycleKind::NodeStarted));
assert_eq!(m.known_capabilities(), vec![Capability::Background]);
assert_eq!(
m.wakeups,
vec![
WakeupSchedule::new("ok", 90),
WakeupSchedule::new("extra", 120)
]
);
for shape in [r#"null"#, r#""heartbeat""#, r#"{"heartbeat":300}"#] {
let json = format!(r#"{{"manifest_version":1,"wakeups":{shape}}}"#);
let m = DelegateManifest::from_bytes(json.as_bytes()).unwrap();
assert!(m.wakeups.is_empty(), "{shape}");
}
}
#[test]
fn a_reader_without_the_wakeups_field_still_reads_the_manifest() {
#[derive(Deserialize)]
struct ReaderV0120 {
manifest_version: u16,
#[serde(default, deserialize_with = "lenient_list")]
lifecycle: Vec<LifecycleKind>,
#[serde(default, deserialize_with = "lenient_list")]
capabilities: Vec<Capability>,
}
let m = sample().with_wakeup("heartbeat", 300);
let old: ReaderV0120 = serde_json::from_slice(&m.to_bytes()).unwrap();
assert_eq!(old.manifest_version, 1);
assert_eq!(old.lifecycle, m.lifecycle);
assert_eq!(old.capabilities, m.capabilities);
}
#[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);
}
}