Skip to main content

macula_rust/record/
content_announcement.rs

1//! Content announcements (macula 12.6.0, D27): a node's statement that it
2//! shares the content with a tag 2 content id, served on its content
3//! procedure in a realm and reachable through a station.
4
5use crate::cbor::Value;
6
7use super::{
8    entry, id_field, malformed, payload::is_content_id, text_field, unsigned, Record, RecordError,
9    RecordType,
10};
11
12/// Where the content is served, then its name, size and chunk count, each
13/// left out when empty or `None`; `ttl_ms`, 0 for the default, 48 hours.
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct ContentAnnouncementOptions {
16    pub realm_id: [u8; 32],
17    pub serving_station: [u8; 32],
18    pub procedure: String,
19    pub name: String,
20    pub size: Option<u64>,
21    pub chunk_count: Option<u64>,
22    pub ttl_ms: u64,
23}
24
25/// An unsigned announcement by `announcer_node`, which signs it. A content id
26/// of another size or tag, or an empty procedure, is refused.
27pub fn new_content_announcement(
28    announcer_node: &[u8; 32],
29    mcid: &[u8],
30    opts: &ContentAnnouncementOptions,
31) -> Result<Record, RecordError> {
32    if !is_content_id(Some(&Value::Bytes(mcid.to_vec()))) {
33        return Err(RecordError::NotAContentId);
34    }
35    if opts.procedure.is_empty() {
36        return Err(malformed(
37            "a content announcement names its content procedure",
38        ));
39    }
40    let mut entries = vec![
41        entry("announcer_node", Value::Bytes(announcer_node.to_vec())),
42        entry("mcid", Value::Bytes(mcid.to_vec())),
43        entry("realm_id", Value::Bytes(opts.realm_id.to_vec())),
44        entry(
45            "serving_station",
46            Value::Bytes(opts.serving_station.to_vec()),
47        ),
48        entry("procedure", Value::text(opts.procedure.clone())),
49    ];
50    if !opts.name.is_empty() {
51        entries.push(entry("name", Value::text(opts.name.clone())));
52    }
53    if let Some(size) = opts.size {
54        entries.push(entry("size", Value::Int(i128::from(size))));
55    }
56    if let Some(chunks) = opts.chunk_count {
57        entries.push(entry("chunk_count", Value::Int(i128::from(chunks))));
58    }
59    Ok(unsigned(
60        RecordType::CONTENT_ANNOUNCEMENT,
61        Value::Map(entries),
62        opts.ttl_ms,
63    ))
64}
65
66/// A content announcement's payload.
67#[derive(Debug, Clone, Default, PartialEq, Eq)]
68pub struct ContentAnnouncement {
69    pub announcer_node: [u8; 32],
70    pub mcid: Vec<u8>,
71    pub realm_id: [u8; 32],
72    pub serving_station: [u8; 32],
73    pub procedure: String,
74    pub name: String,
75    pub size: Option<u64>,
76    pub chunk_count: Option<u64>,
77}
78
79/// Reads a content announcement's payload.
80pub fn read_content_announcement(r: &Record) -> Result<ContentAnnouncement, RecordError> {
81    if r.record_type != RecordType::CONTENT_ANNOUNCEMENT {
82        return Err(malformed("not a content announcement"));
83    }
84    let p = &r.payload;
85    let optional = |name: &str| match p.get(name) {
86        Some(Value::Int(n)) if *n >= 0 => u64::try_from(*n).ok(),
87        _ => None,
88    };
89    Ok(ContentAnnouncement {
90        announcer_node: id_field(p, "announcer_node"),
91        mcid: match p.get("mcid") {
92            Some(Value::Bytes(b)) => b.clone(),
93            _ => Vec::new(),
94        },
95        realm_id: id_field(p, "realm_id"),
96        serving_station: id_field(p, "serving_station"),
97        procedure: text_field(p, "procedure"),
98        name: text_field(p, "name"),
99        size: optional("size"),
100        chunk_count: optional("chunk_count"),
101    })
102}