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 && 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 #[inline]
109 pub const fn new(value: u32) -> Result<Self> {
110 from_bytes(u32::to_be_bytes(value))
111 }
112
113 #[inline]
117 pub const unsafe fn new_unchecked(value: u32) -> Self {
118 Self(normalize(value))
119 }
120
121 #[inline]
123 pub const fn from_array(value: [u8; 4]) -> Result<Self> {
124 from_bytes(value)
125 }
126
127 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 #[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 pub fn normalize(&mut self) {
146 self.0 = normalize(self.0);
147 }
148
149 pub fn display(&self) -> Display {
152 Display(u32::from_be(normalize(self.0)))
153 }
154
155 #[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#[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#[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}