freenet_stdlib/delegate_manifest.rs
1//! The delegate manifest: what a delegate asks the node for, declared inside
2//! its own WASM.
3//!
4//! A delegate that wants more than the request/response model it has always
5//! had (lifecycle events, periodic wake-ups; later perhaps notifications) says
6//! so in a manifest. The `#[delegate(manifest(...))]` attribute writes it into a WASM
7//! custom section named [`MANIFEST_SECTION_NAME`]. The node reads that section
8//! when the delegate is registered, without running any delegate code.
9//!
10//! # Why a manifest at all
11//!
12//! Two jobs, both of which need the node to know what a delegate wants
13//! *before* it runs:
14//!
15//! 1. **Backward compatibility of new inbound messages.** An
16//! [`InboundDelegateMsg`](crate::prelude::InboundDelegateMsg) variant added
17//! in a later stdlib is a hard decode error in a delegate built against an
18//! earlier one (see `WIRE-FORMAT.md`). So the node sends a new kind of
19//! inbound message — [`LifecycleEvent`] today — **only** to a delegate
20//! whose manifest lists it. A delegate without a manifest never receives
21//! one, and behaves exactly as it did before manifests existed.
22//! 2. **Consent.** Some capabilities are gated by the node on the user's
23//! permission ("run in the background"). The node reads the capabilities
24//! from the manifest and asks the user once, at registration time, for the
25//! ones not yet granted.
26//!
27//! # Why it cannot be swapped
28//!
29//! The section is part of the WASM module, and a delegate's key is derived
30//! from the hash of that module. Changing the manifest changes the delegate
31//! key, exactly as changing any line of code does.
32//!
33//! # Encoding: JSON, deliberately
34//!
35//! The payload is UTF-8 JSON, not bincode. The manifest is the one piece of
36//! delegate metadata the node must read from delegates built against *any*
37//! stdlib, including ones newer than the node. bincode is positional and
38//! cannot skip what it does not know (`WIRE-FORMAT.md`), so a newer stdlib
39//! adding a field or a capability would make older nodes reject the whole
40//! manifest. JSON names its fields, so an older reader ignores fields it does
41//! not know, and unknown capability or lifecycle names decode as
42//! [`Capability::Unknown`] / [`LifecycleKind::Unknown`] and are ignored rather
43//! than rejecting the manifest. (`#[serde(other)]` is safe here because JSON
44//! is self-describing; `WIRE-FORMAT.md`'s warning against it is about
45//! bincode.)
46//!
47//! # Emitting a manifest adds a section, and so changes the delegate key
48//!
49//! Only delegates that write `manifest(...)` get the section. Upgrading stdlib
50//! does not add one to delegates that do not ask for it.
51
52use serde::{Deserialize, Serialize};
53
54/// Name of the WASM custom section holding the manifest.
55pub const MANIFEST_SECTION_NAME: &str = "freenet-manifest";
56
57/// The manifest format version this stdlib writes.
58///
59/// Informational only. Readers accept any version `>= 1` and never gate on it,
60/// because a reader that refused newer versions would drop every capability it
61/// does understand the moment one it does not is added. That works only under
62/// two rules, which are permanent:
63///
64/// - the meaning of an existing field or name never changes; a changed meaning
65/// gets a new field or a new name;
66/// - `lifecycle` and `capabilities` entries are what a reader looks up by
67/// name. A later format that needs parameters for a capability adds a new
68/// top-level field for them. (A reader still tolerates a non-string entry:
69/// it decodes as `Unknown`, see [`DelegateManifest::from_bytes`].)
70pub const MANIFEST_VERSION: u16 = 1;
71
72/// Largest manifest payload a reader accepts, in bytes. A manifest is a
73/// handful of short names; anything bigger is not a manifest.
74pub const MAX_MANIFEST_BYTES: usize = 4096;
75
76/// Shortest wake-up interval a node honours, in seconds. A manifest asking for
77/// less is treated as asking for this (see [`DelegateManifest::effective_wakeups`]).
78///
79/// The floor is what stops a delegate waking itself into a storm: a wake-up is
80/// work nothing outside the node asked for, on an idle node, forever.
81pub const MIN_WAKEUP_INTERVAL_SECS: u64 = 60;
82
83/// Longest wake-up interval, in seconds (7 days). A longer one is treated as
84/// this. Wake-ups are re-armed when a node starts rather than remembered across
85/// restarts, so a very long interval would mostly measure node uptime anyway.
86pub const MAX_WAKEUP_INTERVAL_SECS: u64 = 7 * 24 * 3600;
87
88/// Longest wake-up tag, in bytes. Entries with a longer (or empty) tag are
89/// ignored.
90pub const MAX_WAKEUP_TAG_BYTES: usize = 64;
91
92/// Most wake-up schedules one delegate may declare. Entries past this are
93/// ignored.
94pub const MAX_WAKEUPS: usize = 4;
95
96/// What a delegate declares it wants from the node.
97///
98/// `#[non_exhaustive]` so fields can be added without a source break; build one
99/// with [`DelegateManifest::new`].
100#[non_exhaustive]
101#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
102pub struct DelegateManifest {
103 /// Format version; see [`MANIFEST_VERSION`].
104 pub manifest_version: u16,
105 /// Lifecycle events the delegate wants delivered. The node never sends a
106 /// [`LifecycleEvent`] of a kind that is not listed here.
107 #[serde(default, deserialize_with = "lenient_list")]
108 pub lifecycle: Vec<LifecycleKind>,
109 /// Node-enforced capabilities the delegate asks the user for.
110 #[serde(default, deserialize_with = "lenient_list")]
111 pub capabilities: Vec<Capability>,
112 /// Periodic wake-ups the delegate asks for; see [`WakeupSchedule`].
113 ///
114 /// Added after the first manifest release (stdlib 0.12.1). Omitted from
115 /// the JSON when empty, so the manifest SECTION of a delegate without
116 /// wake-ups is byte-identical to what 0.12.0 wrote. (Rebuilding a delegate
117 /// against a different stdlib still changes its WASM, and so its key, for
118 /// the usual reasons, e.g. version strings in panic locations; plan a
119 /// migration as for any rebuild.) A reader that predates the field ignores
120 /// it (unknown JSON fields are skipped), so a delegate declaring wake-ups
121 /// still loads, and still gets its lifecycle events, on a node that cannot
122 /// deliver them.
123 ///
124 /// Such a reader also DROPS the field if it re-serializes the manifest
125 /// ([`DelegateManifest::to_bytes`] writes only the fields it knows). A
126 /// node that keeps a re-serialized copy must re-read the manifest from the
127 /// delegate's code after it learns a new field, or it will not see what
128 /// delegates registered under the older version declared. And a node that
129 /// predates wake-ups asks for `Background` only when a lifecycle kind is
130 /// listed: declare one alongside `wakeups` (e.g. `NodeStarted`) so the user
131 /// is asked, and the delegate recorded, on those nodes too.
132 #[serde(
133 default,
134 skip_serializing_if = "Vec::is_empty",
135 deserialize_with = "lenient_wakeups"
136 )]
137 pub wakeups: Vec<WakeupSchedule>,
138}
139
140/// One periodic wake-up a delegate asks for.
141///
142/// The node delivers
143/// [`InboundDelegateMsg::WakeupFired`](crate::prelude::InboundDelegateMsg::WakeupFired)
144/// with `tag`'s bytes about every `every_secs` seconds, with no app open,
145/// under the same conditions as lifecycle events: the manifest lists it and
146/// the delegate's app holds the user's [`Capability::Background`] grant. The
147/// run gets the delegate's registered parameters and no origin.
148///
149/// A node honours an interval between [`MIN_WAKEUP_INTERVAL_SECS`] and
150/// [`MAX_WAKEUP_INTERVAL_SECS`] (clamping one outside that range), a tag of 1
151/// to [`MAX_WAKEUP_TAG_BYTES`] bytes, and at most [`MAX_WAKEUPS`] entries; see
152/// [`DelegateManifest::effective_wakeups`], which is the node's reading.
153///
154/// Why a manifest entry and not a call the delegate makes at run time: a new
155/// host import makes the WASM fail to instantiate on every node that does not
156/// provide it, and a new `OutboundDelegateMsg` variant makes older nodes fail
157/// to decode the whole batch it is in. A manifest field is ignored by older
158/// nodes, so ONE delegate build works on nodes with and without wake-ups.
159///
160/// Timing is the node's business; freenet-core arms a schedule when the
161/// delegate is registered, when its app is granted `Background`, and at each
162/// node start (the first fire comes within about a minute), then fires every
163/// `every_secs` plus a little jitter. A fire that cannot start (delegate busy,
164/// its time budget spent) is retried for up to 45 s and otherwise skipped.
165/// Missed fires (node down, skipped) are not replayed: the next one simply
166/// comes on schedule.
167///
168/// # Rules for this struct's fields (permanent)
169///
170/// Readers ignore fields they do not know, INCLUDING inside an entry. So:
171///
172/// - a new field must be advisory: a reader that ignores it must still do
173/// something acceptable. A field that RESTRICTS the schedule (a quiet
174/// window, a cap) would be silently ignored by older nodes; put such a
175/// thing in a new top-level manifest field instead, whose absence older
176/// readers cannot misread as consent;
177/// - a new field must carry `#[serde(default)]`, or every entry written
178/// without it (every existing delegate) fails to decode and is dropped;
179/// - `tag` and `every_secs` keep their meaning forever.
180#[non_exhaustive]
181#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
182pub struct WakeupSchedule {
183 /// Echoed back as the `tag` of `WakeupFired`, so a delegate with several
184 /// schedules can tell them apart.
185 pub tag: String,
186 /// Interval between wake-ups, in seconds.
187 pub every_secs: u64,
188}
189
190impl WakeupSchedule {
191 pub fn new(tag: impl Into<String>, every_secs: u64) -> Self {
192 Self {
193 tag: tag.into(),
194 every_secs,
195 }
196 }
197}
198
199/// A kind of [`LifecycleEvent`] a delegate can ask to receive.
200///
201/// Adding a kind later means adding a variant here **and** a matching
202/// [`LifecycleEvent`] variant. A delegate built before the addition cannot
203/// list the new kind, so it never receives the new event — which is what makes
204/// appending a `LifecycleEvent` variant safe for already-deployed delegates.
205#[non_exhaustive]
206#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
207#[serde(rename_all = "snake_case")]
208pub enum LifecycleKind {
209 /// [`LifecycleEvent::Installed`].
210 Installed,
211 /// [`LifecycleEvent::NodeStarted`].
212 NodeStarted,
213 /// A name this stdlib does not know, written by a newer one. Readers
214 /// ignore it. The macro never writes it; re-serializing a manifest read
215 /// from a newer stdlib does (as `"unknown"`).
216 #[serde(other)]
217 Unknown,
218}
219
220/// A node-enforced capability, granted by the user once per app.
221///
222/// A node that implements capabilities refuses one until the user has granted
223/// it, and remembers the answer per app, so the user is asked once. How a node
224/// identifies an app is the node's business, not part of this format.
225#[non_exhaustive]
226#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
227#[serde(rename_all = "snake_case")]
228pub enum Capability {
229 /// Run without an open app: receive lifecycle events. Required by any
230 /// manifest that lists a lifecycle kind (the macro enforces it).
231 Background,
232 /// A name this stdlib does not know, written by a newer one. Readers
233 /// ignore it. The macro never writes it; re-serializing a manifest read
234 /// from a newer stdlib does (as `"unknown"`).
235 #[serde(other)]
236 Unknown,
237}
238
239/// A lifecycle event, delivered as
240/// [`InboundDelegateMsg::Lifecycle`](crate::prelude::InboundDelegateMsg::Lifecycle).
241///
242/// Only sent to a delegate whose manifest lists the matching
243/// [`LifecycleKind`]. A node that implements delivery also requires the user's
244/// [`Capability::Background`] grant for the delegate's app. The run gets the
245/// delegate's registered parameters and no origin.
246///
247/// # Wire format
248///
249/// bincode, nested inside `InboundDelegateMsg`. Variants are appended, never
250/// inserted or reordered (pinned by `lifecycle_event_tags_are_pinned`). A
251/// variant's fields are frozen once released: `WIRE-FORMAT.md` rule 1 forbids
252/// appending a field to a struct already on the wire, so new information
253/// arrives as a new variant.
254#[non_exhaustive]
255#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
256pub enum LifecycleEvent {
257 /// This delegate was installed on this node: its first registration here,
258 /// or the first time its app was granted [`Capability::Background`] after
259 /// that. Delivered at most once per delegate key per node.
260 ///
261 /// A delegate typically uses it to subscribe to the contracts it watches
262 /// and to do any one-time setup.
263 Installed,
264 /// The node started. Delivered once per node start, after the node has
265 /// finished whatever restore work it does at start-up.
266 ///
267 /// Contract notifications that arrived while the node was down were not
268 /// delivered and are never replayed, so a delegate should re-read any
269 /// contract it depends on rather than assume it saw every change.
270 NodeStarted {
271 /// When the node was last known to be running (milliseconds since the
272 /// Unix epoch), if it knows. Everything between this and now was
273 /// missed. `None` means the node does not know, not that nothing was
274 /// missed.
275 down_since_ms: Option<u64>,
276 },
277}
278
279impl LifecycleEvent {
280 /// The manifest kind that must be listed for this event to be delivered.
281 pub fn kind(&self) -> LifecycleKind {
282 match self {
283 LifecycleEvent::Installed => LifecycleKind::Installed,
284 LifecycleEvent::NodeStarted { .. } => LifecycleKind::NodeStarted,
285 }
286 }
287}
288
289/// Why a manifest could not be read.
290#[derive(Debug, thiserror::Error, PartialEq, Eq)]
291#[non_exhaustive]
292pub enum ManifestError {
293 #[error("not a WASM module (bad magic or version)")]
294 NotWasm,
295 #[error("truncated or malformed WASM section structure")]
296 Malformed,
297 #[error("more than one `{MANIFEST_SECTION_NAME}` custom section")]
298 Duplicate,
299 #[error("manifest is {0} bytes, over the {MAX_MANIFEST_BYTES}-byte limit")]
300 TooLarge(usize),
301 #[error("manifest is not valid JSON for this schema: {0}")]
302 Decode(String),
303 #[error("manifest_version 0 is not a valid version")]
304 BadVersion,
305}
306
307impl DelegateManifest {
308 /// A manifest at the current [`MANIFEST_VERSION`].
309 pub fn new(lifecycle: Vec<LifecycleKind>, capabilities: Vec<Capability>) -> Self {
310 Self {
311 manifest_version: MANIFEST_VERSION,
312 lifecycle,
313 capabilities,
314 wakeups: Vec::new(),
315 }
316 }
317
318 /// This manifest plus a wake-up schedule.
319 pub fn with_wakeup(mut self, tag: impl Into<String>, every_secs: u64) -> Self {
320 self.wakeups.push(WakeupSchedule::new(tag, every_secs));
321 self
322 }
323
324 /// The wake-ups a node honours, as `(tag bytes, interval)`: entries with an
325 /// empty or over-long tag are dropped, a repeated tag keeps its first
326 /// entry, intervals are clamped to
327 /// `[MIN_WAKEUP_INTERVAL_SECS, MAX_WAKEUP_INTERVAL_SECS]`, and only the
328 /// first [`MAX_WAKEUPS`] survivors count.
329 ///
330 /// This is only the manifest's side. It does not check the `Background`
331 /// grant (the node's state, not the manifest's): a node must also require
332 /// that before firing any of these.
333 ///
334 /// Clamped rather than refused: a node that later lowers the floor must
335 /// not make delegates built for it dead on older nodes, and a longer
336 /// interval than asked is the safe direction.
337 pub fn effective_wakeups(&self) -> Vec<(Vec<u8>, std::time::Duration)> {
338 let mut out: Vec<(Vec<u8>, std::time::Duration)> = Vec::new();
339 for w in &self.wakeups {
340 if out.len() >= MAX_WAKEUPS {
341 break;
342 }
343 let tag = w.tag.as_bytes();
344 if tag.is_empty() || tag.len() > MAX_WAKEUP_TAG_BYTES {
345 continue;
346 }
347 if out.iter().any(|(t, _)| t.as_slice() == tag) {
348 continue;
349 }
350 let secs = w
351 .every_secs
352 .clamp(MIN_WAKEUP_INTERVAL_SECS, MAX_WAKEUP_INTERVAL_SECS);
353 out.push((tag.to_vec(), std::time::Duration::from_secs(secs)));
354 }
355 out
356 }
357
358 /// Whether the manifest asks for at least one wake-up a node honours.
359 pub fn wants_wakeups(&self) -> bool {
360 !self.effective_wakeups().is_empty()
361 }
362
363 /// Whether the manifest asks for lifecycle events of this kind.
364 pub fn wants_lifecycle(&self, kind: LifecycleKind) -> bool {
365 kind != LifecycleKind::Unknown && self.lifecycle.contains(&kind)
366 }
367
368 /// Whether the manifest asks for this capability.
369 pub fn wants_capability(&self, cap: Capability) -> bool {
370 cap != Capability::Unknown && self.capabilities.contains(&cap)
371 }
372
373 /// Known capabilities this manifest asks for, deduplicated, in declaration
374 /// order. Unknown names are dropped.
375 pub fn known_capabilities(&self) -> Vec<Capability> {
376 let mut out = Vec::new();
377 for c in &self.capabilities {
378 if *c != Capability::Unknown && !out.contains(c) {
379 out.push(*c);
380 }
381 }
382 out
383 }
384
385 /// Serialize to the section payload.
386 pub fn to_bytes(&self) -> Vec<u8> {
387 serde_json::to_vec(self).expect("a manifest always serializes")
388 }
389
390 /// Parse a section payload.
391 ///
392 /// Tolerant of manifests written by newer stdlibs: unknown fields are
393 /// ignored, a list field that is not an array (`null`, or any other shape)
394 /// reads as empty, and a list entry that is not a
395 /// name this reader knows (an unknown name, or a non-string value) reads as
396 /// `Unknown` rather than failing the manifest, so the known entries next
397 /// to it still count.
398 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ManifestError> {
399 if bytes.len() > MAX_MANIFEST_BYTES {
400 return Err(ManifestError::TooLarge(bytes.len()));
401 }
402 let m: DelegateManifest =
403 serde_json::from_slice(bytes).map_err(|e| ManifestError::Decode(e.to_string()))?;
404 if m.manifest_version == 0 {
405 return Err(ManifestError::BadVersion);
406 }
407 Ok(m)
408 }
409
410 /// Read the manifest from a raw WASM module (no version prefix).
411 ///
412 /// `Ok(None)` means the module has no manifest section: a delegate that
413 /// asked for nothing, which is every delegate built before manifests
414 /// existed. Only the section headers are walked; nothing is executed or
415 /// validated beyond what locating the section needs.
416 pub fn from_wasm(module: &[u8]) -> Result<Option<Self>, ManifestError> {
417 let mut found: Option<&[u8]> = None;
418 for section in custom_sections(module)? {
419 let (name, payload) = section?;
420 if name == MANIFEST_SECTION_NAME.as_bytes() {
421 if found.is_some() {
422 return Err(ManifestError::Duplicate);
423 }
424 found = Some(payload);
425 }
426 }
427 found.map(Self::from_bytes).transpose()
428 }
429}
430
431/// Decode a list whose entries are enum names, mapping any entry this reader
432/// cannot decode to the enum's `#[serde(other)]` variant.
433fn lenient_list<'de, D, T>(d: D) -> Result<Vec<T>, D::Error>
434where
435 D: serde::Deserializer<'de>,
436 T: serde::de::DeserializeOwned + Unknownable,
437{
438 // Anything but an array (`null`, or a shape a later format might use)
439 // reads as an empty list: asking for nothing is the safe direction.
440 let serde_json::Value::Array(raw) = serde_json::Value::deserialize(d)? else {
441 return Ok(Vec::new());
442 };
443 Ok(raw
444 .into_iter()
445 .map(|v| serde_json::from_value(v).unwrap_or_else(|_| T::unknown()))
446 .collect())
447}
448
449/// Decode the wake-up list, dropping any entry that is valid JSON but not a
450/// schedule this reader knows (a later format might add a shape it does not)
451/// instead of failing the manifest. Input the JSON parser itself rejects
452/// inside `wakeups` (malformed text, a lone surrogate, a number out of range,
453/// a duplicate `wakeups` key) fails the whole manifest; a 0.12.0 reader
454/// skipped the field without parsing it. Only the delegate's author can write
455/// such input, and the macro never does.
456fn lenient_wakeups<'de, D>(d: D) -> Result<Vec<WakeupSchedule>, D::Error>
457where
458 D: serde::Deserializer<'de>,
459{
460 let serde_json::Value::Array(raw) = serde_json::Value::deserialize(d)? else {
461 return Ok(Vec::new());
462 };
463 Ok(raw
464 .into_iter()
465 .filter_map(|v| serde_json::from_value(v).ok())
466 .collect())
467}
468
469trait Unknownable {
470 fn unknown() -> Self;
471}
472impl Unknownable for LifecycleKind {
473 fn unknown() -> Self {
474 LifecycleKind::Unknown
475 }
476}
477impl Unknownable for Capability {
478 fn unknown() -> Self {
479 Capability::Unknown
480 }
481}
482
483/// Used by `#[delegate(manifest(...))]` to check, at compile time, that the
484/// section name and version it writes are the ones this stdlib reads. Not
485/// part of the public API.
486#[doc(hidden)]
487pub const fn __manifest_macro_agrees(section: &str, version: u16) -> bool {
488 let (a, b) = (section.as_bytes(), MANIFEST_SECTION_NAME.as_bytes());
489 if a.len() != b.len() || version != MANIFEST_VERSION {
490 return false;
491 }
492 let mut i = 0;
493 while i < a.len() {
494 if a[i] != b[i] {
495 return false;
496 }
497 i += 1;
498 }
499 true
500}
501
502/// A custom section's `(name, payload)`.
503type CustomSection<'a> = (&'a [u8], &'a [u8]);
504
505/// Iterate over a WASM module's custom sections.
506fn custom_sections(
507 module: &[u8],
508) -> Result<impl Iterator<Item = Result<CustomSection<'_>, ManifestError>>, ManifestError> {
509 const HEADER: [u8; 8] = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
510 if module.len() < HEADER.len() || module[..HEADER.len()] != HEADER {
511 return Err(ManifestError::NotWasm);
512 }
513 let mut pos = HEADER.len();
514 let mut failed = false;
515 Ok(std::iter::from_fn(move || loop {
516 if failed || pos >= module.len() {
517 return None;
518 }
519 let parsed = (|| {
520 let id = module[pos];
521 let mut p = pos + 1;
522 let size = read_leb_u32(module, &mut p)? as usize;
523 let end = p.checked_add(size).ok_or(ManifestError::Malformed)?;
524 if end > module.len() {
525 return Err(ManifestError::Malformed);
526 }
527 let custom = if id == 0 {
528 let name_len = read_leb_u32(module, &mut p)? as usize;
529 let name_end = p.checked_add(name_len).ok_or(ManifestError::Malformed)?;
530 if name_end > end {
531 return Err(ManifestError::Malformed);
532 }
533 Some((&module[p..name_end], &module[name_end..end]))
534 } else {
535 None
536 };
537 Ok((end, custom))
538 })();
539 match parsed {
540 Ok((end, custom)) => {
541 pos = end;
542 if let Some(c) = custom {
543 return Some(Ok(c));
544 }
545 }
546 Err(e) => {
547 failed = true;
548 return Some(Err(e));
549 }
550 }
551 }))
552}
553
554fn read_leb_u32(buf: &[u8], pos: &mut usize) -> Result<u32, ManifestError> {
555 let mut result: u32 = 0;
556 for i in 0..5 {
557 let byte = *buf.get(*pos).ok_or(ManifestError::Malformed)?;
558 *pos += 1;
559 if i == 4 && byte & 0xf0 != 0 {
560 return Err(ManifestError::Malformed);
561 }
562 result |= u32::from(byte & 0x7f) << (7 * i);
563 if byte & 0x80 == 0 {
564 return Ok(result);
565 }
566 }
567 Err(ManifestError::Malformed)
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 fn leb(mut v: u32) -> Vec<u8> {
575 let mut out = Vec::new();
576 loop {
577 let mut b = (v & 0x7f) as u8;
578 v >>= 7;
579 if v != 0 {
580 b |= 0x80;
581 }
582 out.push(b);
583 if v == 0 {
584 return out;
585 }
586 }
587 }
588
589 fn custom_section(name: &str, payload: &[u8]) -> Vec<u8> {
590 let mut body = leb(name.len() as u32);
591 body.extend_from_slice(name.as_bytes());
592 body.extend_from_slice(payload);
593 let mut out = vec![0u8];
594 out.extend(leb(body.len() as u32));
595 out.extend(body);
596 out
597 }
598
599 fn module(sections: &[Vec<u8>]) -> Vec<u8> {
600 let mut m = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
601 // A non-custom section first (type section, empty vec), so the walker
602 // has to step over a section it does not care about.
603 m.extend([0x01, 0x01, 0x00]);
604 for s in sections {
605 m.extend_from_slice(s);
606 }
607 m
608 }
609
610 fn sample() -> DelegateManifest {
611 DelegateManifest::new(
612 vec![LifecycleKind::Installed, LifecycleKind::NodeStarted],
613 vec![Capability::Background],
614 )
615 }
616
617 #[test]
618 fn round_trips_through_a_wasm_custom_section() {
619 let m = module(&[
620 custom_section("name", b"whatever"),
621 custom_section(MANIFEST_SECTION_NAME, &sample().to_bytes()),
622 ]);
623 assert_eq!(DelegateManifest::from_wasm(&m).unwrap(), Some(sample()));
624 }
625
626 #[test]
627 fn a_module_without_the_section_has_no_manifest() {
628 let m = module(&[custom_section("producers", b"rustc")]);
629 assert_eq!(DelegateManifest::from_wasm(&m).unwrap(), None);
630 }
631
632 /// The exact JSON the macro writes. If this changes, every delegate that
633 /// declares a manifest re-keys on its next build, and older nodes must
634 /// still read it.
635 #[test]
636 fn json_shape_is_pinned() {
637 assert_eq!(
638 String::from_utf8(sample().to_bytes()).unwrap(),
639 r#"{"manifest_version":1,"lifecycle":["installed","node_started"],"capabilities":["background"]}"#
640 );
641 }
642
643 /// A manifest from a newer stdlib — an unknown field, an unknown
644 /// capability, an unknown lifecycle kind — must still be read, keeping
645 /// what this reader knows. Otherwise one new capability name would strip
646 /// every older node of the kinds it does understand.
647 #[test]
648 fn a_newer_manifest_is_read_keeping_known_entries() {
649 let json = br#"{"manifest_version":3,"lifecycle":["installed","woke_up"],
650 "capabilities":["background","teleport"],"brand_new_field":{"x":1}}"#;
651 let m = DelegateManifest::from_bytes(json).unwrap();
652 assert!(m.wants_lifecycle(LifecycleKind::Installed));
653 assert!(!m.wants_lifecycle(LifecycleKind::NodeStarted));
654 assert!(!m.wants_lifecycle(LifecycleKind::Unknown));
655 assert!(!m.wants_capability(Capability::Unknown));
656 assert_eq!(m.known_capabilities(), vec![Capability::Background]);
657 }
658
659 /// With wake-ups the JSON gains one trailing field, and only then: the
660 /// no-wake-up form above stays byte-identical to 0.12.0's.
661 #[test]
662 fn json_shape_with_wakeups_is_pinned() {
663 let m = sample().with_wakeup("heartbeat", 300);
664 assert_eq!(
665 String::from_utf8(m.to_bytes()).unwrap(),
666 r#"{"manifest_version":1,"lifecycle":["installed","node_started"],"capabilities":["background"],"wakeups":[{"tag":"heartbeat","every_secs":300}]}"#
667 );
668 assert_eq!(DelegateManifest::from_bytes(&m.to_bytes()).unwrap(), m);
669 }
670
671 /// What a node honours, at every boundary.
672 #[test]
673 fn effective_wakeups_clamp_and_filter_at_the_boundaries() {
674 let secs = |m: &DelegateManifest| {
675 m.effective_wakeups()
676 .into_iter()
677 .map(|(t, d)| (String::from_utf8(t).unwrap(), d.as_secs()))
678 .collect::<Vec<_>>()
679 };
680 let one = |every: u64| secs(&sample().with_wakeup("t", every));
681 assert_eq!(one(0), vec![("t".into(), MIN_WAKEUP_INTERVAL_SECS)]);
682 assert_eq!(
683 one(MIN_WAKEUP_INTERVAL_SECS - 1),
684 vec![("t".into(), MIN_WAKEUP_INTERVAL_SECS)]
685 );
686 assert_eq!(
687 one(MIN_WAKEUP_INTERVAL_SECS),
688 vec![("t".into(), MIN_WAKEUP_INTERVAL_SECS)]
689 );
690 assert_eq!(
691 one(MIN_WAKEUP_INTERVAL_SECS + 1),
692 vec![("t".into(), MIN_WAKEUP_INTERVAL_SECS + 1)]
693 );
694 assert_eq!(
695 one(MAX_WAKEUP_INTERVAL_SECS),
696 vec![("t".into(), MAX_WAKEUP_INTERVAL_SECS)]
697 );
698 assert_eq!(
699 one(MAX_WAKEUP_INTERVAL_SECS + 1),
700 vec![("t".into(), MAX_WAKEUP_INTERVAL_SECS)]
701 );
702 assert_eq!(one(u64::MAX), vec![("t".into(), MAX_WAKEUP_INTERVAL_SECS)]);
703
704 // Tags: empty and over-long are dropped, the longest allowed is kept.
705 let max_tag = "x".repeat(MAX_WAKEUP_TAG_BYTES);
706 let long_tag = "x".repeat(MAX_WAKEUP_TAG_BYTES + 1);
707 let m = sample()
708 .with_wakeup("", 120)
709 .with_wakeup(long_tag, 120)
710 .with_wakeup(max_tag.clone(), 120);
711 assert_eq!(secs(&m), vec![(max_tag, 120)]);
712
713 // The limit is in BYTES, not characters: 32 two-byte chars is exactly
714 // the limit, one more ASCII byte is over it.
715 let utf8_max = "é".repeat(MAX_WAKEUP_TAG_BYTES / 2);
716 assert_eq!(utf8_max.len(), MAX_WAKEUP_TAG_BYTES);
717 let utf8_over = format!("{utf8_max}a");
718 let m = sample()
719 .with_wakeup(utf8_over, 120)
720 .with_wakeup(utf8_max.clone(), 120);
721 assert_eq!(secs(&m), vec![(utf8_max, 120)]);
722
723 // A repeated tag keeps its first entry.
724 let m = sample().with_wakeup("a", 120).with_wakeup("a", 600);
725 assert_eq!(secs(&m), vec![("a".into(), 120)]);
726
727 // At most MAX_WAKEUPS, counting only entries that survive the filter.
728 let mut m = sample().with_wakeup("", 60);
729 for i in 0..MAX_WAKEUPS + 1 {
730 m = m.with_wakeup(format!("w{i}"), 60);
731 }
732 let got = secs(&m);
733 assert_eq!(got.len(), MAX_WAKEUPS);
734 assert_eq!(got[0].0, "w0");
735 assert_eq!(got[MAX_WAKEUPS - 1].0, format!("w{}", MAX_WAKEUPS - 1));
736
737 assert!(!sample().wants_wakeups());
738 assert!(!sample().with_wakeup("", 60).wants_wakeups());
739 assert!(sample().with_wakeup("a", 60).wants_wakeups());
740 }
741
742 /// A malformed or future-shaped wake-up entry is dropped, not fatal: the
743 /// lifecycle kinds and capabilities next to it must still count.
744 #[test]
745 fn a_malformed_wakeup_entry_is_dropped_not_fatal() {
746 let json = br#"{"manifest_version":2,"lifecycle":["node_started"],
747 "capabilities":["background"],
748 "wakeups":[{"tag":"ok","every_secs":90},{"tag":7},"junk",{"cron":"* * *"},
749 {"tag":"extra","every_secs":120,"jitter":5}]}"#;
750 let m = DelegateManifest::from_bytes(json).unwrap();
751 assert!(m.wants_lifecycle(LifecycleKind::NodeStarted));
752 assert_eq!(m.known_capabilities(), vec![Capability::Background]);
753 assert_eq!(
754 m.wakeups,
755 vec![
756 WakeupSchedule::new("ok", 90),
757 WakeupSchedule::new("extra", 120)
758 ]
759 );
760 for shape in [r#"null"#, r#""heartbeat""#, r#"{"heartbeat":300}"#] {
761 let json = format!(r#"{{"manifest_version":1,"wakeups":{shape}}}"#);
762 let m = DelegateManifest::from_bytes(json.as_bytes()).unwrap();
763 assert!(m.wakeups.is_empty(), "{shape}");
764 }
765 }
766
767 /// The reader that shipped in 0.12.0 (and so in every node that
768 /// understands manifests but not wake-ups) has no `wakeups` field. This
769 /// replicates its schema exactly and shows it reads a wake-up manifest,
770 /// keeping everything else: the property that lets one delegate build run
771 /// on nodes with and without wake-ups.
772 #[test]
773 fn a_reader_without_the_wakeups_field_still_reads_the_manifest() {
774 #[derive(Deserialize)]
775 struct ReaderV0120 {
776 manifest_version: u16,
777 #[serde(default, deserialize_with = "lenient_list")]
778 lifecycle: Vec<LifecycleKind>,
779 #[serde(default, deserialize_with = "lenient_list")]
780 capabilities: Vec<Capability>,
781 }
782 let m = sample().with_wakeup("heartbeat", 300);
783 let old: ReaderV0120 = serde_json::from_slice(&m.to_bytes()).unwrap();
784 assert_eq!(old.manifest_version, 1);
785 assert_eq!(old.lifecycle, m.lifecycle);
786 assert_eq!(old.capabilities, m.capabilities);
787 }
788
789 #[test]
790 fn missing_lists_default_to_empty() {
791 let m = DelegateManifest::from_bytes(br#"{"manifest_version":1}"#).unwrap();
792 assert!(m.lifecycle.is_empty() && m.capabilities.is_empty());
793 let m = DelegateManifest::from_bytes(
794 br#"{"manifest_version":1,"lifecycle":null,"capabilities":null}"#,
795 )
796 .unwrap();
797 assert!(m.lifecycle.is_empty() && m.capabilities.is_empty());
798 let m = DelegateManifest::from_bytes(
799 br#"{"manifest_version":1,"lifecycle":"installed","capabilities":{"background":{}}}"#,
800 )
801 .unwrap();
802 assert!(m.lifecycle.is_empty() && m.capabilities.is_empty());
803 }
804
805 /// A later format might write an entry that is not a bare name. That entry
806 /// is unknown to this reader; the known ones next to it must still count.
807 #[test]
808 fn a_non_string_entry_reads_as_unknown_not_as_an_error() {
809 let json = br#"{"manifest_version":2,
810 "lifecycle":[{"woke_up":{"every_s":60}},"node_started",7],
811 "capabilities":[{"notify":{"max_per_hour":4}},"background",null]}"#;
812 let m = DelegateManifest::from_bytes(json).unwrap();
813 assert_eq!(
814 m.lifecycle,
815 vec![
816 LifecycleKind::Unknown,
817 LifecycleKind::NodeStarted,
818 LifecycleKind::Unknown
819 ]
820 );
821 assert_eq!(m.known_capabilities(), vec![Capability::Background]);
822 }
823
824 #[test]
825 fn macro_agreement_check() {
826 assert!(__manifest_macro_agrees(
827 MANIFEST_SECTION_NAME,
828 MANIFEST_VERSION
829 ));
830 assert!(!__manifest_macro_agrees(
831 "freenet-manifesT",
832 MANIFEST_VERSION
833 ));
834 assert!(!__manifest_macro_agrees(
835 "freenet-manifest2",
836 MANIFEST_VERSION
837 ));
838 assert!(!__manifest_macro_agrees(
839 MANIFEST_SECTION_NAME,
840 MANIFEST_VERSION + 1
841 ));
842 }
843
844 #[test]
845 fn rejects_version_zero_oversize_and_garbage() {
846 assert_eq!(
847 DelegateManifest::from_bytes(br#"{"manifest_version":0}"#),
848 Err(ManifestError::BadVersion)
849 );
850 let big = vec![b' '; MAX_MANIFEST_BYTES + 1];
851 assert_eq!(
852 DelegateManifest::from_bytes(&big),
853 Err(ManifestError::TooLarge(MAX_MANIFEST_BYTES + 1))
854 );
855 assert!(matches!(
856 DelegateManifest::from_bytes(b"not json"),
857 Err(ManifestError::Decode(_))
858 ));
859 }
860
861 /// Two sections would be ambiguous. It also catches two crates in one
862 /// build each emitting a manifest: the linker would normally concatenate
863 /// same-named sections into one, which fails to decode instead, but a
864 /// post-link tool could leave them separate.
865 #[test]
866 fn rejects_a_duplicate_section() {
867 let payload = sample().to_bytes();
868 let m = module(&[
869 custom_section(MANIFEST_SECTION_NAME, &payload),
870 custom_section(MANIFEST_SECTION_NAME, &payload),
871 ]);
872 assert_eq!(
873 DelegateManifest::from_wasm(&m),
874 Err(ManifestError::Duplicate)
875 );
876 }
877
878 #[test]
879 fn concatenated_manifests_fail_to_decode() {
880 let mut payload = sample().to_bytes();
881 payload.extend(sample().to_bytes());
882 let m = module(&[custom_section(MANIFEST_SECTION_NAME, &payload)]);
883 assert!(matches!(
884 DelegateManifest::from_wasm(&m),
885 Err(ManifestError::Decode(_))
886 ));
887 }
888
889 #[test]
890 fn rejects_non_wasm_and_truncated_modules() {
891 assert_eq!(
892 DelegateManifest::from_wasm(b"\0asm"),
893 Err(ManifestError::NotWasm)
894 );
895 assert_eq!(
896 DelegateManifest::from_wasm(b"hello world, not wasm"),
897 Err(ManifestError::NotWasm)
898 );
899 let mut m = module(&[custom_section(MANIFEST_SECTION_NAME, &sample().to_bytes())]);
900 m.truncate(m.len() - 3);
901 assert_eq!(
902 DelegateManifest::from_wasm(&m),
903 Err(ManifestError::Malformed)
904 );
905 // A section whose declared size runs past the end.
906 let mut m = module(&[]);
907 m.extend([0x00, 0xff, 0xff, 0x03]);
908 assert_eq!(
909 DelegateManifest::from_wasm(&m),
910 Err(ManifestError::Malformed)
911 );
912 // An over-long LEB128 (more than 5 bytes).
913 let mut m = module(&[]);
914 m.extend([0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00]);
915 assert_eq!(
916 DelegateManifest::from_wasm(&m),
917 Err(ManifestError::Malformed)
918 );
919 }
920
921 /// Wire pin for the nested enum. Appending a variant is fine; reordering
922 /// or inserting one silently reinterprets deployed delegates' input.
923 #[test]
924 fn lifecycle_event_tags_are_pinned() {
925 fn tag(e: &LifecycleEvent) -> u32 {
926 match e {
927 LifecycleEvent::Installed => 0,
928 LifecycleEvent::NodeStarted { .. } => 1,
929 }
930 }
931 let all = [
932 LifecycleEvent::Installed,
933 LifecycleEvent::NodeStarted {
934 down_since_ms: Some(0x0102_0304_0506_0708),
935 },
936 ];
937 for e in &all {
938 let enc = bincode::serialize(e).unwrap();
939 assert_eq!(u32::from_le_bytes(enc[..4].try_into().unwrap()), tag(e));
940 assert_eq!(&bincode::deserialize::<LifecycleEvent>(&enc).unwrap(), e);
941 }
942 // Full byte layout of NodeStarted: tag 1, Option tag 1, u64 LE.
943 assert_eq!(
944 bincode::serialize(&all[1]).unwrap(),
945 vec![1, 0, 0, 0, 1, 8, 7, 6, 5, 4, 3, 2, 1]
946 );
947 // `kind()` agrees with the manifest kinds.
948 assert_eq!(all[0].kind(), LifecycleKind::Installed);
949 assert_eq!(all[1].kind(), LifecycleKind::NodeStarted);
950 }
951}