use core::fmt;
use cloud_sdk_sanitization::SecretBuffer;
use crate::buffer::write_u64;
use crate::transport::{HeaderName, RequestHeader};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SchemaVersionError {
ZeroMajor,
InvalidVersion,
UnreviewedMajor,
InvalidHeader,
OutputTooSmall,
}
impl_static_error!(SchemaVersionError,
Self::ZeroMajor => "schema major version must be nonzero",
Self::InvalidVersion => "schema version is not canonical major.minor text",
Self::UnreviewedMajor => "schema version differs from the reviewed major",
Self::InvalidHeader => "schema validation header is invalid",
Self::OutputTooSmall => "schema version output is too small",
);
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SchemaVersion {
major: u16,
minor: u16,
}
impl SchemaVersion {
pub const fn new(major: u16, minor: u16) -> Result<Self, SchemaVersionError> {
if major == 0 {
return Err(SchemaVersionError::ZeroMajor);
}
Ok(Self { major, minor })
}
pub fn parse(value: &[u8]) -> Result<Self, SchemaVersionError> {
let dot = value
.iter()
.position(|byte| *byte == b'.')
.ok_or(SchemaVersionError::InvalidVersion)?;
if value
.get(dot.saturating_add(1)..)
.is_none_or(|part| part.is_empty())
|| value.get(..dot).is_none_or(|part| part.is_empty())
|| value
.get(dot.saturating_add(1)..)
.is_some_and(|part| part.contains(&b'.'))
{
return Err(SchemaVersionError::InvalidVersion);
}
let major = parse_component(value.get(..dot).ok_or(SchemaVersionError::InvalidVersion)?)?;
let minor = parse_component(
value
.get(dot.saturating_add(1)..)
.ok_or(SchemaVersionError::InvalidVersion)?,
)?;
Self::new(major, minor)
}
#[must_use]
pub const fn major(self) -> u16 {
self.major
}
#[must_use]
pub const fn minor(self) -> u16 {
self.minor
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct ReviewedSchemaMajor {
major: u16,
source_sha256: [u8; 32],
}
impl ReviewedSchemaMajor {
pub const fn new(major: u16, source_sha256: [u8; 32]) -> Result<Self, SchemaVersionError> {
if major == 0 {
return Err(SchemaVersionError::ZeroMajor);
}
Ok(Self {
major,
source_sha256,
})
}
#[must_use]
pub const fn major(self) -> u16 {
self.major
}
#[must_use]
pub const fn source_sha256(self) -> [u8; 32] {
self.source_sha256
}
pub const fn validate(self, version: SchemaVersion) -> Result<(), SchemaVersionError> {
if version.major != self.major {
return Err(SchemaVersionError::UnreviewedMajor);
}
Ok(())
}
}
impl fmt::Debug for ReviewedSchemaMajor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ReviewedSchemaMajor")
.field("major", &self.major)
.field("source_sha256", &"[source-locked]")
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ValidationSchemaHeader<'a> {
name: HeaderName<'a>,
version: SchemaVersion,
evidence: ReviewedSchemaMajor,
}
impl<'a> ValidationSchemaHeader<'a> {
pub fn new(
name: &'a str,
version: SchemaVersion,
evidence: ReviewedSchemaMajor,
) -> Result<Self, SchemaVersionError> {
evidence.validate(version)?;
let name = HeaderName::new(name).map_err(|_| SchemaVersionError::InvalidHeader)?;
RequestHeader::new(name.as_str(), "1.0").map_err(|_| SchemaVersionError::InvalidHeader)?;
Ok(Self {
name,
version,
evidence,
})
}
#[must_use]
pub const fn name(self) -> HeaderName<'a> {
self.name
}
#[must_use]
pub const fn version(self) -> SchemaVersion {
self.version
}
#[must_use]
pub const fn evidence(self) -> ReviewedSchemaMajor {
self.evidence
}
pub fn with_validation_header<R>(
self,
scratch: &mut [u8],
inspect: impl FnOnce(RequestHeader<'_>) -> R,
) -> Result<R, SchemaVersionError> {
let mut scratch = SecretBuffer::new(scratch);
let mut len = 0_usize;
write_u64(
scratch.as_mut_slice(),
&mut len,
u64::from(self.version.major),
SchemaVersionError::OutputTooSmall,
)?;
crate::buffer::write_byte(
scratch.as_mut_slice(),
&mut len,
b'.',
SchemaVersionError::OutputTooSmall,
)?;
write_u64(
scratch.as_mut_slice(),
&mut len,
u64::from(self.version.minor),
SchemaVersionError::OutputTooSmall,
)?;
let value = core::str::from_utf8(
scratch
.as_slice()
.get(..len)
.ok_or(SchemaVersionError::OutputTooSmall)?,
)
.map_err(|_| SchemaVersionError::InvalidVersion)?;
let header = RequestHeader::new(self.name.as_str(), value)
.map_err(|_| SchemaVersionError::InvalidHeader)?;
Ok(inspect(header))
}
}
fn parse_component(value: &[u8]) -> Result<u16, SchemaVersionError> {
if value.is_empty()
|| (value.len() > 1 && value.first() == Some(&b'0'))
|| !value.iter().all(u8::is_ascii_digit)
{
return Err(SchemaVersionError::InvalidVersion);
}
let mut parsed = 0_u16;
for byte in value {
let digit = byte
.checked_sub(b'0')
.ok_or(SchemaVersionError::InvalidVersion)?;
parsed = parsed
.checked_mul(10)
.and_then(|current| current.checked_add(u16::from(digit)))
.ok_or(SchemaVersionError::InvalidVersion)?;
}
Ok(parsed)
}
#[cfg(test)]
mod tests;