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}