popcap_pak/
entry.rs

1use crate::FileTime;
2use std::borrow::Cow;
3use std::io::Cursor;
4use std::io::Read;
5
6/// An Entry, representative of a file inside a pakfile.
7#[derive(Debug)]
8pub struct Entry<'a> {
9    /// The "path" of the file.
10    ///
11    /// The encoding is unspecified,
12    /// but this is only ascii in PopCap Games.
13    pub(crate) path: Box<[u8]>,
14    pub(crate) file_time: FileTime,
15    pub(crate) data: Cursor<Cow<'a, [u8]>>,
16}
17
18impl<'a> Entry<'a> {
19    /// Get the starting index into the path variable of the name of the file, if it exists.
20    ///
21    /// # Returns
22    /// If the path only contains the file name, this returns `None`.
23    fn get_file_name_start_index(&self) -> Option<usize> {
24        self.path
25            .iter()
26            .rev()
27            .position(|&b| b == b'/' || b == b'\\')
28            .map(|index| self.path.len() - index)
29    }
30
31    /// Get the directory of the file, or [`None`] if it doesn't exist.
32    pub fn dir(&self) -> Option<&[u8]> {
33        let index = self.get_file_name_start_index()?.saturating_sub(1);
34        Some(&self.path[..index])
35    }
36
37    /// The same as [`Self::dir`],
38    /// but tries to make the byte slice a str.
39    ///
40    /// This is provided as most paths are ascii.
41    pub fn dir_str(&self) -> Option<Result<&str, std::str::Utf8Error>> {
42        self.dir().map(std::str::from_utf8)
43    }
44
45    /// Get the name of the file.
46    pub fn name(&self) -> &[u8] {
47        let index = self.get_file_name_start_index().unwrap_or(0);
48        &self.path[index..]
49    }
50
51    /// The same as [`Self::name`],
52    /// but tries to make the byte slice a str.
53    ///
54    /// This is provided as most paths are ascii.
55    pub fn name_str(&self) -> Result<&str, std::str::Utf8Error> {
56        std::str::from_utf8(self.name())
57    }
58
59    /// Get the entire path of the file.
60    pub fn path(&self) -> &[u8] {
61        &self.path
62    }
63
64    /// The same as [`Self::path`],
65    /// but tries to make the byte slice a str.
66    ///
67    /// This is provided as most paths are ascii.
68    pub fn path_str(&self) -> Result<&str, std::str::Utf8Error> {
69        std::str::from_utf8(self.path())
70    }
71
72    /// Get the [`FileTime`].
73    ///
74    /// This represents access and modification time; there is no distinction.
75    pub fn file_time(&self) -> FileTime {
76        self.file_time
77    }
78
79    /// Get an iterator over all the bytes in the file.
80    pub fn iter_data(&self) -> impl Iterator<Item = u8> + '_ {
81        let borrowed = self.is_borrowed();
82
83        self.data
84            .get_ref()
85            .iter()
86            .copied()
87            .map(move |b| if borrowed { b ^ 0xf7 } else { b })
88    }
89
90    /// Check if the file is borrowed aka does not own its data.
91    pub fn is_borrowed(&self) -> bool {
92        match self.data.get_ref() {
93            Cow::Owned(_) => false,
94            Cow::Borrowed(_) => true,
95        }
96    }
97
98    /// Get the filesize.
99    pub fn size(&self) -> usize {
100        self.data.get_ref().len()
101    }
102
103    /// Decrypt the data and take ownership if it is borrowed.
104    ///
105    /// Position data of the file is lost.
106    pub fn into_owned(mut self) -> Entry<'static> {
107        Entry {
108            path: self.path,
109            file_time: self.file_time,
110            data: {
111                // TODO: Preserve position data. Just reuse the old cursor, swap new data in/out.
112                match self.data.get_mut() {
113                    Cow::Owned(data) => Cursor::new(std::mem::take(data).into()),
114                    Cow::Borrowed(data) => Cursor::new(data.iter().map(|b| b ^ 0xf7).collect()),
115                }
116            },
117        }
118    }
119}
120
121impl<'a> Read for Entry<'a> {
122    fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
123        let n = self.data.read(buf)?;
124        if self.is_borrowed() {
125            buf.iter_mut().for_each(|el| *el ^= 0xf7);
126        }
127        Ok(n)
128    }
129}
130
131impl<'a> PartialEq for Entry<'a> {
132    fn eq(&self, other: &Self) -> bool {
133        self.path == other.path // TODO: Consider not including filetime/path checks
134            && self.file_time == other.file_time
135            && self.iter_data().eq(other.iter_data())
136    }
137}