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