four_char_code/
lib.rs

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/// An enum representing a conversion (eg. string->fcc or format->fcc) error
12#[derive(Debug, Clone, Copy)]
13pub enum FccConversionError {
14    /// Given string is > 4 bytes
15    TooLong,
16    /// Given string is < 4 bytes
17    TooShort,
18    /// Given string contains a non printable ascii char
19    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/// The main structure, actually a u32.
50#[repr(transparent)]
51#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
52pub struct FourCharCode(u32);
53
54/// Helper struct to safely print [FourCharCode] with `format!`.
55#[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    /// Returns a [FourCharCode] if value is valid, an error describing the problem otherwise.
112    #[inline]
113    pub const fn new(value: u32) -> Result<Self> {
114        from_bytes(u32::to_be_bytes(value))
115    }
116
117    /// Returns a [FourCharCode] containing the given value.
118    /// # Safety
119    /// Passing an invalid value can cause a panic
120    #[inline]
121    pub const unsafe fn new_unchecked(value: u32) -> Self {
122        Self(normalize(value))
123    }
124
125    /// Returns a [FourCharCode] if values are valid, an error describing the problem otherwise.
126    #[inline]
127    pub const fn from_array(value: [u8; 4]) -> Result<Self> {
128        from_bytes(value)
129    }
130
131    /// Returns a [FourCharCode] if slice is valid, an error describing the problem otherwise.
132    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    /// Returns a [FourCharCode] if string is valid, an error describing the problem otherwise.
143    #[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    /// Substitute leading zeroes with spaces (padding with space).
149    pub fn normalize(&mut self) {
150        self.0 = normalize(self.0);
151    }
152
153    /// Returns an object that implements [core::fmt::Display] for safely printing
154    /// fourcc's that may contain non-ASCII characters.
155    pub fn display(&self) -> Display {
156        Display(u32::from_be(normalize(self.0)))
157    }
158
159    /// Returns the underlying `u32` this [FourCharCode] represents
160    #[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/// Create a checked [FourCharCode] at compile time
403#[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/// Returns a [FourCharCode] from a `format!` like expression without allocation if valid.
418/// Returns an error describing the problem otherwise.
419#[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}