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    ///
49    /// # Errors
50    /// Returns a codec error if the value cannot be represented by the implementation.
51    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError>;
52    /// Consume a value into its encoding.
53    ///
54    /// # Errors
55    /// Returns the codec error from encoding this value.
56    fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
57        Ok(self.to_bytes()?.into_owned())
58    }
59    /// Decode application bytes, returning an error for invalid encodings.
60    ///
61    /// # Errors
62    /// Returns a codec error if the bytes are not a valid value of this type.
63    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError>;
64    /// Enforce the declared maximum and exact width where applicable.
65    ///
66    /// # Errors
67    /// Returns `CodecError::Bounds` when the declared maximum or exact width is violated.
68    fn check_bounds(bytes: &[u8]) -> Result<(), CodecError> {
69        if let Bound::Bounded {
70            max_size,
71            is_fixed_size,
72        } = Self::BOUND
73            && !(bytes.len() <= max_size as usize
74                && (!is_fixed_size || bytes.len() == max_size as usize))
75        {
76            return Err(CodecError::Bounds);
77        }
78        Ok(())
79    }
80    /// Encode and enforce the declared bound.
81    ///
82    /// # Errors
83    /// Returns an encoding error or `CodecError::Bounds` if the encoded bytes violate the declared bound.
84    fn to_bytes_checked(&self) -> Result<Cow<'_, [u8]>, CodecError> {
85        let bytes = self.to_bytes()?;
86        Self::check_bounds(&bytes)?;
87        Ok(bytes)
88    }
89    /// Consume, encode and enforce the declared bound.
90    ///
91    /// # Errors
92    /// Returns an encoding error or `CodecError::Bounds` if the encoded bytes violate the declared bound.
93    fn into_bytes_checked(self) -> Result<Vec<u8>, CodecError> {
94        let bytes = self.into_bytes()?;
95        Self::check_bounds(&bytes)?;
96        Ok(bytes)
97    }
98}
99
100macro_rules! integer {
101    ($($ty:ty),* $(,)?) => {$ (
102        impl Storable for $ty {
103            #[expect(clippy::cast_possible_truncation, reason = "primitive widths are at most 16 bytes")]
104            const BOUND: Bound = Bound::Bounded { max_size: size_of::<Self>() as u32, is_fixed_size: true };
105            fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> { Ok(Cow::Owned(self.to_le_bytes().to_vec())) }
106            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"))?)) }
107        }
108    )*};
109}
110integer!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64);
111impl Storable for () {
112    const BOUND: Bound = Bound::Bounded {
113        max_size: 0,
114        is_fixed_size: true,
115    };
116    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
117        Ok(Cow::Borrowed(&[]))
118    }
119    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
120        Self::check_bounds(&bytes)
121    }
122}
123impl Storable for bool {
124    const BOUND: Bound = Bound::Bounded {
125        max_size: 1,
126        is_fixed_size: true,
127    };
128    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
129        Ok(Cow::Owned(vec![u8::from(*self)]))
130    }
131    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
132        match bytes.as_ref() {
133            [0] => Ok(false),
134            [1] => Ok(true),
135            _ => Err(CodecError::Invalid("boolean encoding")),
136        }
137    }
138}
139impl Storable for Vec<u8> {
140    const BOUND: Bound = Bound::Unbounded;
141    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
142        Ok(Cow::Borrowed(self))
143    }
144    fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
145        Ok(self)
146    }
147    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
148        Ok(bytes.into_owned())
149    }
150}
151impl Storable for String {
152    const BOUND: Bound = Bound::Unbounded;
153    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
154        Ok(Cow::Borrowed(self.as_bytes()))
155    }
156    fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
157        Ok(self.into_bytes())
158    }
159    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
160        Self::from_utf8(bytes.into_owned()).map_err(|_| CodecError::Invalid("UTF-8 record"))
161    }
162}
163impl<const N: usize> Storable for [u8; N] {
164    #[expect(
165        clippy::cast_possible_truncation,
166        reason = "the const assertion admits only u32 widths"
167    )]
168    const BOUND: Bound = Bound::Bounded {
169        max_size: {
170            assert!(N <= u32::MAX as usize);
171            N as u32
172        },
173        is_fixed_size: true,
174    };
175    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
176        Ok(Cow::Borrowed(self))
177    }
178    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
179        bytes
180            .as_ref()
181            .try_into()
182            .map_err(|_| CodecError::Invalid("byte array width"))
183    }
184}
185impl<T: Storable> Storable for Option<T> {
186    const BOUND: Bound = match T::BOUND {
187        Bound::Unbounded => Bound::Unbounded,
188        Bound::Bounded { max_size, .. } => Bound::Bounded {
189            max_size: max_size.checked_add(1).expect("optional bound overflow"),
190            is_fixed_size: false,
191        },
192    };
193    fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
194        let mut bytes = vec![u8::from(self.is_some())];
195        if let Some(value) = self {
196            bytes.extend_from_slice(&value.to_bytes_checked()?);
197        }
198        Ok(Cow::Owned(bytes))
199    }
200    fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
201        match bytes.as_ref().split_first() {
202            Some((0, [])) => Ok(None),
203            Some((1, payload)) => {
204                T::check_bounds(payload)?;
205                T::from_bytes(Cow::Borrowed(payload)).map(Some)
206            }
207            _ => Err(CodecError::Invalid("optional record tag")),
208        }
209    }
210}