1use std::borrow::Cow;
3
4#[cfg(test)]
5mod tests;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum Bound {
14 Unbounded,
16 Bounded {
18 max_size: u32,
20 is_fixed_size: bool,
22 },
23}
24
25impl Bound {
26 pub(crate) const fn check(self, bytes: &[u8]) -> Result<(), CodecError> {
27 if let Self::Bounded {
28 max_size,
29 is_fixed_size,
30 } = self
31 && !(bytes.len() <= max_size as usize
32 && (!is_fixed_size || bytes.len() == max_size as usize))
33 {
34 return Err(CodecError::Bounds);
35 }
36 Ok(())
37 }
38}
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
47pub enum CodecError {
48 #[error("invalid encoding: {0}")]
50 Invalid(&'static str),
51 #[error("record violates codec bound")]
53 Bounds,
54}
55
56pub trait Storable: Sized {
64 const BOUND: Bound;
66 fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError>;
71 fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
76 Ok(self.to_bytes()?.into_owned())
77 }
78 fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError>;
83 fn check_bounds(bytes: &[u8]) -> Result<(), CodecError> {
88 Self::BOUND.check(bytes)
89 }
90 fn to_bytes_checked(&self) -> Result<Cow<'_, [u8]>, CodecError> {
95 let bytes = self.to_bytes()?;
96 Self::BOUND.check(&bytes)?;
97 Ok(bytes)
98 }
99 fn into_bytes_checked(self) -> Result<Vec<u8>, CodecError> {
104 let bytes = self.into_bytes()?;
105 Self::BOUND.check(&bytes)?;
106 Ok(bytes)
107 }
108}
109
110macro_rules! integer {
111 ($($ty:ty),* $(,)?) => {$ (
112 impl Storable for $ty {
113 #[expect(clippy::cast_possible_truncation, reason = "primitive widths are at most 16 bytes")]
114 const BOUND: Bound = Bound::Bounded { max_size: size_of::<Self>() as u32, is_fixed_size: true };
115 fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> { Ok(Cow::Owned(self.to_le_bytes().to_vec())) }
116 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"))?)) }
117 }
118 )*};
119}
120integer!(u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64);
121impl Storable for u8 {
122 const BOUND: Bound = Bound::Bounded {
123 max_size: 1,
124 is_fixed_size: true,
125 };
126 fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
127 Ok(Cow::Borrowed(std::slice::from_ref(self)))
128 }
129 fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
130 Ok(Self::from_le_bytes(
131 bytes
132 .as_ref()
133 .try_into()
134 .map_err(|_| CodecError::Invalid("integer width"))?,
135 ))
136 }
137}
138impl Storable for () {
139 const BOUND: Bound = Bound::Bounded {
140 max_size: 0,
141 is_fixed_size: true,
142 };
143 fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
144 Ok(Cow::Borrowed(&[]))
145 }
146 fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
147 Self::check_bounds(&bytes)
148 }
149}
150impl Storable for bool {
151 const BOUND: Bound = Bound::Bounded {
152 max_size: 1,
153 is_fixed_size: true,
154 };
155 fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
156 Ok(Cow::Borrowed(if *self { &[1] } else { &[0] }))
157 }
158 fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
159 match bytes.as_ref() {
160 [0] => Ok(false),
161 [1] => Ok(true),
162 _ => Err(CodecError::Invalid("boolean encoding")),
163 }
164 }
165}
166impl Storable for Vec<u8> {
167 const BOUND: Bound = Bound::Unbounded;
168 fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
169 Ok(Cow::Borrowed(self))
170 }
171 fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
172 Ok(self)
173 }
174 fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
175 Ok(bytes.into_owned())
176 }
177}
178impl Storable for String {
179 const BOUND: Bound = Bound::Unbounded;
180 fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
181 Ok(Cow::Borrowed(self.as_bytes()))
182 }
183 fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
184 Ok(self.into_bytes())
185 }
186 fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
187 Self::from_utf8(bytes.into_owned()).map_err(|_| CodecError::Invalid("UTF-8 record"))
188 }
189}
190impl<const N: usize> Storable for [u8; N] {
191 #[expect(
192 clippy::cast_possible_truncation,
193 reason = "the const assertion admits only u32 widths"
194 )]
195 const BOUND: Bound = Bound::Bounded {
196 max_size: {
197 assert!(N <= u32::MAX as usize);
198 N as u32
199 },
200 is_fixed_size: true,
201 };
202 fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
203 Ok(Cow::Borrowed(self))
204 }
205 fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
206 bytes
207 .as_ref()
208 .try_into()
209 .map_err(|_| CodecError::Invalid("byte array width"))
210 }
211}
212impl<T: Storable> Storable for Option<T> {
213 const BOUND: Bound = match T::BOUND {
214 Bound::Unbounded => Bound::Unbounded,
215 Bound::Bounded { max_size, .. } => Bound::Bounded {
216 max_size: max_size.checked_add(1).expect("optional bound overflow"),
217 is_fixed_size: false,
218 },
219 };
220 fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError> {
221 let mut bytes = vec![u8::from(self.is_some())];
222 if let Some(value) = self {
223 bytes.extend_from_slice(&value.to_bytes_checked()?);
224 }
225 Ok(Cow::Owned(bytes))
226 }
227 fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError> {
228 match bytes.as_ref().split_first() {
229 Some((0, [])) => Ok(None),
230 Some((1, payload)) => {
231 T::check_bounds(payload)?;
232 T::from_bytes(Cow::Borrowed(payload)).map(Some)
233 }
234 _ => Err(CodecError::Invalid("optional record tag")),
235 }
236 }
237}