1#![cfg_attr(not(feature = "std"), no_std)]
2
3use core::{
4 cmp::Ordering,
5 fmt::{self, Write},
6};
7
8#[cfg(feature = "std")]
9use std::string::{String, ToString};
10
11#[derive(Debug, Clone, Copy)]
13pub enum FccConversionError {
14 TooLong,
16 TooShort,
18 InvalidChar,
20}
21
22impl FccConversionError {
23 pub fn description(&self) -> &str {
24 match self {
25 Self::TooLong => "four char code is too long",
26 Self::TooShort => "four char code is too short",
27 Self::InvalidChar => "invalid char in four char code",
28 }
29 }
30}
31
32impl fmt::Display for FccConversionError {
33 #[inline]
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 fmt::Display::fmt(FccConversionError::description(self), f)
36 }
37}
38
39#[cfg(feature = "std")]
40impl ::std::error::Error for FccConversionError {
41 #[inline]
42 fn description(&self) -> &str {
43 FccConversionError::description(self)
44 }
45}
46
47type Result<T> = core::result::Result<T, FccConversionError>;
48
49#[repr(transparent)]
51#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
52pub struct FourCharCode(u32);
53
54#[repr(transparent)]
56pub struct Display(u32);
57
58const fn from_bytes(mut bytes: [u8; 4]) -> Result<FourCharCode> {
59 let mut null_streak = true;
60
61 let mut i = 3usize;
62 loop {
63 let mut c = bytes[i];
64 if c == 0 {
65 if null_streak {
66 c = 0x20;
67 bytes[i] = c;
68 } else {
69 return Err(FccConversionError::InvalidChar);
70 }
71 } else {
72 null_streak = false;
73 }
74
75 if c <= b'\x1f' || c >= b'\x7f' {
76 return Err(FccConversionError::InvalidChar);
77 }
78
79 if i == 0 {
80 break;
81 }
82 i -= 1;
83 }
84
85 Ok(FourCharCode(u32::from_be_bytes(bytes)))
86}
87
88const fn normalize(value: u32) -> u32 {
89 let mut bytes = u32::to_be_bytes(value);
90
91 let mut i = 3usize;
92 loop {
93 let c = bytes[i];
94
95 if c == 0 {
96 bytes[i] = 0x20;
97 } else {
98 return u32::from_be_bytes(bytes);
99 }
100
101 if i == 0 {
102 break;
103 }
104 i -= 1;
105 }
106
107 u32::from_be_bytes(bytes)
108}
109
110impl FourCharCode {
111 #[inline]
113 pub const fn new(value: u32) -> Result<Self> {
114 from_bytes(u32::to_be_bytes(value))
115 }
116
117 #[inline]
121 pub const unsafe fn new_unchecked(value: u32) -> Self {
122 Self(normalize(value))
123 }
124
125 #[inline]
127 pub const fn from_array(value: [u8; 4]) -> Result<Self> {
128 from_bytes(value)
129 }
130
131 pub const fn from_slice(value: &[u8]) -> Result<Self> {
133 if value.len() < 4 {
134 return Err(FccConversionError::TooShort);
135 } else if value.len() > 4 {
136 return Err(FccConversionError::TooLong);
137 }
138
139 from_bytes([value[0], value[1], value[2], value[3]])
140 }
141
142 #[allow(clippy::should_implement_trait)]
144 pub const fn from_str(value: &str) -> Result<Self> {
145 Self::from_slice(value.as_bytes())
146 }
147
148 pub fn normalize(&mut self) {
150 self.0 = normalize(self.0);
151 }
152
153 pub fn display(&self) -> Display {
156 Display(u32::from_be(normalize(self.0)))
157 }
158
159 #[inline]
161 pub const fn as_u32(&self) -> u32 {
162 self.0
163 }
164}
165
166impl Default for FourCharCode {
167 #[inline]
168 fn default() -> Self {
169 four_char_code!(" ")
170 }
171}
172
173impl PartialEq<u32> for FourCharCode {
174 #[inline]
175 fn eq(&self, other: &u32) -> bool {
176 self.0.eq(other)
177 }
178}
179
180impl PartialOrd<u32> for FourCharCode {
181 #[inline]
182 fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
183 self.0.partial_cmp(other)
184 }
185}
186
187impl PartialEq<str> for FourCharCode {
188 fn eq(&self, other: &str) -> bool {
189 if let Ok(other) = Self::from_str(other) {
190 *self == other
191 } else {
192 false
193 }
194 }
195}
196
197impl PartialEq<&str> for FourCharCode {
198 #[inline]
199 fn eq(&self, other: &&str) -> bool {
200 self.eq(*other)
201 }
202}
203
204impl PartialOrd<str> for FourCharCode {
205 fn partial_cmp(&self, other: &str) -> Option<Ordering> {
206 if let Ok(other) = Self::from_str(other) {
207 self.partial_cmp(&other)
208 } else {
209 None
210 }
211 }
212}
213
214impl PartialOrd<&str> for FourCharCode {
215 #[inline]
216 fn partial_cmp(&self, other: &&str) -> Option<Ordering> {
217 self.partial_cmp(*other)
218 }
219}
220
221impl PartialEq<[u8]> for FourCharCode {
222 fn eq(&self, other: &[u8]) -> bool {
223 if let Ok(other) = Self::from_slice(other) {
224 *self == other
225 } else {
226 false
227 }
228 }
229}
230
231impl PartialEq<&[u8]> for FourCharCode {
232 #[inline]
233 fn eq(&self, other: &&[u8]) -> bool {
234 self.eq(*other)
235 }
236}
237
238impl PartialOrd<[u8]> for FourCharCode {
239 fn partial_cmp(&self, other: &[u8]) -> Option<Ordering> {
240 if let Ok(other) = Self::from_slice(other) {
241 self.partial_cmp(&other)
242 } else {
243 None
244 }
245 }
246}
247
248impl PartialOrd<&[u8]> for FourCharCode {
249 #[inline]
250 fn partial_cmp(&self, other: &&[u8]) -> Option<Ordering> {
251 self.partial_cmp(*other)
252 }
253}
254
255impl PartialEq<[u8; 4]> for FourCharCode {
256 fn eq(&self, other: &[u8; 4]) -> bool {
257 if let Ok(other) = Self::from_array(*other) {
258 *self == other
259 } else {
260 false
261 }
262 }
263}
264
265impl PartialOrd<[u8; 4]> for FourCharCode {
266 fn partial_cmp(&self, other: &[u8; 4]) -> Option<Ordering> {
267 if let Ok(other) = Self::from_array(*other) {
268 self.partial_cmp(&other)
269 } else {
270 None
271 }
272 }
273}
274
275impl fmt::Debug for FourCharCode {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 f.debug_tuple("FourCharCode")
278 .field(&self.display())
279 .finish()
280 }
281}
282
283#[cfg(feature = "std")]
284impl ToString for FourCharCode {
285 #[inline]
286 fn to_string(&self) -> String {
287 let bytes = self.0.to_be_bytes();
288 unsafe { core::str::from_utf8_unchecked(&bytes[..]) }.to_string()
289 }
290}
291
292impl From<FourCharCode> for u32 {
293 #[inline]
294 fn from(value: FourCharCode) -> Self {
295 value.0
296 }
297}
298
299#[cfg(feature = "std")]
300impl From<FourCharCode> for String {
301 #[inline]
302 fn from(value: FourCharCode) -> Self {
303 value.to_string()
304 }
305}
306
307impl fmt::Display for Display {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 let chars = unsafe { core::slice::from_raw_parts((&self.0 as *const u32).cast::<u8>(), 4) };
310 for &c in chars {
311 let c = if c <= b'\x1f' || c >= b'\x7f' {
312 '�'
313 } else {
314 unsafe { char::from_u32_unchecked(c as u32) }
315 };
316 fmt::Display::fmt(&c, f)?;
317 }
318 Ok(())
319 }
320}
321
322impl fmt::Debug for Display {
323 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 let chars = unsafe { core::slice::from_raw_parts((&self.0 as *const u32).cast::<u8>(), 4) };
325 f.write_char('"')?;
326 for &c in chars {
327 if c <= b'\x1f' || c >= b'\x7f' {
328 f.write_char('�')
329 } else if c == b'"' {
330 f.write_str("\\\"")
331 } else {
332 f.write_char(unsafe { char::from_u32_unchecked(c as u32) })
333 }?;
334 }
335 f.write_char('"')
336 }
337}
338
339#[doc(hidden)]
340pub mod __private {
341 use core::fmt::Write;
342
343 use super::{FccConversionError, FourCharCode};
344
345 struct FccBuf {
346 buf: [u8; 4],
347 len: usize,
348 err: Option<FccConversionError>,
349 }
350
351 impl FccBuf {
352 #[inline(always)]
353 fn new() -> Self {
354 Self {
355 buf: [0; 4],
356 len: 0,
357 err: None,
358 }
359 }
360 }
361
362 impl core::fmt::Write for FccBuf {
363 fn write_char(&mut self, c: char) -> core::fmt::Result {
364 if !c.is_ascii() || c.is_control() {
365 self.err = Some(FccConversionError::InvalidChar);
366 Err(core::fmt::Error)
367 } else if self.len == 4 {
368 self.err = Some(FccConversionError::TooLong);
369 Err(core::fmt::Error)
370 } else {
371 unsafe { *self.buf.get_unchecked_mut(self.len) = c as u8 };
372 self.len += 1;
373 Ok(())
374 }
375 }
376
377 #[inline]
378 fn write_fmt(mut self: &mut Self, args: core::fmt::Arguments<'_>) -> core::fmt::Result {
379 core::fmt::write(&mut self, args)
380 }
381
382 fn write_str(&mut self, s: &str) -> core::fmt::Result {
383 for c in s.chars() {
384 self.write_char(c)?;
385 }
386 Ok(())
387 }
388 }
389
390 pub fn fcc_format(
391 args: core::fmt::Arguments<'_>,
392 ) -> core::result::Result<FourCharCode, FccConversionError> {
393 let mut buf = FccBuf::new();
394 buf.write_fmt(args).map_err(|_| buf.err.take().unwrap())?;
395 if buf.len != 4 {
396 return Err(FccConversionError::TooShort);
397 }
398 Ok(FourCharCode(u32::from_be_bytes(buf.buf)))
399 }
400}
401
402#[macro_export]
404macro_rules! four_char_code {
405 ($str:literal) => {
406 match $crate::FourCharCode::from_str($str) {
407 Ok(fcc) => fcc,
408 Err($crate::FccConversionError::TooLong) => panic!("four char code is too long"),
409 Err($crate::FccConversionError::TooShort) => panic!("four char code is too short"),
410 Err($crate::FccConversionError::InvalidChar) => {
411 panic!("invalid char in four char code")
412 }
413 }
414 };
415}
416
417#[macro_export]
420macro_rules! fcc_format {
421 ($fmt:expr) => {
422 $crate::__private::fcc_format(::core::format_args!($fmt))
423 };
424 ($fmt:expr, $($args:tt)*) => {
425 $crate::__private::fcc_format(::core::format_args!($fmt, $($args)*))
426 };
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 const HEX: FourCharCode = four_char_code!("hex_");
434
435 #[test]
436 fn invalid() {
437 assert!(FourCharCode::new(1).is_err());
438 assert!(FourCharCode::from_str("").is_err());
439 assert!(FourCharCode::from_str("test1").is_err());
440 assert!(FourCharCode::from_str("\x7f___").is_err());
441 }
442
443 #[test]
444 fn valid() {
445 assert_eq!(HEX, "hex_");
446 let ui32 = FourCharCode::from_str("ui32");
447 assert!(ui32.is_ok());
448 assert_eq!(ui32.unwrap(), "ui32");
449 }
450
451 #[test]
452 fn format() {
453 let f1mn = fcc_format!("F{}Mn", 1);
454 assert!(f1mn.is_ok());
455 assert_eq!(f1mn.unwrap(), "F1Mn");
456 }
457}