1#![allow(unused_parens)]
2
3#[cfg(not(any(feature = "tokio", feature = "async-std")))]
4compile_error!("Either feature \"tokio\" or \"async-std\" must be enabled for unrar-async.");
5
6use std::borrow::Cow;
7use std::ffi::CString;
8use std::fmt::Write;
9use std::path::Path;
10use std::path::PathBuf;
11
12use compact_str::format_compact;
13use compact_str::CompactString;
14use tracing::instrument;
15use widestring::WideCString;
16
17pub mod error;
18pub use error::Error;
19use error::NulError;
20
21mod consts;
22pub use consts::RE_MULTIPART_EXTENSION;
23pub use consts::RE_EXTENSION;
24mod flags;
25pub use flags::ArchiveFlags;
26pub use flags::OpenMode;
27pub use flags::Operation;
28pub use flags::VolumeInfo;
29mod open_archive;
30pub use open_archive::Entry;
31pub use open_archive::OpenArchive;
32
33pub struct Archive<'a> {
34 filename: Cow<'a, Path>,
35 password: Option<CString>,
36 comments: Option<&'a mut [u8]>
37}
38
39impl<'a> Archive<'a> {
40 #[instrument(err, level = "info", skip(file), fields(archive.file = %file.as_ref().display()))]
42 pub fn new<F>(file: &'a F) -> Result<Self, Error>
43 where
44 F: AsRef<Path> + ?Sized
45 {
46 WideCString::from_os_str(file.as_ref()).map_err(NulError::from)?;
47 Ok(Self{
48 filename: Cow::Borrowed(file.as_ref()),
49 password: None,
50 comments: None
51 })
52 }
53
54 #[instrument(err, level = "debug", skip(file, password), fields(archive.file = %file.as_ref().display()))]
56 pub fn with_password<F, P>(file: &'a F, password: &'a P) -> Result<Self, Error>
57 where
58 F: AsRef<Path> + ?Sized,
59 P: AsRef<str> + ?Sized
60 {
61 let mut this = Self::new(file)?;
62 this.password = Some(CString::new(password.as_ref()).map_err(NulError::from)?);
63 Ok(this)
64 }
65
66 pub fn set_comments(&mut self, comments: &'a mut [u8]) {
69 self.comments = Some(comments)
70 }
71
72 #[inline]
76 pub fn is_archive(&self) -> bool {
77 is_archive(&self.filename)
78 }
79
80 #[inline]
84 pub fn is_multipart(&self) -> bool {
85 is_multipart(&self.filename)
86 }
87
88 pub fn try_all_parts(&self) -> Option<PathBuf> {
92 get_rar_extension(&self.filename).and_then(|full_ext| {
93 RE_MULTIPART_EXTENSION.captures(&full_ext).map(|captures| {
94 let mut replacement = String::from(captures.get(1).unwrap().as_str());
95 replacement.push_str(&"?".repeat(captures.get(2).unwrap().as_str().len()));
96 replacement.push_str(captures.get(3).unwrap().as_str());
97 full_ext.replace(captures.get(0).unwrap().as_str(), &replacement)
98 })
99 }).and_then(|new_ext| {
100 self.filename.file_stem().map(|s| Path::new(s).with_extension(&new_ext[1..]))
101 })
102 }
103
104 pub fn all_parts(&self) -> PathBuf {
108 self.try_all_parts().unwrap_or_else(|| self.filename.to_path_buf())
109 }
110
111 pub fn nth_part(&self, n: usize) -> Option<PathBuf> {
115 get_rar_extension(&self.filename).and_then(|full_ext| {
116 RE_MULTIPART_EXTENSION.captures(&full_ext).map(|captures| {
117 let mut replacement = String::from(captures.get(1).unwrap().as_str());
118 write!(replacement, "{:01$}", n, captures.get(2).unwrap().as_str().len()).unwrap();
120 replacement.push_str(captures.get(3).unwrap().as_str());
121 full_ext.replace(captures.get(0).unwrap().as_str(), &replacement)
122 })
123 }).and_then(|new_ext| {
124 self.filename.file_stem().map(|s| Path::new(s).with_extension(&new_ext[1..]))
125 })
126 }
127
128 pub fn first_part(&self) -> PathBuf {
132 self.nth_part(1).unwrap_or_else(|| self.filename.to_path_buf())
133 }
134
135 pub fn as_first_part(&mut self) {
139 if let Some(first_part) = self.nth_part(1) {
140 self.filename = Cow::Owned(first_part);
141 }
142 }
143
144 #[instrument(err, level = "info", skip(self), fields(archive.file = %self.filename.as_ref().display()))]
146 pub async fn list(self) -> Result<OpenArchive, Error> {
147 self.open(OpenMode::List, None, Operation::Skip).await
148 }
149
150 #[instrument(err, level = "info", skip(self), fields(archive.file = %self.filename.as_ref().display()))]
152 pub async fn list_split(self) -> Result<OpenArchive, Error> {
153 self.open(OpenMode::ListSplit, None, Operation::Skip).await
154 }
155
156 #[instrument(err, level = "info", skip(self, path), fields(archive.file = %self.filename.as_ref().display(), target = %path.as_ref().display()))]
158 pub async fn extract_to(self, path: impl AsRef<Path>) -> Result<OpenArchive, Error> {
159 self.open(OpenMode::Extract, Some(path.as_ref()), Operation::Extract).await
160 }
161
162 #[instrument(err, level = "info", skip(self), fields(archive.file = %self.filename.as_ref().display()))]
164 pub async fn test(self) -> Result<OpenArchive, Error> {
165 self.open(OpenMode::Extract, None, Operation::Test).await
166 }
167
168 async fn open(self, mode: OpenMode, path: Option<&Path>, operation: Operation) -> Result<OpenArchive, Error> {
170 OpenArchive::open(&self.filename, mode, self.password, path, operation).await
171 }
172}
173
174#[inline]
175fn get_rar_extension(path: &Path) -> Option<CompactString> {
176 path.extension().map(|ext| {
177 match path.file_stem().and_then(|s| Path::new(s).extension()) {
178 Some(pre_ext) => format_compact!(".{}.{}", pre_ext.to_string_lossy(), ext.to_string_lossy()),
179 None => format_compact!(".{}", ext.to_string_lossy())
180 }
181 })
182}
183
184#[inline]
185fn is_archive(path: &Path) -> bool {
186 get_rar_extension(path).and_then(|full_ext| {
187 RE_EXTENSION.find(&full_ext).map(|_| ())
188 }).is_some()
189}
190
191#[inline]
192fn is_multipart(path: &Path) -> bool {
193 get_rar_extension(path).and_then(|full_ext| {
194 RE_MULTIPART_EXTENSION.find(&full_ext).map(|_| ())
195 }).is_some()
196}
197
198#[cfg(test)]
199mod tests {
200 use std::path::PathBuf;
201 use super::Archive;
202
203 #[test]
204 fn glob() {
205 assert_eq!(Archive::new("arc.part0010.rar".into()).unwrap().all_parts(), PathBuf::from("arc.part????.rar"));
206 assert_eq!(Archive::new("archive.r100".into()).unwrap().all_parts(), PathBuf::from("archive.r???"));
207 assert_eq!(Archive::new("archive.r9".into()).unwrap().all_parts(), PathBuf::from("archive.r?"));
208 assert_eq!(Archive::new("archive.999".into()).unwrap().all_parts(), PathBuf::from("archive.???"));
209 assert_eq!(Archive::new("archive.rar".into()).unwrap().all_parts(), PathBuf::from("archive.rar"));
210 assert_eq!(Archive::new("random_string".into()).unwrap().all_parts(), PathBuf::from("random_string"));
211 assert_eq!(Archive::new("v8/v8.rar".into()).unwrap().all_parts(), PathBuf::from("v8/v8.rar"));
212 assert_eq!(Archive::new("v8/v8".into()).unwrap().all_parts(), PathBuf::from("v8/v8"));
213 }
214
215 #[test]
216 fn first_part() {
217 assert_eq!(Archive::new("arc.part0010.rar".into()).unwrap().first_part(), PathBuf::from("arc.part0001.rar"));
218 assert_eq!(Archive::new("archive.r100".into()).unwrap().first_part(), PathBuf::from("archive.r001"));
219 assert_eq!(Archive::new("archive.r9".into()).unwrap().first_part(), PathBuf::from("archive.r1"));
220 assert_eq!(Archive::new("archive.999".into()).unwrap().first_part(), PathBuf::from("archive.001"));
221 assert_eq!(Archive::new("archive.rar".into()).unwrap().first_part(), PathBuf::from("archive.rar"));
222 assert_eq!(Archive::new("random_string".into()).unwrap().first_part(), PathBuf::from("random_string"));
223 assert_eq!(Archive::new("v8/v8.rar".into()).unwrap().first_part(), PathBuf::from("v8/v8.rar"));
224 assert_eq!(Archive::new("v8/v8".into()).unwrap().first_part(), PathBuf::from("v8/v8"));
225 }
226
227 #[test]
228 fn is_archive() {
229 assert_eq!(super::is_archive(&PathBuf::from("archive.rar")), true);
230 assert_eq!(super::is_archive(&PathBuf::from("archive.part1.rar")), true);
231 assert_eq!(super::is_archive(&PathBuf::from("archive.part100.rar")), true);
232 assert_eq!(super::is_archive(&PathBuf::from("archive.r10")), true);
233 assert_eq!(super::is_archive(&PathBuf::from("archive.part1rar")), false);
234 assert_eq!(super::is_archive(&PathBuf::from("archive.rar\n")), false);
235 assert_eq!(super::is_archive(&PathBuf::from("archive.zip")), false);
236 }
237}
238