Skip to main content

craft_codec/
config.rs

1use crate::{Error, Result, error::buffer_len};
2
3/// Version of the headerless frame data format.
4pub const FORMAT_VERSION: u8 = 1;
5/// Metadata schema introduced in craft-codec 0.2.0. Older schemas are unsupported.
6pub const METADATA_SCHEMA_VERSION: u16 = 2;
7/// AES-GCM authentication tag length in bytes.
8pub const TAG_LEN: usize = 16;
9
10/// Upper bound on raw bytes covered by the AES profile's frame slots per key.
11/// This is a library usage policy, not a claim of 128-bit security at this limit.
12pub const AES_MAX_BYTES: u64 = 1 << 36;
13/// Maximum number of AES-GCM frame slots per object/key.
14pub const AES_MAX_FRAMES: u64 = 1 << 32;
15/// Maximum configured raw frame length for the AES-GCM profile, 16 MiB.
16pub const AES_MAX_FRAME_LEN: u64 = 1 << 24;
17
18/// Logical frame boundaries, before compression or encryption.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum Framing {
22    /// All frames have this size, except a possibly shorter final frame.
23    Fixed(u64),
24    /// Each nonempty frame may have any size up to this limit.
25    Variable(u64),
26}
27
28impl Framing {
29    /// Maximum number of raw bytes in a frame.
30    pub const fn max_frame_len(self) -> u64 {
31        match self {
32            Self::Fixed(size) | Self::Variable(size) => size,
33        }
34    }
35}
36
37/// Compression identifier in trusted external metadata.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[repr(u8)]
41pub enum Compression {
42    /// Store raw bytes.
43    None = 0,
44    /// Independent LZ4 blocks, retained only when strictly smaller.
45    Lz4 = 1,
46}
47
48/// Complete encryption parameters stored in trusted external metadata.
49#[derive(Clone, Eq, PartialEq)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51pub enum Encryption {
52    /// No authentication or encryption.
53    None,
54    /// AES-256-GCM with a 96-bit frame-index nonce and a 16-byte suffix tag.
55    Aes256Gcm {
56        /// Independent secret key for this immutable object, serialized with metadata.
57        key: [u8; 32],
58    },
59}
60
61impl std::fmt::Debug for Encryption {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            Self::None => f.write_str("None"),
65            Self::Aes256Gcm { .. } => f.write_str("Aes256Gcm { key: [REDACTED] }"),
66        }
67    }
68}
69
70impl Encryption {
71    /// Stored suffix length in bytes.
72    pub const fn tag_len(&self) -> usize {
73        match self {
74            Self::None => 0,
75            Self::Aes256Gcm { .. } => TAG_LEN,
76        }
77    }
78}
79
80/// Validated object-wide settings, including all codec initialization parameters.
81#[derive(Clone, Debug, Eq, PartialEq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize))]
83pub struct Config {
84    framing: Framing,
85    compression: Compression,
86    encryption: Encryption,
87}
88
89#[cfg(feature = "serde")]
90impl<'de> serde::Deserialize<'de> for Config {
91    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
92    where
93        D: serde::Deserializer<'de>,
94    {
95        #[derive(serde::Deserialize)]
96        struct Fields {
97            framing: Framing,
98            compression: Compression,
99            encryption: Encryption,
100        }
101
102        let fields = <Fields as serde::Deserialize>::deserialize(deserializer)?;
103        Self::new(fields.framing, fields.compression, fields.encryption)
104            .map_err(serde::de::Error::custom)
105    }
106}
107
108impl Config {
109    /// Validate a profile, including platform, codec and AES usage limits.
110    /// Metadata for disabled codecs may still be inspected with this type.
111    pub fn new(framing: Framing, compression: Compression, encryption: Encryption) -> Result<Self> {
112        let max = framing.max_frame_len();
113        if max == 0 {
114            return Err(Error::InvalidFrameSize);
115        }
116        buffer_len(
117            max.checked_add(encryption.tag_len() as u64)
118                .ok_or(Error::Overflow)?,
119        )?;
120        // LZ4 block offsets/tables use 32-bit positions. Leave room for its bound.
121        if compression == Compression::Lz4 && max > 0x7e00_0000 {
122            return Err(Error::InvalidFrameSize);
123        }
124        if matches!(encryption, Encryption::Aes256Gcm { .. }) && max > AES_MAX_FRAME_LEN {
125            return Err(Error::InvalidFrameSize);
126        }
127        Ok(Self {
128            framing,
129            compression,
130            encryption,
131        })
132    }
133
134    /// Logical framing rule.
135    pub const fn framing(&self) -> Framing {
136        self.framing
137    }
138    /// Compression format.
139    pub const fn compression(&self) -> Compression {
140        self.compression
141    }
142    /// Encryption format.
143    pub const fn encryption(&self) -> &Encryption {
144        &self.encryption
145    }
146    /// Maximum raw length, in bytes.
147    pub const fn max_frame_len(&self) -> u64 {
148        self.framing.max_frame_len()
149    }
150
151    /// Maximum frame slots for this profile. AES reserves the configured
152    /// maximum length for each slot, even if an actual frame is shorter.
153    pub fn max_frames(&self) -> u64 {
154        match &self.encryption {
155            Encryption::None => u64::MAX,
156            Encryption::Aes256Gcm { .. } => AES_MAX_BYTES
157                .checked_div(self.max_frame_len())
158                .map_or(0, |frames| AES_MAX_FRAMES.min(frames)),
159        }
160    }
161
162    pub(crate) fn validate_frame(&self, index: u64, raw: u64, payload: u64) -> Result<()> {
163        if raw == 0 || raw > self.max_frame_len() {
164            return Err(Error::InvalidFrameSize);
165        }
166        if payload == 0
167            || payload > raw
168            || (self.compression == Compression::None && payload != raw)
169        {
170            return Err(Error::InvalidMetadata);
171        }
172        if index >= self.max_frames() {
173            return Err(Error::UsageLimit);
174        }
175        Ok(())
176    }
177}