unrar_async/
lib.rs

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	/// Creates an `Archive` object to operate on a plain RAR archive
41	#[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	/// Creates an `Archive` object to operate on a password-encrypted RAR archive.
55	#[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	/// Set the comment buffer of the underlying archive.
67	/// Note:  Comments are not yet implemented; this function will have no effect at this time.
68	pub fn set_comments(&mut self, comments: &'a mut [u8]) {
69		self.comments = Some(comments)
70	}
71
72	/// Returns `true` if the filename matches a RAR archive; `false` otherwise.
73	///
74	/// This method does not perform any filesystem operations; it operates purely on the string filename.
75	#[inline]
76	pub fn is_archive(&self) -> bool {
77		is_archive(&self.filename)
78	}
79
80	/// Returns `true` if the filename matches a part of a multipart collection; `false` otherwise.
81	///
82	/// This method does not perform any filesystem operations; it operates purely on the string filename.
83	#[inline]
84	pub fn is_multipart(&self) -> bool {
85		is_multipart(&self.filename)
86	}
87
88	/// Returns a glob string covering all parts of the multipart collection, or `None` if the archive is single-part.
89	///
90	/// This method does not make any filesystem operations; it operates purely on the string filename.
91	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	/// Returns a glob string covering all parts of the multipart collection, or a copy of the archive's entire filename if it's a single-part archive.
105	///
106	/// This method does not make any filesystem operations; it operates purely on the string filename.
107	pub fn all_parts(&self) -> PathBuf {
108		self.try_all_parts().unwrap_or_else(|| self.filename.to_path_buf())
109	}
110
111	/// Returns the nth part of this multi-part collection, or `None` if the archive is single-part.
112	///
113	/// This method does not make any filesystem operations; it operates purely on the string filename.
114	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				// `n` left-padded with zeroes to the length of the archive's numbers (e.g. 3 for part001.rar)
119				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	/// Returns the first part of the multi-part collection, or a copy of the underlying archive's filename if the archive is single-part.
129	///
130	/// This method does not make any filesystem operations; it operates purely on the string filename.
131	pub fn first_part(&self) -> PathBuf {
132		self.nth_part(1).unwrap_or_else(|| self.filename.to_path_buf())
133	}
134
135	/// Changes the filename to point to the first part of the multipart collection.  Does nothing if the archive is single-part.
136	///
137	/// This method does not make any filesystem operations; it operates purely on the string filename.
138	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	/// Opens the underlying archive for listing its contents
145	#[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	/// Opens the underlying archive for listing its contents without omitting or pooling split entries
151	#[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	/// Opens the underlying archive for extracting to the given directory
157	#[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	/// Opens the underlying archive for testing
163	#[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	/// Opens the underlying archive
169	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