use std::borrow::Cow;
use std::collections::BTreeSet;
use serde::de::{DeserializeOwned, IgnoredAny};
use serde::{Deserialize, Serialize};
use super::contracts::{Encoder, valid_identity};
use crate::PREFIX;
const ENTRY_LIMIT: usize = 256;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Section {
pub namespace: Cow<'static, str>,
pub payload: Cow<'static, str>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "Decoded")]
pub struct Manifest {
pub facility: String,
pub owner: String,
pub sections: Vec<Section>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Decoded {
#[serde(default)]
kind: Option<String>,
facility: String,
owner: String,
sections: Vec<Section>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("multiple contribution inventories for `{owner}`")]
Duplicate { owner: String },
#[error("contribution inventory requires a guest or sidecar facility and a valid owner")]
Identity,
#[error("contribution inventory exceeds its section budget")]
Budget,
#[error("contribution namespace `{namespace}` is invalid or repeated")]
Namespace { namespace: String },
#[error("contribution section `{namespace}` is not JSON: {source}")]
Payload {
namespace: String,
#[source]
source: serde_json::Error,
},
#[error("invalid contribution inventory JSON: {0}")]
Decode(#[from] serde_json::Error),
}
impl Section {
pub const fn new(namespace: &'static str, payload: &'static str) -> Self {
Self { namespace: Cow::Borrowed(namespace), payload: Cow::Borrowed(payload) }
}
pub fn decode<T: DeserializeOwned>(&self) -> serde_json::Result<T> {
serde_json::from_str(&self.payload)
}
const fn borrowed(&self) -> (&'static str, &'static str) {
match (&self.namespace, &self.payload) {
(Cow::Borrowed(namespace), Cow::Borrowed(payload)) => (namespace, payload),
_ => panic!("const contribution sections must be borrowed"),
}
}
}
impl TryFrom<Decoded> for Manifest {
type Error = Error;
fn try_from(value: Decoded) -> Result<Self, Error> {
if value.kind.as_deref().is_some_and(|kind| kind != "contributions") {
return Err(Error::Identity);
}
let manifest = Self { facility: value.facility, owner: value.owner, sections: value.sections };
validate(&manifest)?;
Ok(manifest)
}
}
pub fn select(manifests: Vec<Manifest>, facility: &str, owner: &str) -> Result<Vec<Section>, Error> {
let mut matching = manifests.into_iter().filter(|manifest| manifest.facility == facility && manifest.owner == owner);
let Some(manifest) = matching.next() else {
return Ok(Vec::new());
};
if matching.next().is_some() {
return Err(Error::Duplicate { owner: owner.to_owned() });
}
Ok(manifest.sections)
}
pub fn validate(manifest: &Manifest) -> Result<(), Error> {
if !valid_identity(&manifest.facility, &manifest.owner) {
return Err(Error::Identity);
}
if manifest.sections.len() > ENTRY_LIMIT {
return Err(Error::Budget);
}
let mut namespaces = BTreeSet::new();
for section in &manifest.sections {
if !crate::artifact_subset(§ion.namespace) || !namespaces.insert(§ion.namespace) {
return Err(Error::Namespace { namespace: section.namespace.to_string() });
}
serde_json::from_str::<IgnoredAny>(§ion.payload)
.map_err(|source| Error::Payload { namespace: section.namespace.to_string(), source })?;
}
Ok(())
}
pub const fn namespaced(sections: &[Section], namespaces: &[&str]) -> bool {
if sections.len() != namespaces.len() {
return false;
}
let mut index = 0;
while index < sections.len() {
let section = sections[index].borrowed().0.as_bytes();
let expected = namespaces[index].as_bytes();
if section.len() != expected.len() {
return false;
}
let mut byte = 0;
while byte < section.len() {
if section[byte] != expected[byte] {
return false;
}
byte += 1;
}
index += 1;
}
true
}
pub const fn encoded_len(facility: &str, owner: &str, sections: &[Section]) -> usize {
let size = encoded::<0>(facility, owner, sections).cursor;
assert!(size - PREFIX.len() - 1 <= crate::FRAGMENT_LIMIT);
size
}
pub const fn encode<const N: usize>(facility: &str, owner: &str, sections: &[Section]) -> [u8; N] {
assert!(N == encoded_len(facility, owner, sections));
encoded::<N>(facility, owner, sections).bytes
}
const fn encoded<const N: usize>(facility: &str, owner: &str, sections: &[Section]) -> Encoder<N> {
assert!(valid_identity(facility, owner));
assert!(sections.len() <= ENTRY_LIMIT, "contribution sections exceed their budget");
let mut out = Encoder { bytes: [0; N], cursor: 0 };
out.raw(PREFIX);
out.raw(b"{\"kind\":\"contributions\",\"facility\":");
out.quoted(facility);
out.raw(b",\"owner\":");
out.quoted(owner);
out.raw(b",\"sections\":[");
let mut index = 0;
while index < sections.len() {
if index > 0 {
out.raw(b",");
}
let (namespace, payload) = sections[index].borrowed();
assert!(crate::artifact_subset(namespace), "contribution namespaces use the portable artifact subset");
out.raw(b"{\"namespace\":");
out.quoted(namespace);
out.raw(b",\"payload\":");
out.quoted(payload);
out.raw(b"}");
index += 1;
}
out.raw(b"]}\0");
out
}