four_char_code/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![allow(non_camel_case_types)]
3
4extern crate four_char_code_macros_impl;
5extern crate proc_macro_hack;
6
7use core::{
8    cmp::Ordering,
9    fmt::{self, Write},
10};
11
12#[cfg(feature = "std")]
13use std::string::{String, ToString};
14
15/// An enum representing a conversion (eg. string->fcc or format->fcc) error
16#[derive(Debug, Clone, Copy)]
17pub enum FccConversionError {
18    /// Given string is > 4 bytes
19    TooLong,
20    /// Given string is < 4 bytes
21    TooShort,
22    /// Given string contains a non printable ascii char
23    InvalidChar,
24}
25
26impl FccConversionError {
27    pub fn description(&self) -> &str {
28        match self {
29            FccConversionError::TooLong => "four char code is too long",
30            FccConversionError::TooShort => "four char code is too short",
31            FccConversionError::InvalidChar => "invalid char in four char code",
32        }
33    }
34}
35
36impl fmt::Display for FccConversionError {
37    #[inline]
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        fmt::Display::fmt(FccConversionError::description(self), f)
40    }
41}
42
43#[cfg(feature = "std")]
44impl ::std::error::Error for FccConversionError {
45    #[inline]
46    fn description(&self) -> &str {
47        FccConversionError::description(self)
48    }
49}
50
51type Result<T> = core::result::Result<T, FccConversionError>;
52
53/// The main structure, actually a u32.
54#[repr(transparent)]
55#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
56pub struct FourCharCode(u32);
57
58/// Helper struct to safely print [FourCharCode] with `format!`.
59#[repr(transparent)]
60pub struct Display(u32);
61
62fn from_bytes(mut bytes: [u8; 4]) -> Result<FourCharCode> {
63    let mut null_streak = true;
64
65    let mut i = 3usize;
66    loop {
67        let mut c = bytes[i];
68        if c == 0 && null_streak {
69            c = 0x20;
70            bytes[i] = c;
71        } else {
72            null_streak = false;
73        }
74
75        if 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
88fn 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 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(value)
123    }
124
125    /// Returns a [FourCharCode] if values are valid, an error describing the problem otherwise.
126    #[inline]
127    pub 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 fn from_slice(value: &[u8]) -> Result<Self> {
133        match value.len().cmp(&4) {
134            Ordering::Less => return Err(FccConversionError::TooShort),
135            Ordering::Greater => return Err(FccConversionError::TooLong),
136            _ => (),
137        }
138
139        from_bytes(unsafe {
140            [
141                *value.get_unchecked(0),
142                *value.get_unchecked(1),
143                *value.get_unchecked(2),
144                *value.get_unchecked(3),
145            ]
146        })
147    }
148
149    /// Returns a [FourCharCode] if string is valid, an error describing the problem otherwise.
150    #[allow(clippy::should_implement_trait)]
151    pub fn from_str(value: &str) -> Result<Self> {
152        Self::from_slice(value.as_bytes())
153    }
154
155    /// Substitute leading zeroes with spaces (padding with space).
156    pub fn normalize(&mut self) {
157        self.0 = normalize(self.0);
158    }
159
160    /// Returns an object that implements [core::fmt::Display] for safely printing
161    /// fourcc's that may contain non-ASCII characters.
162    #[allow(clippy::trivially_copy_pass_by_ref)]
163    pub fn display(&self) -> Display {
164        Display(u32::from_be(normalize(self.0)))
165    }
166
167    /// Returns the underlying `u32` this [FourCharCode] represents
168    #[inline]
169    pub const fn as_u32(&self) -> u32 {
170        self.0
171    }
172}
173
174impl Default for FourCharCode {
175    #[inline]
176    fn default() -> Self {
177        four_char_code!("    ")
178    }
179}
180
181impl PartialEq<u32> for FourCharCode {
182    #[inline]
183    fn eq(&self, other: &u32) -> bool {
184        self.0.eq(other)
185    }
186}
187
188impl PartialOrd<u32> for FourCharCode {
189    #[inline]
190    fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
191        self.0.partial_cmp(other)
192    }
193}
194
195impl PartialEq<str> for FourCharCode {
196    fn eq(&self, other: &str) -> bool {
197        if let Ok(other) = Self::from_str(other) {
198            *self == other
199        } else {
200            false
201        }
202    }
203}
204
205impl PartialEq<&str> for FourCharCode {
206    #[inline]
207    fn eq(&self, other: &&str) -> bool {
208        self.eq(*other)
209    }
210}
211
212impl PartialOrd<str> for FourCharCode {
213    fn partial_cmp(&self, other: &str) -> Option<Ordering> {
214        if let Ok(other) = Self::from_str(other) {
215            self.partial_cmp(&other)
216        } else {
217            None
218        }
219    }
220}
221
222impl PartialOrd<&str> for FourCharCode {
223    #[inline]
224    fn partial_cmp(&self, other: &&str) -> Option<Ordering> {
225        self.partial_cmp(*other)
226    }
227}
228
229impl PartialEq<[u8]> for FourCharCode {
230    fn eq(&self, other: &[u8]) -> bool {
231        if let Ok(other) = Self::from_slice(other) {
232            *self == other
233        } else {
234            false
235        }
236    }
237}
238
239impl PartialEq<&[u8]> for FourCharCode {
240    #[inline]
241    fn eq(&self, other: &&[u8]) -> bool {
242        self.eq(*other)
243    }
244}
245
246impl PartialOrd<[u8]> for FourCharCode {
247    fn partial_cmp(&self, other: &[u8]) -> Option<Ordering> {
248        if let Ok(other) = Self::from_slice(other) {
249            self.partial_cmp(&other)
250        } else {
251            None
252        }
253    }
254}
255
256impl PartialOrd<&[u8]> for FourCharCode {
257    #[inline]
258    fn partial_cmp(&self, other: &&[u8]) -> Option<Ordering> {
259        self.partial_cmp(*other)
260    }
261}
262
263impl PartialEq<[u8; 4]> for FourCharCode {
264    fn eq(&self, other: &[u8; 4]) -> bool {
265        if let Ok(other) = Self::from_array(*other) {
266            *self == other
267        } else {
268            false
269        }
270    }
271}
272
273impl PartialOrd<[u8; 4]> for FourCharCode {
274    fn partial_cmp(&self, other: &[u8; 4]) -> Option<Ordering> {
275        if let Ok(other) = Self::from_array(*other) {
276            self.partial_cmp(&other)
277        } else {
278            None
279        }
280    }
281}
282
283impl fmt::Debug for FourCharCode {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        f.debug_tuple("FourCharCode")
286            .field(&self.display())
287            .finish()
288    }
289}
290
291#[cfg(feature = "std")]
292#[allow(clippy::to_string_trait_impl)]
293impl ToString for FourCharCode {
294    #[inline]
295    fn to_string(&self) -> String {
296        let bytes = self.0.to_be_bytes();
297        unsafe { core::str::from_utf8_unchecked(&bytes[..]) }.to_string()
298    }
299}
300
301impl From<FourCharCode> for u32 {
302    #[inline]
303    fn from(value: FourCharCode) -> Self {
304        value.0
305    }
306}
307
308#[cfg(feature = "std")]
309impl From<FourCharCode> for String {
310    #[inline]
311    fn from(value: FourCharCode) -> Self {
312        value.to_string()
313    }
314}
315
316impl fmt::Display for Display {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        let chars = unsafe { core::slice::from_raw_parts((&self.0 as *const u32) as *const u8, 4) };
319        for &c in chars {
320            let c = if c <= b'\x1f' || c >= b'\x7f' {
321                '�'
322            } else {
323                #[allow(clippy::transmute_int_to_char)]
324                unsafe {
325                    core::mem::transmute::<u32, char>(u32::from(c))
326                }
327            };
328            fmt::Display::fmt(&c, f)?;
329        }
330        Ok(())
331    }
332}
333
334impl fmt::Debug for Display {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        let chars = unsafe { core::slice::from_raw_parts((&self.0 as *const u32) as *const u8, 4) };
337        f.write_char('"')?;
338        for &c in chars {
339            if c <= b'\x1f' || c >= b'\x7f' {
340                f.write_char('�')
341            } else if c == b'"' {
342                f.write_str("\\\"")
343            } else {
344                #[allow(clippy::transmute_int_to_char)]
345                f.write_char(unsafe { core::mem::transmute::<u32, char>(u32::from(c)) })
346            }?;
347        }
348        f.write_char('"')
349    }
350}
351
352#[doc(hidden)]
353#[cfg(ge_1_38_0)]
354pub mod __private {
355    use core::fmt::Write;
356
357    use super::{FccConversionError, FourCharCode};
358
359    struct FccBuf {
360        buf: [u8; 4],
361        len: usize,
362        err: Option<FccConversionError>,
363    }
364
365    impl FccBuf {
366        #[inline(always)]
367        fn new() -> Self {
368            Self {
369                buf: [0; 4],
370                len: 0,
371                err: None,
372            }
373        }
374    }
375
376    impl core::fmt::Write for FccBuf {
377        fn write_char(&mut self, c: char) -> core::fmt::Result {
378            if !c.is_ascii() || c.is_control() {
379                self.err = Some(FccConversionError::InvalidChar);
380                Err(core::fmt::Error)
381            } else if self.len == 4 {
382                self.err = Some(FccConversionError::TooLong);
383                Err(core::fmt::Error)
384            } else {
385                unsafe { *self.buf.get_unchecked_mut(self.len) = c as u8 };
386                self.len += 1;
387                Ok(())
388            }
389        }
390
391        #[inline]
392        fn write_fmt(mut self: &mut Self, args: core::fmt::Arguments<'_>) -> core::fmt::Result {
393            core::fmt::write(&mut self, args)
394        }
395
396        fn write_str(&mut self, s: &str) -> core::fmt::Result {
397            for c in s.chars() {
398                self.write_char(c)?;
399            }
400            Ok(())
401        }
402    }
403
404    pub fn fcc_format(
405        args: core::fmt::Arguments<'_>,
406    ) -> core::result::Result<FourCharCode, FccConversionError> {
407        let mut buf = FccBuf::new();
408        buf.write_fmt(args).map_err(|_| buf.err.take().unwrap())?;
409        if buf.len != 4 {
410            return Err(FccConversionError::TooShort);
411        }
412        Ok(FourCharCode(u32::from_be_bytes(buf.buf)))
413    }
414}
415
416#[derive(proc_macro_hack::ProcMacroHack)]
417enum _26four_char_code_macros_impl_14four_char_code {
418    Value = (
419        stringify! {
420            #[doc(hidden)]
421            pub use four_char_code_macros_impl::_proc_macro_hack_four_char_code;
422
423            /// Create a checked [FourCharCode] at compile time
424            #[macro_export]
425            macro_rules! four_char_code {
426                ($($proc_macro:tt)*) => {
427                    {
428                        #[derive($crate::_proc_macro_hack_four_char_code)]
429                        #[allow(dead_code)]
430                        enum ProcMacroHack {
431                            Value = (stringify!($($proc_macro)*), 0).1
432                        }
433                        unsafe { $crate::FourCharCode::new_unchecked(proc_macro_call!()) }
434                    }
435                };
436            }
437        },
438        0,
439    )
440        .1,
441}
442
443#[cfg(ge_1_38_0)]
444/// Returns a [FourCharCode] from a `format!` like expression without allocation if valid.
445/// Returns an error describing the problem otherwise.
446#[macro_export]
447macro_rules! fcc_format {
448    ($fmt:expr) => {
449        $crate::__private::fcc_format(::core::format_args!($fmt))
450    };
451    ($fmt:expr, $($args:tt)*) => {
452        $crate::__private::fcc_format(::core::format_args!($fmt, $($args)*))
453    };
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    const HEX: FourCharCode = four_char_code!("hex_");
461
462    #[test]
463    fn invalid() {
464        assert!(FourCharCode::new(128).is_err());
465        assert!(FourCharCode::from_str("").is_err());
466        assert!(FourCharCode::from_str("test1").is_err());
467        assert!(FourCharCode::from_slice(b"\x80___").is_err());
468    }
469
470    #[test]
471    fn valid() {
472        assert_eq!(HEX, "hex_");
473        let ui32 = FourCharCode::from_str("ui32");
474        assert!(ui32.is_ok());
475        assert_eq!(ui32.unwrap(), "ui32");
476        let zigr = FourCharCode::from_str("\0igr");
477        assert!(zigr.is_ok());
478        assert_eq!(zigr.unwrap(), "\0igr");
479    }
480
481    #[cfg(ge_1_38_0)]
482    #[test]
483    fn format() {
484        let f1mn = fcc_format!("F{}Mn", 1);
485        assert!(f1mn.is_ok());
486        assert_eq!(f1mn.unwrap(), "F1Mn");
487    }
488}