libpna/chunk/types.rs
1use std::{
2 error::Error,
3 fmt::{self, Debug, Display, Formatter},
4};
5
6/// [ChunkType] validation error.
7#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
8pub enum ChunkTypeError {
9 /// Contains non-ascii alphabet error.
10 NonAsciiAlphabetic,
11 /// The second character is not lowercase error.
12 NonPrivateChunkType,
13 /// The third character is not uppercase error.
14 Reserved,
15}
16
17impl Display for ChunkTypeError {
18 #[inline]
19 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
20 Display::fmt(
21 match self {
22 Self::NonAsciiAlphabetic => "All characters must be ASCII alphabetic",
23 Self::NonPrivateChunkType => "The second character must be lowercase",
24 Self::Reserved => "The third character must be uppercase",
25 },
26 f,
27 )
28 }
29}
30
31impl Error for ChunkTypeError {}
32
33/// A 4-byte chunk type code.
34///
35/// PNA uses a chunk-based format inspired by PNG. Each chunk has a 4-character
36/// type code that determines how the chunk should be interpreted.
37///
38/// # Chunk Type Naming Convention
39///
40/// The case of each letter in the chunk type encodes important properties:
41///
42/// | Position | Uppercase | Lowercase |
43/// |----------|-----------|-----------|
44/// | 1st | Critical (must understand) | Ancillary (can ignore) |
45/// | 2nd | Public (standardized) | Private (application-specific) |
46/// | 3rd | Reserved (must be uppercase) | - |
47/// | 4th | Unsafe to copy | Safe to copy if unknown |
48///
49/// # Critical Chunks
50///
51/// These chunks are essential for reading the archive structure:
52///
53/// - **Archive structure**: [`AHED`](Self::AHED) (header), [`AEND`](Self::AEND) (end),
54/// [`ANXT`](Self::ANXT) (next part)
55/// - **Entry structure**: [`FHED`](Self::FHED) (header), [`FDAT`](Self::FDAT) (data),
56/// [`FEND`](Self::FEND) (end)
57/// - **Solid mode**: [`SHED`](Self::SHED) (header), [`SDAT`](Self::SDAT) (data),
58/// [`SEND`](Self::SEND) (end)
59/// - **Encryption**: [`PHSF`](Self::PHSF) (password hash string format)
60///
61/// # Ancillary Chunks
62///
63/// These chunks contain optional metadata that can be safely ignored:
64///
65/// - **Timestamps**: [`cTIM`](Self::cTIM), [`mTIM`](Self::mTIM), [`aTIM`](Self::aTIM)
66/// (seconds), [`cTNS`](Self::cTNS), [`mTNS`](Self::mTNS), [`aTNS`](Self::aTNS) (nanoseconds)
67/// - **File info**: [`fSIZ`](Self::fSIZ) (size), [`fPRM`](Self::fPRM) (permissions)
68/// - **Extended attributes**: [`xATR`](Self::xATR)
69///
70/// # Creating Private Chunks
71///
72/// Use [`ChunkType::private`] to create application-specific chunk types:
73///
74/// ```rust
75/// use libpna::ChunkType;
76///
77/// // Private chunk type must have lowercase second letter
78/// let my_chunk = ChunkType::private(*b"myTy").unwrap();
79/// assert!(my_chunk.is_private());
80/// ```
81#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
82pub struct ChunkType(pub(crate) [u8; 4]);
83
84impl ChunkType {
85 // -- Critical chunks --
86 /// Archive header
87 pub const AHED: ChunkType = ChunkType(*b"AHED");
88 /// Archive end marker
89 pub const AEND: ChunkType = ChunkType(*b"AEND");
90 /// Archive next part marker
91 pub const ANXT: ChunkType = ChunkType(*b"ANXT");
92 /// Entry header
93 pub const FHED: ChunkType = ChunkType(*b"FHED");
94 /// Password hash string format
95 pub const PHSF: ChunkType = ChunkType(*b"PHSF");
96 /// Entry data stream
97 pub const FDAT: ChunkType = ChunkType(*b"FDAT");
98 /// Entry data stream end marker
99 pub const FEND: ChunkType = ChunkType(*b"FEND");
100 /// Solid mode data header
101 pub const SHED: ChunkType = ChunkType(*b"SHED");
102 /// Solid mode data stream
103 pub const SDAT: ChunkType = ChunkType(*b"SDAT");
104 /// Solid mode data stream end marker
105 pub const SEND: ChunkType = ChunkType(*b"SEND");
106
107 // -- Auxiliary chunks --
108 /// Raw file size
109 #[allow(non_upper_case_globals)]
110 pub const fSIZ: ChunkType = ChunkType(*b"fSIZ");
111 /// Creation datetime
112 #[allow(non_upper_case_globals)]
113 pub const cTIM: ChunkType = ChunkType(*b"cTIM");
114 /// Last modified datetime
115 #[allow(non_upper_case_globals)]
116 pub const mTIM: ChunkType = ChunkType(*b"mTIM");
117 /// Last accessed datetime
118 #[allow(non_upper_case_globals)]
119 pub const aTIM: ChunkType = ChunkType(*b"aTIM");
120 /// Nanoseconds for creation datetime
121 #[allow(non_upper_case_globals)]
122 pub const cTNS: ChunkType = ChunkType(*b"cTNS");
123 /// Nanoseconds for last modified datetime
124 #[allow(non_upper_case_globals)]
125 pub const mTNS: ChunkType = ChunkType(*b"mTNS");
126 /// Nanoseconds for last accessed datetime
127 #[allow(non_upper_case_globals)]
128 pub const aTNS: ChunkType = ChunkType(*b"aTNS");
129 /// Entry permissions
130 #[allow(non_upper_case_globals)]
131 pub const fPRM: ChunkType = ChunkType(*b"fPRM");
132 /// Extended attribute
133 #[allow(non_upper_case_globals)]
134 pub const xATR: ChunkType = ChunkType(*b"xATR");
135
136 /// Returns the length of the chunk type code.
137 ///
138 /// # Returns
139 ///
140 /// An integer value representing the length of the chunk type code.
141 ///
142 /// # Examples
143 ///
144 /// ```rust
145 /// use libpna::ChunkType;
146 ///
147 /// let chunk_type = ChunkType::AHED;
148 ///
149 /// assert_eq!(chunk_type.len(), 4);
150 /// ```
151 #[allow(clippy::len_without_is_empty)]
152 #[inline]
153 pub const fn len(&self) -> usize {
154 self.0.len()
155 }
156
157 /// Creates private [ChunkType].
158 ///
159 /// # Errors
160 ///
161 /// This function will return an error in the following cases:
162 /// - Value contains non-ASCII alphabet characters
163 /// - The second character is not lowercase
164 /// - The third character is not uppercase
165 ///
166 /// # Examples
167 ///
168 /// ```rust
169 /// # use libpna::{ChunkType, ChunkTypeError};
170 /// assert!(ChunkType::private(*b"myTy").is_ok());
171 /// assert_eq!(
172 /// ChunkType::private(*b"zeR\0").unwrap_err(),
173 /// ChunkTypeError::NonAsciiAlphabetic
174 /// );
175 /// assert_eq!(
176 /// ChunkType::private(*b"pRIv").unwrap_err(),
177 /// ChunkTypeError::NonPrivateChunkType
178 /// );
179 /// assert_eq!(
180 /// ChunkType::private(*b"rese").unwrap_err(),
181 /// ChunkTypeError::Reserved
182 /// );
183 /// ```
184 #[inline]
185 pub const fn private(ty: [u8; 4]) -> Result<Self, ChunkTypeError> {
186 // NOTE: use a while statement for const context.
187 let mut idx = 0;
188 while idx < ty.len() {
189 if !ty[idx].is_ascii_alphabetic() {
190 return Err(ChunkTypeError::NonAsciiAlphabetic);
191 }
192 idx += 1;
193 }
194 if !ty[1].is_ascii_lowercase() {
195 return Err(ChunkTypeError::NonPrivateChunkType);
196 }
197 if !ty[2].is_ascii_uppercase() {
198 return Err(ChunkTypeError::Reserved);
199 }
200 Ok(Self(ty))
201 }
202
203 /// Creates a custom [`ChunkType`] without validation.
204 ///
205 /// # Panics
206 /// Panics if the chunk type contains non-UTF-8 characters and it is
207 /// formatted with `Display`.
208 /// ```no_run
209 /// # use libpna::ChunkType;
210 ///
211 /// let custom_chunk_type = unsafe { ChunkType::from_unchecked([0xe3, 0x81, 0x82, 0xe3]) };
212 /// format!("{}", custom_chunk_type);
213 /// ```
214 ///
215 /// # Safety
216 /// Callers must ensure the value consists only of ASCII alphabetic
217 /// characters ('a'..'z' and 'A'..'Z').
218 /// ```rust
219 /// # use libpna::ChunkType;
220 ///
221 /// let custom_chunk_type = unsafe { ChunkType::from_unchecked(*b"myTy") };
222 /// format!("{}", custom_chunk_type);
223 /// ```
224 #[inline]
225 pub const unsafe fn from_unchecked(ty: [u8; 4]) -> Self {
226 Self(ty)
227 }
228
229 // -- Chunk type determination --
230
231 /// Returns true if the chunk is critical.
232 #[inline]
233 pub const fn is_critical(&self) -> bool {
234 self.0[0] & 32 == 0
235 }
236
237 /// Returns true if the chunk is private.
238 #[inline]
239 pub const fn is_private(&self) -> bool {
240 self.0[1] & 32 != 0
241 }
242
243 /// Checks whether the reserved bit of the chunk name is set.
244 /// If it is set, the chunk name is invalid.
245 #[inline]
246 pub const fn is_set_reserved(&self) -> bool {
247 self.0[2] & 32 != 0
248 }
249
250 /// Returns true if the chunk is safe to copy if unknown.
251 #[inline]
252 pub const fn is_safe_to_copy(&self) -> bool {
253 self.0[3] & 32 != 0
254 }
255}
256
257impl Debug for ChunkType {
258 #[inline]
259 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
260 struct DebugType([u8; 4]);
261
262 impl Debug for DebugType {
263 #[inline]
264 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
265 for &c in &self.0[..] {
266 write!(f, "{}", char::from(c).escape_debug())?;
267 }
268 Ok(())
269 }
270 }
271
272 f.debug_struct("ChunkType")
273 .field("type", &DebugType(self.0))
274 .field("critical", &self.is_critical())
275 .field("private", &self.is_private())
276 .field("reserved", &self.is_set_reserved())
277 .field("safe_to_copy", &self.is_safe_to_copy())
278 .finish()
279 }
280}
281
282impl Display for ChunkType {
283 #[inline]
284 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
285 // SAFETY: A field checked to be ASCII alphabetic in the constructor.
286 debug_assert!(
287 self.0.iter().all(|b| b.is_ascii_alphabetic()),
288 "ChunkType invariant violated: contains non-ASCII alphabetic bytes {:?}",
289 self.0
290 );
291 Display::fmt(unsafe { std::str::from_utf8_unchecked(&self.0) }, f)
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
299 use wasm_bindgen_test::wasm_bindgen_test as test;
300
301 #[test]
302 fn to_string() {
303 assert_eq!("AHED", ChunkType::AHED.to_string());
304 }
305
306 #[test]
307 fn is_critical() {
308 assert!(ChunkType::AHED.is_critical());
309 assert!(!ChunkType::cTIM.is_critical());
310 }
311
312 #[test]
313 fn is_private() {
314 assert!(!ChunkType::AHED.is_private());
315 assert!(ChunkType::private(*b"myTy").unwrap().is_private());
316 }
317
318 #[test]
319 fn is_set_reserved() {
320 assert!(!ChunkType::AHED.is_set_reserved());
321 }
322
323 #[test]
324 fn is_safe_to_copy() {
325 assert!(!ChunkType::AHED.is_safe_to_copy());
326 }
327}