a3s_code_core/capability/
descriptor.rs1use 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#[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#[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}