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 && null_streak {
65            c = 0x20;
66            bytes[i] = c;
67        } else {
68            null_streak = false;
69        }
70
71        if c > b'\x7f' {
72            return Err(FccConversionError::InvalidChar);
73        }
74
75        if i == 0 {
76            break;
77        }
78        i -= 1;
79    }
80
81    Ok(FourCharCode(u32::from_be_bytes(bytes)))
82}
83
84const fn normalize(value: u32) -> u32 {
85    let mut bytes = u32::to_be_bytes(value);
86
87    let mut i = 3usize;
88    loop {
89        let c = bytes[i];
90
91        if c == 0 {
92            bytes[i] = 0x20;
93        } else {
94            return u32::from_be_bytes(bytes);
95        }
96
97        if i == 0 {
98            break;
99        }
100        i -= 1;
101    }
102
103    u32::from_be_bytes(bytes)
104}
105
106impl FourCharCode {
107    /// Returns a [FourCharCode] if value is valid, an error describing the problem otherwise.
108    #[inline]
109    pub const fn new(value: u32) -> Result<Self> {
110        from_bytes(u32::to_be_bytes(value))
111    }
112
113    /// Returns a [FourCharCode] containing the given value.
114    /// # Safety
115    /// Passing an invalid value can cause a panic
116    #[inline]
117    pub const unsafe fn new_unchecked(value: u32) -> Self {
118        Self(normalize(value))
119    }
120
121    /// Returns a [FourCharCode] if values are valid, an error describing the problem otherwise.
122    #[inline]
123    pub const fn from_array(value: [u8; 4]) -> Result<Self> {
124        from_bytes(value)
125    }
126
127    /// Returns a [FourCharCode] if slice is valid, an error describing the problem otherwise.
128    pub const fn from_slice(value: &[u8]) -> Result<Self> {
129        if value.len() < 4 {
130            return Err(FccConversionError::TooShort);
131        } else if value.len() > 4 {
132            return Err(FccConversionError::TooLong);
133        }
134
135        from_bytes([value[0], value[1], value[2], value[3]])
136    }
137
138    /// Returns a [FourCharCode] if string is valid, an error describing the problem otherwise.
139    #[allow(clippy::should_implement_trait)]
140    pub const fn from_str(value: &str) -> Result<Self> {
141        Self::from_slice(value.as_bytes())
142    }
143
144    /// Substitute leading zeroes with spaces (padding with space).
145    pub fn normalize(&mut self) {
146        self.0 = normalize(self.0);
147    }
148
149    /// Returns an object that implements [core::fmt::Display] for safely printing
150    /// fourcc's that may contain non-ASCII characters.
151    pub fn display(&self) -> Display {
152        Display(u32::from_be(normalize(self.0)))
153    }
154
155    /// Returns the underlying `u32` this [FourCharCode] represents
156    #[inline]
157    pub const fn as_u32(&self) -> u32 {
158        self.0
159    }
160}
161
162impl Default for FourCharCode {
163    #[inline]
164    fn default() -> Self {
165        four_char_code!("    ")
166    }
167}
168
169impl PartialEq<u32> for FourCharCode {
170    #[inline]
171    fn eq(&self, other: &u32) -> bool {
172        self.0.eq(other)
173    }
174}
175
176impl PartialOrd<u32> for FourCharCode {
177    #[inline]
178    fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
179        self.0.partial_cmp(other)
180    }
181}
182
183impl PartialEq<str> for FourCharCode {
184    fn eq(&self, other: &str) -> bool {
185        if let Ok(other) = Self::from_str(other) {
186            *self == other
187        } else {
188            false
189        }
190    }
191}
192
193impl PartialEq<&str> for FourCharCode {
194    #[inline]
195    fn eq(&self, other: &&str) -> bool {
196        self.eq(*other)
197    }
198}
199
200impl PartialOrd<str> for FourCharCode {
201    fn partial_cmp(&self, other: &str) -> Option<Ordering> {
202        if let Ok(other) = Self::from_str(other) {
203            self.partial_cmp(&other)
204        } else {
205            None
206        }
207    }
208}
209
210impl PartialOrd<&str> for FourCharCode {
211    #[inline]
212    fn partial_cmp(&self, other: &&str) -> Option<Ordering> {
213        self.partial_cmp(*other)
214    }
215}
216
217impl PartialEq<[u8]> for FourCharCode {
218    fn eq(&self, other: &[u8]) -> bool {
219        if let Ok(other) = Self::from_slice(other) {
220            *self == other
221        } else {
222            false
223        }
224    }
225}
226
227impl PartialEq<&[u8]> for FourCharCode {
228    #[inline]
229    fn eq(&self, other: &&[u8]) -> bool {
230        self.eq(*other)
231    }
232}
233
234impl PartialOrd<[u8]> for FourCharCode {
235    fn partial_cmp(&self, other: &[u8]) -> Option<Ordering> {
236        if let Ok(other) = Self::from_slice(other) {
237            self.partial_cmp(&other)
238        } else {
239            None
240        }
241    }
242}
243
244impl PartialOrd<&[u8]> for FourCharCode {
245    #[inline]
246    fn partial_cmp(&self, other: &&[u8]) -> Option<Ordering> {
247        self.partial_cmp(*other)
248    }
249}
250
251impl PartialEq<[u8; 4]> for FourCharCode {
252    fn eq(&self, other: &[u8; 4]) -> bool {
253        if let Ok(other) = Self::from_array(*other) {
254            *self == other
255        } else {
256            false
257        }
258    }
259}
260
261impl PartialOrd<[u8; 4]> for FourCharCode {
262    fn partial_cmp(&self, other: &[u8; 4]) -> Option<Ordering> {
263        if let Ok(other) = Self::from_array(*other) {
264            self.partial_cmp(&other)
265        } else {
266            None
267        }
268    }
269}
270
271impl fmt::Debug for FourCharCode {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        f.debug_tuple("FourCharCode")
274            .field(&self.display())
275            .finish()
276    }
277}
278
279#[cfg(feature = "std")]
280#[allow(clippy::to_string_trait_impl)]
281impl ToString for FourCharCode {
282    #[inline]
283    fn to_string(&self) -> String {
284        let bytes = self.0.to_be_bytes();
285        unsafe { core::str::from_utf8_unchecked(&bytes[..]) }.to_string()
286    }
287}
288
289impl From<FourCharCode> for u32 {
290    #[inline]
291    fn from(value: FourCharCode) -> Self {
292        value.0
293    }
294}
295
296#[cfg(feature = "std")]
297impl From<FourCharCode> for String {
298    #[inline]
299    fn from(value: FourCharCode) -> Self {
300        value.to_string()
301    }
302}
303
304impl fmt::Display for Display {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        let chars = unsafe { core::slice::from_raw_parts((&self.0 as *const u32).cast::<u8>(), 4) };
307        for &c in chars {
308            let c = if c <= b'\x1f' || c >= b'\x7f' {
309                '�'
310            } else {
311                unsafe { char::from_u32_unchecked(c as u32) }
312            };
313            fmt::Display::fmt(&c, f)?;
314        }
315        Ok(())
316    }
317}
318
319impl fmt::Debug for Display {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        let chars = unsafe { core::slice::from_raw_parts((&self.0 as *const u32).cast::<u8>(), 4) };
322        f.write_char('"')?;
323        for &c in chars {
324            if c <= b'\x1f' || c >= b'\x7f' {
325                f.write_char('�')
326            } else if c == b'"' {
327                f.write_str("\\\"")
328            } else {
329                f.write_char(unsafe { char::from_u32_unchecked(c as u32) })
330            }?;
331        }
332        f.write_char('"')
333    }
334}
335
336#[doc(hidden)]
337pub mod __private {
338    use core::fmt::Write;
339
340    use super::{FccConversionError, FourCharCode};
341
342    struct FccBuf {
343        buf: [u8; 4],
344        len: usize,
345        err: Option<FccConversionError>,
346    }
347
348    impl FccBuf {
349        #[inline(always)]
350        fn new() -> Self {
351            Self {
352                buf: [0; 4],
353                len: 0,
354                err: None,
355            }
356        }
357    }
358
359    impl core::fmt::Write for FccBuf {
360        fn write_char(&mut self, c: char) -> core::fmt::Result {
361            if !c.is_ascii() || c.is_control() {
362                self.err = Some(FccConversionError::InvalidChar);
363                Err(core::fmt::Error)
364            } else if self.len == 4 {
365                self.err = Some(FccConversionError::TooLong);
366                Err(core::fmt::Error)
367            } else {
368                unsafe { *self.buf.get_unchecked_mut(self.len) = c as u8 };
369                self.len += 1;
370                Ok(())
371            }
372        }
373
374        #[inline]
375        fn write_fmt(mut self: &mut Self, args: core::fmt::Arguments<'_>) -> core::fmt::Result {
376            core::fmt::write(&mut self, args)
377        }
378
379        fn write_str(&mut self, s: &str) -> core::fmt::Result {
380            for c in s.chars() {
381                self.write_char(c)?;
382            }
383            Ok(())
384        }
385    }
386
387    pub fn fcc_format(
388        args: core::fmt::Arguments<'_>,
389    ) -> core::result::Result<FourCharCode, FccConversionError> {
390        let mut buf = FccBuf::new();
391        buf.write_fmt(args).map_err(|_| buf.err.take().unwrap())?;
392        if buf.len != 4 {
393            return Err(FccConversionError::TooShort);
394        }
395        Ok(FourCharCode(u32::from_be_bytes(buf.buf)))
396    }
397}
398
399/// Create a checked [FourCharCode] at compile time
400#[macro_export]
401macro_rules! four_char_code {
402    ($str:literal) => {
403        match $crate::FourCharCode::from_str($str) {
404            Ok(fcc) => fcc,
405            Err($crate::FccConversionError::TooLong) => panic!("four char code is too long"),
406            Err($crate::FccConversionError::TooShort) => panic!("four char code is too short"),
407            Err($crate::FccConversionError::InvalidChar) => {
408                panic!("invalid char in four char code")
409            }
410        }
411    };
412}
413
414/// Returns a [FourCharCode] from a `format!` like expression without allocation if valid.
415/// Returns an error describing the problem otherwise.
416#[macro_export]
417macro_rules! fcc_format {
418    ($fmt:expr) => {
419        $crate::__private::fcc_format(::core::format_args!($fmt))
420    };
421    ($fmt:expr, $($args:tt)*) => {
422        $crate::__private::fcc_format(::core::format_args!($fmt, $($args)*))
423    };
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    const HEX: FourCharCode = four_char_code!("hex_");
431
432    #[test]
433    fn invalid() {
434        assert!(FourCharCode::new(128).is_err());
435        assert!(FourCharCode::from_str("").is_err());
436        assert!(FourCharCode::from_str("test1").is_err());
437        assert!(FourCharCode::from_slice(b"\x80___").is_err());
438    }
439
440    #[test]
441    fn valid() {
442        assert_eq!(HEX, "hex_");
443        let ui32 = FourCharCode::from_str("ui32");
444        assert!(ui32.is_ok());
445        assert_eq!(ui32.unwrap(), "ui32");
446        let zigr = FourCharCode::from_str("\0igr");
447        assert!(zigr.is_ok());
448        assert_eq!(zigr.unwrap(), "\0igr");
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}