hadris_common/types/
file.rs1use core::{fmt, ops::Range};
2
3#[derive(Clone, Copy, PartialEq, Eq)]
20pub struct FixedFilename<const N: usize> {
21 pub data: [u8; N],
22 pub len: usize,
23}
24
25impl<const N: usize> FixedFilename<N> {
26 pub const fn empty() -> Self {
27 Self {
28 data: [0; N],
29 len: 0,
30 }
31 }
32
33 pub const fn with_size(size: usize) -> Self {
34 assert!(size <= N);
35 Self {
36 data: [0; N],
37 len: size,
38 }
39 }
40
41 pub fn as_str(&self) -> &str {
50 core::str::from_utf8(self.as_bytes()).expect("FixedFilename contains invalid UTF-8")
51 }
52
53 pub fn try_as_str(&self) -> Result<&str, core::str::Utf8Error> {
55 core::str::from_utf8(self.as_bytes())
56 }
57
58 pub fn allocate(&mut self, bytes: usize) {
59 let len = self.len;
60 assert!(bytes + len <= N);
61 self.len += bytes;
62 }
64
65 pub fn as_bytes(&self) -> &[u8] {
66 &self.data[0..self.len]
67 }
68
69 pub fn as_bytes_mut(&mut self) -> &mut [u8] {
70 &mut self.data[0..self.len]
71 }
72
73 pub fn truncate(&mut self, new_size: usize) {
74 assert!(new_size <= N);
75 self.len = new_size;
76 }
77
78 pub fn len(&self) -> usize {
79 self.len
80 }
81
82 pub fn is_empty(&self) -> bool {
83 self.len == 0
84 }
85
86 pub fn push_slice(&mut self, slice: &[u8]) -> Range<usize> {
87 assert!(self.len + slice.len() <= self.data.len());
88 let start = self.len;
89 self.len += slice.len();
90 self.data[start..self.len].copy_from_slice(slice);
91 start..self.len
92 }
93
94 pub fn push_byte(&mut self, b: u8) -> usize {
95 assert!(self.len < N);
96 self.data[self.len] = b;
97 self.len += 1;
98 self.len - 1
99 }
100
101 pub fn try_push_slice(&mut self, slice: &[u8]) -> Option<Range<usize>> {
102 if self.len + slice.len() > N {
103 return None;
104 }
105 Some(self.push_slice(slice))
106 }
107
108 pub fn try_push_byte(&mut self, b: u8) -> Option<usize> {
109 if self.len >= N {
110 return None;
111 }
112 Some(self.push_byte(b))
113 }
114
115 pub fn remaining_capacity(&self) -> usize {
116 N - self.len
117 }
118}
119
120impl<const N: usize> fmt::Debug for FixedFilename<N> {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 match self.try_as_str() {
123 Ok(s) => f.debug_tuple("FixedFilename").field(&s).finish(),
124 Err(_) => f
125 .debug_tuple("FixedFilename")
126 .field(&self.as_bytes())
127 .finish(),
128 }
129 }
130}
131
132impl<const N: usize> fmt::Display for FixedFilename<N> {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 match self.try_as_str() {
135 Ok(s) => f.write_str(s),
136 Err(_) => write!(f, "{:?}", self.as_bytes()),
137 }
138 }
139}
140
141impl<const N: usize> From<&[u8]> for FixedFilename<N> {
142 fn from(value: &[u8]) -> Self {
143 assert!(value.len() <= N);
144 let mut str = FixedFilename::with_size(value.len());
145 str.data[0..value.len()].copy_from_slice(value);
146 str
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn try_as_str_rejects_invalid_utf8() {
156 let mut filename = FixedFilename::<10>::with_size(10);
160 filename.as_bytes_mut()[0] = 0xff;
161
162 assert!(filename.try_as_str().is_err());
163 }
164
165 #[test]
166 fn debug_does_not_panic_on_invalid_utf8() {
167 use core::fmt::Write as _;
168 struct Sink;
169 impl core::fmt::Write for Sink {
170 fn write_str(&mut self, _: &str) -> core::fmt::Result {
171 Ok(())
172 }
173 }
174 let mut filename = FixedFilename::<10>::with_size(10);
175 filename.as_bytes_mut()[0] = 0xff;
176 write!(Sink, "{:?}", filename).unwrap();
178 write!(Sink, "{}", filename).unwrap();
179 }
180
181 #[test]
182 fn try_as_str_round_trips_valid_utf8() {
183 let name = FixedFilename::<64>::from(b"readme.txt".as_slice());
184 assert_eq!(name.try_as_str().unwrap(), "readme.txt");
185 assert_eq!(name.as_str(), "readme.txt");
186 }
187
188 #[test]
189 #[should_panic(expected = "invalid UTF-8")]
190 fn as_str_panics_on_invalid_utf8() {
191 let mut filename = FixedFilename::<10>::with_size(10);
192 filename.as_bytes_mut()[0] = 0xff;
193 let _ = filename.as_str();
194 }
195}