Skip to main content

cloud_sdk/
schema.rs

1//! Explicit provider schema-version validation contracts.
2
3use core::fmt;
4
5use cloud_sdk_sanitization::SecretBuffer;
6
7use crate::buffer::write_u64;
8use crate::transport::{HeaderName, RequestHeader};
9
10/// Schema-version validation failure.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum SchemaVersionError {
13    /// Major schema versions must be nonzero.
14    ZeroMajor,
15    /// A schema version was not canonical `major.minor` decimal text.
16    InvalidVersion,
17    /// The selected version differs from the reviewed major version.
18    UnreviewedMajor,
19    /// The validation-only header name is invalid.
20    InvalidHeader,
21    /// Caller scratch cannot hold the complete encoded version.
22    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/// Canonical provider schema version with explicit major and minor parts.
34#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub struct SchemaVersion {
36    major: u16,
37    minor: u16,
38}
39
40impl SchemaVersion {
41    /// Creates a schema version with a nonzero major component.
42    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    /// Parses strict canonical `major.minor` ASCII decimal text.
50    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    /// Returns the major version selected by the account or validation probe.
75    #[must_use]
76    pub const fn major(self) -> u16 {
77        self.major
78    }
79
80    /// Returns the minor schema version.
81    #[must_use]
82    pub const fn minor(self) -> u16 {
83        self.minor
84    }
85}
86
87/// Source-reviewed major version and immutable evidence digest.
88#[derive(Clone, Copy, Eq, PartialEq)]
89pub struct ReviewedSchemaMajor {
90    major: u16,
91    source_sha256: [u8; 32],
92}
93
94impl ReviewedSchemaMajor {
95    /// Binds one nonzero major to the exact reviewed source digest.
96    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    /// Returns the reviewed major version.
107    #[must_use]
108    pub const fn major(self) -> u16 {
109        self.major
110    }
111
112    /// Returns the exact SHA-256 source-lock evidence.
113    #[must_use]
114    pub const fn source_sha256(self) -> [u8; 32] {
115        self.source_sha256
116    }
117
118    /// Rejects a version from any unreviewed major line.
119    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/// Explicit validation-only schema override header.
138#[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    /// Creates an override only when its major matches reviewed evidence.
147    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    /// Returns the validation-only header name.
163    #[must_use]
164    pub const fn name(self) -> HeaderName<'a> {
165        self.name
166    }
167
168    /// Returns the exact reviewed schema version.
169    #[must_use]
170    pub const fn version(self) -> SchemaVersion {
171        self.version
172    }
173
174    /// Returns the evidence binding used to admit this override.
175    #[must_use]
176    pub const fn evidence(self) -> ReviewedSchemaMajor {
177        self.evidence
178    }
179
180    /// Builds the public validation header only for the duration of `inspect`.
181    ///
182    /// This deliberately named method keeps the override out of default
183    /// production request construction.
184    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;