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}