Skip to main content

harmos_domain/inventory/
contributions.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14/* === Imports === */
15
16// Standard Library Imports
17use std::borrow::Cow;
18use std::collections::BTreeSet;
19// Third Party Imports
20use serde::de::{DeserializeOwned, IgnoredAny};
21use serde::{Deserialize, Serialize};
22// Local Imports
23use super::contracts::{Encoder, valid_identity};
24use crate::PREFIX;
25
26/* === Constants === */
27
28const ENTRY_LIMIT: usize = 256;
29
30/* === Macros === */
31
32/* === Type Aliases === */
33
34/* === Structs === */
35
36#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct Section {
39    pub namespace: Cow<'static, str>,
40    pub payload: Cow<'static, str>,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(try_from = "Decoded")]
45pub struct Manifest {
46    pub facility: String,
47    pub owner: String,
48    pub sections: Vec<Section>,
49}
50
51#[derive(Deserialize)]
52#[serde(deny_unknown_fields)]
53struct Decoded {
54    #[serde(default)]
55    kind: Option<String>,
56
57    facility: String,
58    owner: String,
59    sections: Vec<Section>,
60}
61
62/* === Enums === */
63
64#[derive(Debug, thiserror::Error)]
65pub enum Error {
66    #[error("multiple contribution inventories for `{owner}`")]
67    Duplicate { owner: String },
68
69    #[error("contribution inventory requires a guest or sidecar facility and a valid owner")]
70    Identity,
71
72    #[error("contribution inventory exceeds its section budget")]
73    Budget,
74
75    #[error("contribution namespace `{namespace}` is invalid or repeated")]
76    Namespace { namespace: String },
77
78    #[error("contribution section `{namespace}` is not JSON: {source}")]
79    Payload {
80        namespace: String,
81
82        #[source]
83        source: serde_json::Error,
84    },
85
86    #[error("invalid contribution inventory JSON: {0}")]
87    Decode(#[from] serde_json::Error),
88}
89
90/* === Traits === */
91
92/* === Implementations === */
93
94impl Section {
95    pub const fn new(namespace: &'static str, payload: &'static str) -> Self {
96        Self {
97            namespace: Cow::Borrowed(namespace),
98            payload: Cow::Borrowed(payload),
99        }
100    }
101
102    pub fn decode<T: DeserializeOwned>(&self) -> serde_json::Result<T> {
103        serde_json::from_str(&self.payload)
104    }
105
106    const fn borrowed(&self) -> (&'static str, &'static str) {
107        match (&self.namespace, &self.payload) {
108            (Cow::Borrowed(namespace), Cow::Borrowed(payload)) => (namespace, payload),
109            _ => panic!("const contribution sections must be borrowed"),
110        }
111    }
112}
113
114impl TryFrom<Decoded> for Manifest {
115    type Error = Error;
116
117    fn try_from(value: Decoded) -> Result<Self, Error> {
118        if value.kind.as_deref().is_some_and(|kind| kind != "contributions") {
119            return Err(Error::Identity);
120        }
121        let manifest = Self {
122            facility: value.facility,
123            owner: value.owner,
124            sections: value.sections,
125        };
126        validate(&manifest)?;
127        Ok(manifest)
128    }
129}
130
131/* === Public Functions === */
132
133pub fn select(manifests: Vec<Manifest>, facility: &str, owner: &str) -> Result<Vec<Section>, Error> {
134    let mut matching = manifests.into_iter().filter(|manifest| manifest.facility == facility && manifest.owner == owner);
135    let Some(manifest) = matching.next() else {
136        return Ok(Vec::new());
137    };
138    if matching.next().is_some() {
139        return Err(Error::Duplicate { owner: owner.to_owned() });
140    }
141    Ok(manifest.sections)
142}
143
144pub fn validate(manifest: &Manifest) -> Result<(), Error> {
145    if !valid_identity(&manifest.facility, &manifest.owner) {
146        return Err(Error::Identity);
147    }
148    if manifest.sections.len() > ENTRY_LIMIT {
149        return Err(Error::Budget);
150    }
151    let mut namespaces = BTreeSet::new();
152    for section in &manifest.sections {
153        if !crate::artifact_subset(&section.namespace) || !namespaces.insert(&section.namespace) {
154            return Err(Error::Namespace { namespace: section.namespace.to_string() });
155        }
156        serde_json::from_str::<IgnoredAny>(&section.payload).map_err(|source| Error::Payload {
157            namespace: section.namespace.to_string(),
158            source,
159        })?;
160    }
161    Ok(())
162}
163
164pub const fn namespaced(sections: &[Section], namespaces: &[&str]) -> bool {
165    if sections.len() != namespaces.len() {
166        return false;
167    }
168    let mut index = 0;
169    while index < sections.len() {
170        let section = sections[index].borrowed().0.as_bytes();
171        let expected = namespaces[index].as_bytes();
172        if section.len() != expected.len() {
173            return false;
174        }
175        let mut byte = 0;
176        while byte < section.len() {
177            if section[byte] != expected[byte] {
178                return false;
179            }
180            byte += 1;
181        }
182        index += 1;
183    }
184    true
185}
186
187pub const fn encoded_len(facility: &str, owner: &str, sections: &[Section]) -> usize {
188    let size = encoded::<0>(facility, owner, sections).cursor;
189    assert!(size - PREFIX.len() - 1 <= crate::FRAGMENT_LIMIT);
190    size
191}
192
193pub const fn encode<const N: usize>(facility: &str, owner: &str, sections: &[Section]) -> [u8; N] {
194    assert!(N == encoded_len(facility, owner, sections));
195    encoded::<N>(facility, owner, sections).bytes
196}
197
198/* === Private Functions === */
199
200const fn encoded<const N: usize>(facility: &str, owner: &str, sections: &[Section]) -> Encoder<N> {
201    assert!(valid_identity(facility, owner));
202    assert!(sections.len() <= ENTRY_LIMIT, "contribution sections exceed their budget");
203    let mut out = Encoder { bytes: [0; N], cursor: 0 };
204    out.raw(PREFIX);
205    out.raw(b"{\"kind\":\"contributions\",\"facility\":");
206    out.quoted(facility);
207    out.raw(b",\"owner\":");
208    out.quoted(owner);
209    out.raw(b",\"sections\":[");
210    let mut index = 0;
211    while index < sections.len() {
212        if index > 0 {
213            out.raw(b",");
214        }
215        let (namespace, payload) = sections[index].borrowed();
216        assert!(crate::artifact_subset(namespace), "contribution namespaces use the portable artifact subset");
217        out.raw(b"{\"namespace\":");
218        out.quoted(namespace);
219        out.raw(b",\"payload\":");
220        out.quoted(payload);
221        out.raw(b"}");
222        index += 1;
223    }
224    out.raw(b"]}\0");
225    out
226}
227
228/* === Tests === */