1use std::borrow::Cow;
3
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum Bound {
11 Unbounded,
13 Bounded {
15 max_size: u32,
17 is_fixed_size: bool,
19 },
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
29pub enum CodecError {
30 #[error("invalid encoding: {0}")]
32 Invalid(&'static str),
33 #[error("record violates codec bound")]
35 Bounds,
36}
37
38pub trait Storable: Sized {
45 const BOUND: Bound;
47 fn to_bytes(&self) -> Result<Cow<'_, [u8]>, CodecError>;
52 fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
57 Ok(self.to_bytes()?.into_owned())
58 }
59 fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError>;
64 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 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 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}