Skip to main content

a3s_code_core/capability/
descriptor.rs

1use std::collections::BTreeMap;
2
3use serde::Serialize;
4
5use super::{
6    CapabilityId, CapabilityKind, CapabilitySetError, CapabilitySource, Sha256Digest,
7    MAX_CAPABILITIES,
8};
9
10pub const MAX_CAPABILITY_DEPENDENCIES: usize = 128;
11const MAX_CAPABILITY_PUBLIC_NAME_BYTES: usize = 256;
12
13/// Serializable identity plane for one projected capability surface.
14#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct CapabilityDescriptor {
17    id: CapabilityId,
18    public_name: Box<str>,
19    surface_digest: Sha256Digest,
20    dependencies: Vec<CapabilityId>,
21}
22
23impl CapabilityDescriptor {
24    pub fn new(
25        source: &CapabilitySource,
26        kind: CapabilityKind,
27        local_id: impl Into<String>,
28        public_name: impl Into<String>,
29        surface_digest: Sha256Digest,
30        dependencies: impl IntoIterator<Item = CapabilityId>,
31    ) -> Result<Self, CapabilitySetError> {
32        let id = CapabilityId::new(source, kind, local_id)?;
33        let public_name = public_name.into();
34        validate_public_name(&public_name)?;
35        let mut dependencies = dependencies.into_iter().collect::<Vec<_>>();
36        if dependencies.len() > MAX_CAPABILITY_DEPENDENCIES {
37            return Err(CapabilitySetError::BoundExceeded {
38                field: "dependencies",
39                max: MAX_CAPABILITY_DEPENDENCIES,
40            });
41        }
42        dependencies.sort();
43        for pair in dependencies.windows(2) {
44            if pair[0] == pair[1] {
45                return Err(CapabilitySetError::DuplicateDependency {
46                    capability: id.to_string(),
47                    dependency: pair[0].to_string(),
48                });
49            }
50        }
51        if dependencies.binary_search(&id).is_ok() {
52            return Err(CapabilitySetError::SelfDependency {
53                capability: id.to_string(),
54            });
55        }
56        Ok(Self {
57            id,
58            public_name: public_name.into_boxed_str(),
59            surface_digest,
60            dependencies,
61        })
62    }
63
64    pub fn id(&self) -> &CapabilityId {
65        &self.id
66    }
67
68    pub fn public_name(&self) -> &str {
69        &self.public_name
70    }
71
72    pub fn surface_digest(&self) -> &Sha256Digest {
73        &self.surface_digest
74    }
75
76    pub fn dependencies(&self) -> &[CapabilityId] {
77        &self.dependencies
78    }
79}
80
81/// Complete descriptor batch owned by one exact source generation.
82#[derive(Clone, Debug, Eq, PartialEq)]
83pub struct CapabilityContribution {
84    source: CapabilitySource,
85    descriptors: BTreeMap<CapabilityId, CapabilityDescriptor>,
86}
87
88impl CapabilityContribution {
89    pub fn new(
90        source: CapabilitySource,
91        descriptors: impl IntoIterator<Item = CapabilityDescriptor>,
92    ) -> Result<Self, CapabilitySetError> {
93        let mut canonical = BTreeMap::new();
94        for descriptor in descriptors {
95            if canonical.len() >= MAX_CAPABILITIES {
96                return Err(CapabilitySetError::BoundExceeded {
97                    field: "capabilities",
98                    max: MAX_CAPABILITIES,
99                });
100            }
101            if descriptor.id().source() != source.id() {
102                return Err(CapabilitySetError::SourceMismatch {
103                    capability: descriptor.id().to_string(),
104                    expected_source: source.id().to_string(),
105                    actual_source: descriptor.id().source().to_string(),
106                });
107            }
108            let id = descriptor.id().clone();
109            if canonical.insert(id.clone(), descriptor).is_some() {
110                return Err(CapabilitySetError::DuplicateCapability {
111                    capability: id.to_string(),
112                });
113            }
114        }
115        if canonical.is_empty() {
116            return Err(CapabilitySetError::EmptyContribution {
117                source_id: source.id().to_string(),
118            });
119        }
120        Ok(Self {
121            source,
122            descriptors: canonical,
123        })
124    }
125
126    pub fn source(&self) -> &CapabilitySource {
127        &self.source
128    }
129
130    pub fn len(&self) -> usize {
131        self.descriptors.len()
132    }
133
134    pub fn is_empty(&self) -> bool {
135        self.descriptors.is_empty()
136    }
137
138    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&CapabilityId, &CapabilityDescriptor)> {
139        self.descriptors.iter()
140    }
141
142    pub(super) fn into_parts(
143        self,
144    ) -> (
145        CapabilitySource,
146        BTreeMap<CapabilityId, CapabilityDescriptor>,
147    ) {
148        (self.source, self.descriptors)
149    }
150}
151
152fn validate_public_name(value: &str) -> Result<(), CapabilitySetError> {
153    if value.is_empty() || value.trim() != value || value.chars().any(char::is_control) {
154        return Err(CapabilitySetError::InvalidIdentifier {
155            field: "public_name",
156            reason: "it is empty, padded, or contains control characters",
157        });
158    }
159    if value.len() > MAX_CAPABILITY_PUBLIC_NAME_BYTES {
160        return Err(CapabilitySetError::BoundExceeded {
161            field: "public_name",
162            max: MAX_CAPABILITY_PUBLIC_NAME_BYTES,
163        });
164    }
165    Ok(())
166}