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>;
49 fn into_bytes(self) -> Result<Vec<u8>, CodecError> {
51 Ok(self.to_bytes()?.into_owned())
52 }
53 fn from_bytes(bytes: Cow<'_, [u8]>) -> Result<Self, CodecError>;
55 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 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 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}