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