Add typed schema records and strict version checks

This commit is contained in:
ms2k 2026-07-13 04:00:50 +09:00
commit 16defc768a
6 changed files with 3164 additions and 1 deletions

View file

@ -5,5 +5,9 @@ edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
postcard.workspace = true
serde.workspace = true
[lints]
workspace = true

View file

@ -0,0 +1,85 @@
//! Strict bounded Postcard structure checks shared by schema-record persistence and tests.
use std::fmt;
use serde::Serialize;
use serde::de::DeserializeOwned;
pub const MAX_SCHEMA_RECORD_BYTES: usize = 1024 * 1024;
#[derive(Debug)]
pub enum CodecError {
RecordTooLarge { length: usize, maximum: usize },
TrailingPayload,
NonMinimalPayload,
MalformedPayload(postcard::Error),
}
impl fmt::Display for CodecError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RecordTooLarge { length, maximum } => {
write!(formatter, "schema record length {length} exceeds {maximum}")
}
Self::TrailingPayload => {
formatter.write_str("schema record has trailing payload bytes")
}
Self::NonMinimalPayload => formatter
.write_str("schema record does not use the minimal Postcard representation"),
Self::MalformedPayload(error) => write!(formatter, "malformed schema payload: {error}"),
}
}
}
impl std::error::Error for CodecError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::MalformedPayload(error) => Some(error),
_ => None,
}
}
}
/// Encodes one schema value using Postcard's minimal representation.
///
/// # Errors
///
/// Returns an error when serialization fails or the encoded value exceeds the generic limit.
pub fn encode<T: Serialize>(record: &T) -> Result<Vec<u8>, CodecError> {
let encoded = postcard::to_stdvec(record).map_err(CodecError::MalformedPayload)?;
if encoded.len() > MAX_SCHEMA_RECORD_BYTES {
return Err(CodecError::RecordTooLarge {
length: encoded.len(),
maximum: MAX_SCHEMA_RECORD_BYTES,
});
}
Ok(encoded)
}
/// Decodes one complete, minimally encoded schema value after enforcing the generic size limit.
///
/// # Errors
///
/// Returns an error for oversized, malformed, trailing, or non-minimal Postcard input.
pub fn decode<T>(encoded: &[u8]) -> Result<T, CodecError>
where
T: DeserializeOwned + Serialize,
{
if encoded.len() > MAX_SCHEMA_RECORD_BYTES {
return Err(CodecError::RecordTooLarge {
length: encoded.len(),
maximum: MAX_SCHEMA_RECORD_BYTES,
});
}
let (record, remainder) =
postcard::take_from_bytes(encoded).map_err(CodecError::MalformedPayload)?;
if !remainder.is_empty() {
return Err(CodecError::TrailingPayload);
}
let minimal = postcard::to_stdvec(&record).map_err(CodecError::MalformedPayload)?;
if minimal != encoded {
return Err(CodecError::NonMinimalPayload);
}
Ok(record)
}

View file

@ -0,0 +1,191 @@
use serde::{Deserialize, Serialize};
pub const OPAQUE_ID_BYTES: usize = 32;
pub const SHA256_BYTES: usize = 32;
macro_rules! opaque_identity {
($($name:ident),+ $(,)?) => {
$(
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct $name([u8; OPAQUE_ID_BYTES]);
impl $name {
#[must_use]
pub const fn new(bytes: [u8; OPAQUE_ID_BYTES]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; OPAQUE_ID_BYTES] {
&self.0
}
}
)+
};
}
macro_rules! digest_identity {
($($name:ident),+ $(,)?) => {
$(
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct $name([u8; SHA256_BYTES]);
impl $name {
#[must_use]
pub const fn new(bytes: [u8; SHA256_BYTES]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; SHA256_BYTES] {
&self.0
}
}
)+
};
}
macro_rules! numeric_identity {
($($name:ident($inner:ty)),+ $(,)?) => {
$(
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct $name($inner);
impl $name {
#[must_use]
pub const fn new(value: $inner) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> $inner {
self.0
}
}
)+
};
}
opaque_identity!(
DomainId,
CoordinatorId,
NetworkSidecarId,
SandboxId,
ClientId,
SessionId,
SessionAnchorId,
RequestId,
PreparedArtifactId,
PrepareOperationId,
ProtocolCredentialId,
CapabilityId,
ContainerId,
PolicyId,
);
digest_identity!(
ConfigurationFingerprint,
DerivedPolicyDigest,
ReadinessFingerprint,
BundleDigest,
SelectedManifestDigest,
IdMapDigest,
CertificateDigest,
TrustManifestDigest,
RequestDigest,
StreamDigest,
);
numeric_identity!(
GenerationId(u64),
ProcessId(u32),
ProcessStartTime(u64),
GuestProcessGroupId(u32),
FileDescriptor(i32),
DeviceId(u64),
InodeId(u64),
MountId(u64),
);
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct HostPath(Vec<u8>);
impl HostPath {
#[must_use]
pub const fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct SourceIdentity(Vec<u8>);
impl SourceIdentity {
#[must_use]
pub const fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ProcessIdentity {
pub pid: ProcessId,
pub start_time: ProcessStartTime,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct NamespaceIdentity {
pub device: DeviceId,
pub inode: InodeId,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct MountIdentity {
pub mount_id: MountId,
pub device: DeviceId,
pub inode: InodeId,
}
#[cfg(test)]
mod tests {
use std::any::TypeId;
use std::collections::HashSet;
use super::{
ContainerId, CoordinatorId, DomainId, GenerationId, MountIdentity, NamespaceIdentity,
NetworkSidecarId, PreparedArtifactId, ProcessIdentity, RequestId, SandboxId, SessionId,
};
#[test]
fn identity_categories_are_nominally_distinct() {
let identities = [
TypeId::of::<DomainId>(),
TypeId::of::<GenerationId>(),
TypeId::of::<CoordinatorId>(),
TypeId::of::<NetworkSidecarId>(),
TypeId::of::<SandboxId>(),
TypeId::of::<SessionId>(),
TypeId::of::<RequestId>(),
TypeId::of::<PreparedArtifactId>(),
TypeId::of::<ContainerId>(),
TypeId::of::<ProcessIdentity>(),
TypeId::of::<NamespaceIdentity>(),
TypeId::of::<MountIdentity>(),
];
assert_eq!(
identities.into_iter().collect::<HashSet<_>>().len(),
identities.len()
);
}
}

View file

@ -1,5 +1,9 @@
#![doc = "Shared production interfaces for Lazaret processes."]
pub mod codec;
pub mod identity;
pub mod records;
pub const PROTOCOL_NAME: &str = "lazaret";
#[cfg(test)]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff