Skip to main content

ic_storage/
storable.rs

1//! Storage-owned record codec contract. Application records normally use IC Wire.
2use std::borrow::Cow;
3
4///
5/// Bound
6///
7/// Maximum encoded record size, with an optional exact-width requirement.
8///
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum Bound {
11    /// No type-level bound; collection resource limits still apply.
12    Unbounded,
13    /// A finite encoded-size limit.
14    Bounded {
15        /// Maximum encoded byte count.
16        max_size: u32,
17        /// Whether every encoding must have exactly the maximum length.
18        is_fixed_size: bool,
19    },
20}
21
22///
23/// CodecError
24///
25/// Invalid application encoding or a violation of its declared byte bound.
26///
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
29pub enum CodecError {
30    /// The payload does not satisfy the codec.
31    #[error("invalid encoding: {0}")]
32    Invalid(&'static str),
33    /// Encoded bytes violate the declared maximum or fixed width.
34    #[error("record violates codec bound")]
35    Bounds,
36}
37
38///
39/// Storable
40///
41/// Encode and decode a collection record. The caller owns its schema; Storage
42/// enforces its declared byte bound before writes and after reading bytes.
43///
44pub trait Storable: Sized {
45    /// Record size contract.
46    const BOUND: Bound;
47    /// Borrow or allocate an encoding.
48    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError>;
49    /// Consume a value into its encoding.
50    fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
51        Ok(self.to_bytes()?.into_owned())
52    }
53    /// Decode application bytes, returning an error for invalid encodings.
54    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError>;
55    /// Enforce the declared maximum and exact width where applicable.
56    fn check_bounds(bytes: &[u8]) -> Result<(), CodecError> {
57        if let Bound::Bounded {
58            max_size,
59            is_fixed_size,
60        } = Self::BOUND
61            && !(bytes.len() <= max_size as usize
62                && (!is_fixed_size || bytes.len() == max_size as usize))
63        {
64            return Err(CodecError::Bounds);
65        }
66        Ok(())
67    }
68    /// Encode and enforce the declared bound.
69    fn to_bytes_checked(&self) -> Result<Cow<'_, [u8]>, CodecError> {
70        let bytes = self.to_bytes()?;
71        Self::check_bounds(&bytes)?;
72        Ok(bytes)
73    }
74    /// Consume, encode and enforce the declared bound.
75    fn into_bytes_checked(self) -> Result<Vec<u8>, CodecError> {
76        let bytes = self.into_bytes()?;
77        Self::check_bounds(&bytes)?;
78        Ok(bytes)
79    }
80}
81
82macro_rules! integer {
83    ($($ty:ty),* $(,)?) => {$ (
84        impl Storable for $ty {
85            #[expect(clippy::cast_possible_truncation, reason = "primitive widths are at most 16 bytes")]
86            const BOUND: Bound = Bound::Bounded { max_size: size_of::<Self>() as u32, is_fixed_size: true };
87            fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> { Ok(Cow::Owned(self.to_le_bytes().to_vec())) }
88            fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> { Ok(Self::from_le_bytes(bytes.as_ref().try_into().map_err(|_| CodecError::Invalid("integer width"))?)) }
89        }
90    )*};
91}
92integer!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64);
93impl Storable for () {
94    const BOUND: Bound = Bound::Bounded {
95        max_size: 0,
96        is_fixed_size: true,
97    };
98    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
99        Ok(Cow::Borrowed(&[]))
100    }
101    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
102        Self::check_bounds(&bytes)
103    }
104}
105impl Storable for bool {
106    const BOUND: Bound = Bound::Bounded {
107        max_size: 1,
108        is_fixed_size: true,
109    };
110    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
111        Ok(Cow::Owned(vec![u8::from(*self)]))
112    }
113    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
114        match bytes.as_ref() {
115            [0] => Ok(false),
116            [1] => Ok(true),
117            _ => Err(CodecError::Invalid("boolean encoding")),
118        }
119    }
120}
121impl Storable for Vec<u8> {
122    const BOUND: Bound = Bound::Unbounded;
123    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
124        Ok(Cow::Borrowed(self))
125    }
126    fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
127        Ok(self)
128    }
129    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
130        Ok(bytes.into_owned())
131    }
132}
133impl Storable for String {
134    const BOUND: Bound = Bound::Unbounded;
135    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
136        Ok(Cow::Borrowed(self.as_bytes()))
137    }
138    fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
139        Ok(self.into_bytes())
140    }
141    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
142        Self::from_utf8(bytes.into_owned()).map_err(|_| CodecError::Invalid("UTF-8 record"))
143    }
144}
145impl<const N: usize> Storable for [u8; N] {
146    #[expect(
147        clippy::cast_possible_truncation,
148        reason = "the const assertion admits only u32 widths"
149    )]
150    const BOUND: Bound = Bound::Bounded {
151        max_size: {
152            assert!(N <= u32::MAX as usize);
153            N as u32
154        },
155        is_fixed_size: true,
156    };
157    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
158        Ok(Cow::Borrowed(self))
159    }
160    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
161        bytes
162            .as_ref()
163            .try_into()
164            .map_err(|_| CodecError::Invalid("byte array width"))
165    }
166}
167impl<T: Storable> Storable for Option<T> {
168    const BOUND: Bound = match T::BOUND {
169        Bound::Unbounded => Bound::Unbounded,
170        Bound::Bounded { max_size, .. } => Bound::Bounded {
171            max_size: max_size.checked_add(1).expect("optional bound overflow"),
172            is_fixed_size: false,
173        },
174    };
175    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
176        let mut bytes = vec![u8::from(self.is_some())];
177        if let Some(value) = self {
178            bytes.extend_from_slice(&value.to_bytes_checked()?);
179        }
180        Ok(Cow::Owned(bytes))
181    }
182    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
183        match bytes.as_ref().split_first() {
184            Some((0, [])) => Ok(None),
185            Some((1, payload)) => {
186                T::check_bounds(payload)?;
187                T::from_bytes(Cow::Borrowed(payload)).map(Some)
188            }
189            _ => Err(CodecError::Invalid("optional record tag")),
190        }
191    }
192}