wpress_oxide/
reader.rs

1use crate::common::{ExtractError, FileParseError, Header, HeaderError, EOF_BLOCK, HEADER_SIZE};
2use clean_path::Clean;
3use std::{
4    fs::{create_dir_all, File},
5    io::{self, Read, Seek, SeekFrom},
6    path::{Path, PathBuf, StripPrefixError},
7};
8
9/// Structure that can read, parse and extract a wpress archive file.
10pub struct Reader {
11    file: std::fs::File,
12    headers: Vec<Header>,
13}
14
15fn trim_clean<P: AsRef<Path>>(path: P) -> Result<PathBuf, StripPrefixError> {
16    let cleaned = path.as_ref().clean();
17    if cleaned.starts_with("/") {
18        return Ok(cleaned.strip_prefix("/")?.to_path_buf());
19    }
20    Ok(cleaned)
21}
22
23impl Reader {
24    /// Creates a new `Reader` with the path supplied as the source file.
25    pub fn new<P: AsRef<Path>>(path: P) -> Result<Reader, FileParseError> {
26        let mut file = std::fs::File::open(path)?;
27        let mut headers = Vec::new();
28        let mut buf = vec![0; HEADER_SIZE];
29        loop {
30            if HEADER_SIZE != file.read(&mut buf)? {
31                Err(FileParseError::Header(HeaderError::IncompleteHeader))?;
32            }
33            if EOF_BLOCK == buf {
34                break;
35            }
36            let header = Header::from_bytes(&buf)?;
37            let next_header = header.size as i64;
38            headers.push(header);
39            file.seek(SeekFrom::Current(next_header))?;
40        }
41        Ok(Reader { file, headers })
42    }
43
44    /// Extracts all the files inside the archive to the provided destination directory.
45    ///
46    /// # Example
47    ///
48    /// ```
49    /// # use std::fs::remove_dir_all;
50    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
51    /// use wpress_oxide::Reader;
52    /// let mut r = Reader::new("tests/reader/archive.wpress")?;
53    /// r.extract_to("tests/reader_output_0")?;
54    /// #    remove_dir_all("tests/reader_output_0")?;
55    /// #    Ok(())
56    /// # }
57    /// ```
58    pub fn extract_to<P: AsRef<Path>>(&mut self, destination: P) -> Result<(), ExtractError> {
59        let destination = destination.as_ref();
60        self.file.rewind()?;
61        for header in self.headers.iter() {
62            self.file.seek(io::SeekFrom::Current(HEADER_SIZE as i64))?;
63            let clean = trim_clean([&header.prefix, &header.name].iter().collect::<PathBuf>())?;
64            let path = Path::new(destination).join(clean);
65            let dir = path.parent().unwrap_or(Path::new(destination));
66            create_dir_all(dir)?;
67            let mut handle = File::create(path)?;
68            io::copy(&mut (&mut self.file).take(header.size), &mut handle)?;
69        }
70        Ok(())
71    }
72
73    /// Extracts all the files inside the archive to the current directory.
74    ///
75    /// # Example
76    ///
77    /// ```
78    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
79    /// use wpress_oxide::Reader;
80    /// let mut r = Reader::new("tests/reader/archive.wpress")?;
81    /// r.extract()?;
82    /// #    Ok(())
83    /// # }
84    /// ```
85    pub fn extract(&mut self) -> Result<(), ExtractError> {
86        self.extract_to(".")
87    }
88
89    /// Returns number of files in the current archive.
90    pub fn files_count(&self) -> usize {
91        self.headers.len()
92    }
93
94    /// Returns a borrowed header slice with metadata about the files in the archive.
95    pub fn headers(&self) -> &[Header] {
96        &self.headers
97    }
98
99    /// Returns a copied vector of headers or metadata about the files in the archive.
100    pub fn headers_owned(&self) -> Vec<Header> {
101        self.headers.clone()
102    }
103
104    /// Extract a single file, given either its name or *complete path inside the archive*, to a
105    /// destination directory. Preserves the directory hierarchy of the archive during extraction.
106    ///
107    /// # Examples
108    ///
109    /// ## Extract all files from the archive that match a filename
110    ///
111    /// ```
112    /// # use std::fs::remove_dir_all;
113    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
114    /// use wpress_oxide::Reader;
115    /// let mut r = Reader::new("tests/reader/archive.wpress")?;
116    /// r.extract_file("file.txt", "tests/reader_output_1")?;
117    /// #    remove_dir_all("tests/reader_output_1")?;
118    /// #    Ok(())
119    /// # }
120    /// ```
121    ///
122    /// ## Extract a file with a specific path in the archive
123    ///
124    /// ```
125    /// # use std::fs::remove_dir_all;
126    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
127    /// use wpress_oxide::Reader;
128    /// let mut r = Reader::new("tests/reader/archive.wpress")?;
129    /// r.extract_file(
130    ///     "tests/writer/directory/subdirectory/file.txt",
131    ///     "tests/reader_output_2",
132    /// )?;
133    /// #    remove_dir_all("tests/reader_output_2")?;
134    /// #    Ok(())
135    /// # }
136    /// ```
137
138    pub fn extract_file<P: AsRef<Path>>(
139        &mut self,
140        filename: P,
141        destination: P,
142    ) -> Result<(), ExtractError> {
143        self.file.rewind()?;
144        let mut offset = 0;
145        let filename = filename.as_ref();
146        let destination = destination.as_ref();
147
148        for header in self.headers.iter() {
149            offset += HEADER_SIZE as u64;
150            let original_path = [&header.prefix, &header.name].iter().collect::<PathBuf>();
151            let clean = trim_clean(&original_path)?;
152
153            if Path::new(&header.name) == filename || clean == filename || original_path == filename
154            {
155                let path = destination.join(clean);
156                let dir = path.parent().unwrap_or(destination);
157                create_dir_all(dir)?;
158                let mut handle = File::create(path)?;
159                self.file.seek(SeekFrom::Start(offset))?;
160                io::copy(&mut (&mut self.file).take(header.size), &mut handle)?;
161                break;
162            }
163
164            offset += header.size;
165        }
166        Ok(())
167    }
168}