Add typed schema records and strict version checks
This commit is contained in:
parent
b438dd5201
commit
16defc768a
4 changed files with 3164 additions and 1 deletions
85
crates/lazaret-core/src/codec.rs
Normal file
85
crates/lazaret-core/src/codec.rs
Normal 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)
|
||||
}
|
||||
191
crates/lazaret-core/src/identity.rs
Normal file
191
crates/lazaret-core/src/identity.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
1493
crates/lazaret-core/src/records.rs
Normal file
1493
crates/lazaret-core/src/records.rs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue