Skip to main content

craft_codec/
config.rs

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