harmos_domain/inventory/
contributions.rs1use std::borrow::Cow;
18use std::collections::BTreeSet;
19use serde::de::{DeserializeOwned, IgnoredAny};
21use serde::{Deserialize, Serialize};
22use super::contracts::{Encoder, valid_identity};
24use crate::PREFIX;
25
26const ENTRY_LIMIT: usize = 256;
29
30#[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#[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
90impl 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
124pub 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(§ion.namespace) || !namespaces.insert(§ion.namespace) {
147 return Err(Error::Namespace { namespace: section.namespace.to_string() });
148 }
149 serde_json::from_str::<IgnoredAny>(§ion.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
189const 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