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 {
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
131pub 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(§ion.namespace) || !namespaces.insert(§ion.namespace) {
154 return Err(Error::Namespace { namespace: section.namespace.to_string() });
155 }
156 serde_json::from_str::<IgnoredAny>(§ion.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
198const 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