doom_fish_utils/
four_char_code.rs1use std::fmt;
7use std::str::FromStr;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct FourCharCode(u32);
26
27impl FourCharCode {
28 #[inline]
39 #[must_use]
40 pub const fn from_bytes(bytes: [u8; 4]) -> Self {
41 Self(u32::from_be_bytes(bytes))
42 }
43
44 #[must_use]
46 pub const fn from_slice(bytes: &[u8]) -> Option<Self> {
47 if bytes.len() != 4 {
48 return None;
49 }
50
51 let code = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
52 Some(Self(code))
53 }
54
55 #[inline]
67 #[must_use]
68 pub const fn as_u32(self) -> u32 {
69 self.0
70 }
71
72 #[inline]
74 #[must_use]
75 pub const fn as_bytes(self) -> [u8; 4] {
76 self.0.to_be_bytes()
77 }
78
79 #[inline]
81 #[must_use]
82 pub const fn from_u32(value: u32) -> Self {
83 Self(value)
84 }
85
86 #[inline]
88 #[must_use]
89 pub const fn equals(self, other: Self) -> bool {
90 self.0 == other.0
91 }
92
93 #[must_use]
95 pub fn display(self) -> String {
96 let bytes = self.0.to_be_bytes();
97 String::from_utf8_lossy(&bytes).to_string()
98 }
99}
100
101impl FromStr for FourCharCode {
102 type Err = &'static str;
103
104 fn from_str(s: &str) -> Result<Self, Self::Err> {
105 if s.len() != 4 {
106 return Err("FourCharCode must be exactly 4 characters");
107 }
108 if !s.is_ascii() {
109 return Err("FourCharCode must contain only ASCII characters");
110 }
111
112 let bytes = s.as_bytes();
113 let code = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
114 Ok(Self(code))
115 }
116}
117
118impl From<u32> for FourCharCode {
119 fn from(value: u32) -> Self {
120 Self(value)
121 }
122}
123
124impl From<FourCharCode> for u32 {
125 fn from(code: FourCharCode) -> Self {
126 code.0
127 }
128}
129
130impl fmt::Display for FourCharCode {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write!(f, "{}", self.display())
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::FourCharCode;
139
140 #[test]
141 fn bytes_and_integers_round_trip() {
142 let code = FourCharCode::from_bytes(*b"avc1");
143
144 assert_eq!(code.as_u32(), 0x6176_6331);
145 assert_eq!(code.as_bytes(), *b"avc1");
146 assert_eq!(FourCharCode::from_u32(0x6176_6331), code);
147 assert_eq!(FourCharCode::from(0x6176_6331), code);
148 assert_eq!(u32::from(code), 0x6176_6331);
149 assert!(code.equals(FourCharCode::from_bytes(*b"avc1")));
150 assert!(!code.equals(FourCharCode::from_bytes(*b"hvc1")));
151 }
152
153 #[test]
154 fn from_slice_requires_exactly_four_bytes() {
155 assert_eq!(
156 FourCharCode::from_slice(b"BGRA"),
157 Some(FourCharCode::from_bytes(*b"BGRA"))
158 );
159 assert_eq!(FourCharCode::from_slice(b"BGR"), None);
160 assert_eq!(FourCharCode::from_slice(b"BGRA8"), None);
161 assert_eq!(FourCharCode::from_slice(&[]), None);
162 }
163
164 #[test]
165 fn parsing_rejects_wrong_lengths_and_non_ascii() {
166 assert_eq!(
167 "420v".parse::<FourCharCode>(),
168 Ok(FourCharCode::from_bytes(*b"420v"))
169 );
170 assert_eq!(
171 "BGR".parse::<FourCharCode>(),
172 Err("FourCharCode must be exactly 4 characters")
173 );
174 assert_eq!(
175 "BGRA8".parse::<FourCharCode>(),
176 Err("FourCharCode must be exactly 4 characters")
177 );
178 assert_eq!(
179 "ab\u{e9}".parse::<FourCharCode>(),
180 Err("FourCharCode must contain only ASCII characters")
181 );
182 }
183
184 #[test]
185 fn display_shows_the_code_characters() {
186 let code = FourCharCode::from_bytes(*b"lpcm");
187
188 assert_eq!(code.display(), "lpcm");
189 assert_eq!(code.to_string(), "lpcm");
190 assert_eq!(
191 FourCharCode::from_bytes([b'a', 0xff, b'b', b'c']).display(),
192 "a\u{fffd}bc"
193 );
194 }
195}