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 { namespace: Cow::Borrowed(namespace), payload: Cow::Borrowed(payload) }
97    }
98
99    pub fn decode<T: DeserializeOwned>(&self) -> serde_json::Result<T> {
100        serde_json::from_str(&self.payload)
101    }
102
103    const fn borrowed(&self) -> (&'static str, &'static str) {
104        match (&self.namespace, &self.payload) {
105            (Cow::Borrowed(namespace), Cow::Borrowed(payload)) => (namespace, payload),
106            _ => panic!("const contribution sections must be borrowed"),
107        }
108    }
109}
110
111impl TryFrom<Decoded> for Manifest {
112    type Error = Error;
113
114    fn try_from(value: Decoded) -> Result<Self, Error> {
115        if value.kind.as_deref().is_some_and(|kind| kind != "contributions") {
116            return Err(Error::Identity);
117        }
118        let manifest = Self { facility: value.facility, owner: value.owner, sections: value.sections };
119        validate(&manifest)?;
120        Ok(manifest)
121    }
122}
123
124/* === Public Functions === */
125
126pub fn select(manifests: Vec<Manifest>, facility: &str, owner: &str) -> Result<Vec<Section>, Error> {
127    let mut matching = manifests.into_iter().filter(|manifest| manifest.facility == facility && manifest.owner == owner);
128    let Some(manifest) = matching.next() else {
129        return Ok(Vec::new());
130    };
131    if matching.next().is_some() {
132        return Err(Error::Duplicate { owner: owner.to_owned() });
133    }
134    Ok(manifest.sections)
135}
136
137pub fn validate(manifest: &Manifest) -> Result<(), Error> {
138    if !valid_identity(&manifest.facility, &manifest.owner) {
139        return Err(Error::Identity);
140    }
141    if manifest.sections.len() > ENTRY_LIMIT {
142        return Err(Error::Budget);
143    }
144    let mut namespaces = BTreeSet::new();
145    for section in &manifest.sections {
146        if !crate::artifact_subset(&section.namespace) || !namespaces.insert(&section.namespace) {
147            return Err(Error::Namespace { namespace: section.namespace.to_string() });
148        }
149        serde_json::from_str::<IgnoredAny>(&section.payload)
150            .map_err(|source| Error::Payload { namespace: section.namespace.to_string(), source })?;
151    }
152    Ok(())
153}
154
155pub const fn namespaced(sections: &[Section], namespaces: &[&str]) -> bool {
156    if sections.len() != namespaces.len() {
157        return false;
158    }
159    let mut index = 0;
160    while index < sections.len() {
161        let section = sections[index].borrowed().0.as_bytes();
162        let expected = namespaces[index].as_bytes();
163        if section.len() != expected.len() {
164            return false;
165        }
166        let mut byte = 0;
167        while byte < section.len() {
168            if section[byte] != expected[byte] {
169                return false;
170            }
171            byte += 1;
172        }
173        index += 1;
174    }
175    true
176}
177
178pub const fn encoded_len(facility: &str, owner: &str, sections: &[Section]) -> usize {
179    let size = encoded::<0>(facility, owner, sections).cursor;
180    assert!(size - PREFIX.len() - 1 <= crate::FRAGMENT_LIMIT);
181    size
182}
183
184pub const fn encode<const N: usize>(facility: &str, owner: &str, sections: &[Section]) -> [u8; N] {
185    assert!(N == encoded_len(facility, owner, sections));
186    encoded::<N>(facility, owner, sections).bytes
187}
188
189/* === Private Functions === */
190
191const fn encoded<const N: usize>(facility: &str, owner: &str, sections: &[Section]) -> Encoder<N> {
192    assert!(valid_identity(facility, owner));
193    assert!(sections.len() <= ENTRY_LIMIT, "contribution sections exceed their budget");
194    let mut out = Encoder { bytes: [0; N], cursor: 0 };
195    out.raw(PREFIX);
196    out.raw(b"{\"kind\":\"contributions\",\"facility\":");
197    out.quoted(facility);
198    out.raw(b",\"owner\":");
199    out.quoted(owner);
200    out.raw(b",\"sections\":[");
201    let mut index = 0;
202    while index < sections.len() {
203        if index > 0 {
204            out.raw(b",");
205        }
206        let (namespace, payload) = sections[index].borrowed();
207        assert!(crate::artifact_subset(namespace), "contribution namespaces use the portable artifact subset");
208        out.raw(b"{\"namespace\":");
209        out.quoted(namespace);
210        out.raw(b",\"payload\":");
211        out.quoted(payload);
212        out.raw(b"}");
213        index += 1;
214    }
215    out.raw(b"]}\0");
216    out
217}
218
219/* === Tests === */