1use core::fmt;
4
5use cloud_sdk_sanitization::SecretBuffer;
6
7use crate::buffer::write_u64;
8use crate::transport::{HeaderName, RequestHeader};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum SchemaVersionError {
13 ZeroMajor,
15 InvalidVersion,
17 UnreviewedMajor,
19 InvalidHeader,
21 OutputTooSmall,
23}
24
25impl_static_error!(SchemaVersionError,
26 Self::ZeroMajor => "schema major version must be nonzero",
27 Self::InvalidVersion => "schema version is not canonical major.minor text",
28 Self::UnreviewedMajor => "schema version differs from the reviewed major",
29 Self::InvalidHeader => "schema validation header is invalid",
30 Self::OutputTooSmall => "schema version output is too small",
31);
32
33#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub struct SchemaVersion {
36 major: u16,
37 minor: u16,
38}
39
40impl SchemaVersion {
41 pub const fn new(major: u16, minor: u16) -> Result<Self, SchemaVersionError> {
43 if major == 0 {
44 return Err(SchemaVersionError::ZeroMajor);
45 }
46 Ok(Self { major, minor })
47 }
48
49 pub fn parse(value: &[u8]) -> Result<Self, SchemaVersionError> {
51 let dot = value
52 .iter()
53 .position(|byte| *byte == b'.')
54 .ok_or(SchemaVersionError::InvalidVersion)?;
55 if value
56 .get(dot.saturating_add(1)..)
57 .is_none_or(|part| part.is_empty())
58 || value.get(..dot).is_none_or(|part| part.is_empty())
59 || value
60 .get(dot.saturating_add(1)..)
61 .is_some_and(|part| part.contains(&b'.'))
62 {
63 return Err(SchemaVersionError::InvalidVersion);
64 }
65 let major = parse_component(value.get(..dot).ok_or(SchemaVersionError::InvalidVersion)?)?;
66 let minor = parse_component(
67 value
68 .get(dot.saturating_add(1)..)
69 .ok_or(SchemaVersionError::InvalidVersion)?,
70 )?;
71 Self::new(major, minor)
72 }
73
74 #[must_use]
76 pub const fn major(self) -> u16 {
77 self.major
78 }
79
80 #[must_use]
82 pub const fn minor(self) -> u16 {
83 self.minor
84 }
85}
86
87#[derive(Clone, Copy, Eq, PartialEq)]
89pub struct ReviewedSchemaMajor {
90 major: u16,
91 source_sha256: [u8; 32],
92}
93
94impl ReviewedSchemaMajor {
95 pub const fn new(major: u16, source_sha256: [u8; 32]) -> Result<Self, SchemaVersionError> {
97 if major == 0 {
98 return Err(SchemaVersionError::ZeroMajor);
99 }
100 Ok(Self {
101 major,
102 source_sha256,
103 })
104 }
105
106 #[must_use]
108 pub const fn major(self) -> u16 {
109 self.major
110 }
111
112 #[must_use]
114 pub const fn source_sha256(self) -> [u8; 32] {
115 self.source_sha256
116 }
117
118 pub const fn validate(self, version: SchemaVersion) -> Result<(), SchemaVersionError> {
120 if version.major != self.major {
121 return Err(SchemaVersionError::UnreviewedMajor);
122 }
123 Ok(())
124 }
125}
126
127impl fmt::Debug for ReviewedSchemaMajor {
128 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129 formatter
130 .debug_struct("ReviewedSchemaMajor")
131 .field("major", &self.major)
132 .field("source_sha256", &"[source-locked]")
133 .finish()
134 }
135}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
139pub struct ValidationSchemaHeader<'a> {
140 name: HeaderName<'a>,
141 version: SchemaVersion,
142 evidence: ReviewedSchemaMajor,
143}
144
145impl<'a> ValidationSchemaHeader<'a> {
146 pub fn new(
148 name: &'a str,
149 version: SchemaVersion,
150 evidence: ReviewedSchemaMajor,
151 ) -> Result<Self, SchemaVersionError> {
152 evidence.validate(version)?;
153 let name = HeaderName::new(name).map_err(|_| SchemaVersionError::InvalidHeader)?;
154 RequestHeader::new(name.as_str(), "1.0").map_err(|_| SchemaVersionError::InvalidHeader)?;
155 Ok(Self {
156 name,
157 version,
158 evidence,
159 })
160 }
161
162 #[must_use]
164 pub const fn name(self) -> HeaderName<'a> {
165 self.name
166 }
167
168 #[must_use]
170 pub const fn version(self) -> SchemaVersion {
171 self.version
172 }
173
174 #[must_use]
176 pub const fn evidence(self) -> ReviewedSchemaMajor {
177 self.evidence
178 }
179
180 pub fn with_validation_header<R>(
185 self,
186 scratch: &mut [u8],
187 inspect: impl FnOnce(RequestHeader<'_>) -> R,
188 ) -> Result<R, SchemaVersionError> {
189 let mut scratch = SecretBuffer::new(scratch);
190 let mut len = 0_usize;
191 write_u64(
192 scratch.as_mut_slice(),
193 &mut len,
194 u64::from(self.version.major),
195 SchemaVersionError::OutputTooSmall,
196 )?;
197 crate::buffer::write_byte(
198 scratch.as_mut_slice(),
199 &mut len,
200 b'.',
201 SchemaVersionError::OutputTooSmall,
202 )?;
203 write_u64(
204 scratch.as_mut_slice(),
205 &mut len,
206 u64::from(self.version.minor),
207 SchemaVersionError::OutputTooSmall,
208 )?;
209 let value = core::str::from_utf8(
210 scratch
211 .as_slice()
212 .get(..len)
213 .ok_or(SchemaVersionError::OutputTooSmall)?,
214 )
215 .map_err(|_| SchemaVersionError::InvalidVersion)?;
216 let header = RequestHeader::new(self.name.as_str(), value)
217 .map_err(|_| SchemaVersionError::InvalidHeader)?;
218 Ok(inspect(header))
219 }
220}
221
222fn parse_component(value: &[u8]) -> Result<u16, SchemaVersionError> {
223 if value.is_empty()
224 || (value.len() > 1 && value.first() == Some(&b'0'))
225 || !value.iter().all(u8::is_ascii_digit)
226 {
227 return Err(SchemaVersionError::InvalidVersion);
228 }
229 let mut parsed = 0_u16;
230 for byte in value {
231 let digit = byte
232 .checked_sub(b'0')
233 .ok_or(SchemaVersionError::InvalidVersion)?;
234 parsed = parsed
235 .checked_mul(10)
236 .and_then(|current| current.checked_add(u16::from(digit)))
237 .ok_or(SchemaVersionError::InvalidVersion)?;
238 }
239 Ok(parsed)
240}
241
242#[cfg(test)]
243mod tests;