unrar-async 0.1.12

List and extract .rar archives, async
Documentation
use std::ffi::CStr;
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::path::PathBuf;
use std::pin::Pin;
use std::ptr;
use std::sync::Arc;

use futures::Future;
use futures::stream::Stream;
use futures::stream::StreamExt;
use futures::task::Context;
use futures::task::Poll;
use tracing::instrument;

use crate::error::Code;
use crate::error::RarError;
use crate::error::When;
use crate::error::Error;
//use crate::flags::ArchiveFlags;
use crate::flags::OpenMode;
use crate::flags::Operation;
//use crate::flags::VolumeInfo;

mod data;
pub use data::Entry;
use data::Handle;
use data::HeaderData;
use data::OpenArchiveData;

#[cfg(feature = "async-std")]
#[inline]
async fn spawn_blocking<T: Send + 'static>(func: impl FnOnce() -> T + Send + 'static) -> Result<T, Error> {
	Ok(async_std::task::spawn_blocking(func).await)
}

#[cfg(feature = "tokio")]
#[inline]
async fn spawn_blocking<T: Send + 'static>(func: impl FnOnce() -> T + Send + 'static) -> Result<T, Error> {
	Ok(tokio::task::spawn_blocking(func).await?)
}

#[derive(Clone, Copy)]
struct UserData(*mut [u8; 256]);
unsafe impl Send for UserData {}
unsafe impl Sync for UserData {}

impl Default for UserData {
	#[inline]
	fn default() -> Self {
		let allocation = Box::new([0u8; 256]);
		Self(Box::into_raw(allocation))
	}
}

/// Represents an archive that is open for operation
pub struct OpenArchive {
	path: PathBuf,
	handle: Arc<Handle>,
	operation: Operation,
	destination: Option<CString>,
	damaged: bool,
	//flags: ArchiveFlags,
	#[allow(clippy::type_complexity)]
	current_future: Option<Pin<Box<dyn Future<Output = Result<Entry, Error>> + Send>>>,
	userdata: UserData
}

impl OpenArchive {
	#[instrument(err, level = "info", skip(password))]
	pub(crate) async fn open(filename: &Path, mode: OpenMode, password: Option<CString>, destination: Option<&Path>, operation: Operation) -> Result<Self, Error> {
		let destination = destination.map(path_to_cstring).map_or(Ok(None), |r| r.map(Some)).map_err(Error::from)?;
		//let filename = WideCStr::from_os_str(filename).unwrap(); // Already checked by Archive::new()
		let data = OpenArchiveData::new(path_to_cstring(filename)?, mode as u32);
		let (handle, data) = spawn_blocking(move || {
			let p = unsafe { unrar_sys::RAROpenArchive(&mut data.as_ffi() as *mut _) } as *mut unrar_sys::HANDLE;
			match p.is_null() {
				false => Ok((Arc::new(Handle::from_ffi(p)), data)),
				true => Err(Error::NulHandle)
			}
		}).await??;
		let result = Code::try_from(data.open_result).or(Err(RarError::InvalidCode(data.open_result)))?;

		if let Some(pw) = password {
			unsafe { unrar_sys::RARSetPassword(handle.as_ffi(), pw.as_ptr() as *const _) }
		}

		match result {
			Code::Success => Ok(Self::new(filename.into(), handle, operation, destination)),
			e => Err(Error::Rar(RarError::from((e, When::Open))))
		}
	}

	fn new(path: PathBuf, handle: Arc<Handle>, operation: Operation, destination: Option<CString>) -> Self {
		let mut this = Self{
			path,
			handle,
			operation,
			destination,
			damaged: false,
			current_future: None,
			userdata: UserData::default()
		};
		this.queue_next_future();
		this
	}

	/// Process the archive in full; collect and return the results
	#[instrument(err, level = "info", skip(self), fields(archive.path = %self.path.display(), archive.operation = ?self.operation, archive.destination = ?self.destination))]
	pub async fn process(&mut self) -> Result<Vec<Entry>, Error> {
		let mut results = Vec::new();
		while let Some(item) = self.next().await {
			results.push(item?);
		}
		Ok(results)
	}

	extern "C" fn callback(msg: unrar_sys::UINT, user_data: unrar_sys::LPARAM, p1: unrar_sys::LPARAM, p2: unrar_sys::LPARAM) -> std::os::raw::c_int {
		match msg {
			unrar_sys::UCM_CHANGEVOLUME => {
				let ptr = p1 as *const _;
				let next = std::str::from_utf8(unsafe { CStr::from_ptr(ptr) }.to_bytes()).unwrap();
				let our_option = unsafe { &mut *(user_data as *mut Option<String>) };
				*our_option = Some(String::from(next));
				match p2 {
					// Next volume not found; -1 means stop
					unrar_sys::RAR_VOL_ASK => -1,
					// Next volume found; 1 means continue
					_ => 1
				}
			},
			_ => 0
		}
	}

	#[inline]
	fn queue_next_future(&mut self) {
		self.current_future = Some(Box::pin(next_entry(self.handle.clone(), self.operation, self.destination.clone(), self.userdata)));
	}

	/*
	#[inline]
	pub fn is_locked(&self) -> bool {
		self.flags.contains(ArchiveFlags::LOCK)
	}

	#[inline]
	pub fn has_encrypted_headers(&self) -> bool {
		self.flags.contains(ArchiveFlags::ENC_HEADERS)
	}

	#[inline]
	pub fn has_recovery_record(&self) -> bool {
		self.flags.contains(ArchiveFlags::RECOVERY)
	}

	#[inline]
	pub fn has_comment(&self) -> bool {
		self.flags.contains(ArchiveFlags::COMMENT)
	}

	#[inline]
	/// Solid archive; all files are in a single compressed block
	pub fn is_solid(&self) -> bool {
		self.flags.contains(ArchiveFlags::SOLID)
	}

	#[inline]
	/// Indicates whether or not the archive file is split into multiple volumes, and - if so - whether or not the file is the first volume
	pub fn volume_info(&self) -> VolumeInfo {
		if(self.flags.contains(ArchiveFlags::FIRST_VOLUME)) {
			VolumeInfo::First
		} else if(self.flags.contains(ArchiveFlags::VOLUME)) {
			VolumeInfo::Subsequent
		} else {
			VolumeInfo::None
		}
	}
	*/
}

async fn next_entry(handle: Arc<Handle>, operation: Operation, destination: Option<CString>, userdata: UserData) -> Result<Entry, Error> {
	unsafe {
		unrar_sys::RARSetCallback(handle.as_ffi(), OpenArchive::callback, userdata.0 as unrar_sys::LPARAM);
	}

	let read_result: Result<(Code, HeaderData), Error> = {
		let handle = handle.clone();
		let mut header = HeaderData::default();
		spawn_blocking(move || unsafe {
			let read_result = unrar_sys::RARReadHeader(handle.as_ffi(), &mut header as *mut _ as *mut _) as u32;
			match Code::try_from(read_result) {
				Ok(code) => Ok((code, header)),
				Err(_) => Err(RarError::InvalidCode(read_result).into())
			}
		}).await?
	};
	let (code, header) = read_result?;

	let process_result = match code {
		Code::Success => {
			let result = spawn_blocking(move || unsafe {
				let process_result = unrar_sys::RARProcessFile(
					handle.as_ffi(),
					operation as i32,
					destination.as_ref().map(|s| s.as_ptr() as *const _).unwrap_or(ptr::null()),
					ptr::null()
				) as u32;
				Code::try_from(process_result).or(Err(RarError::InvalidCode(process_result)))
			}).await?;
			Ok(result?)
		},
		Code::EndArchive => Err(RarError::EndArchive),
		c => Err(RarError::from((c, When::Read)))
	}?;

	match process_result {
		Code::Success => Ok(Entry::try_from(header)?),
		c => Err(RarError::from((c, When::Process)).into())
	}
}

impl Stream for OpenArchive {
	type Item = Result<Entry, Error>;

	#[instrument(level = "trace", skip(self, ctx), fields(archive.path = %self.path.display(), archive.operation = ?self.operation, archive.destination = ?self.destination))]
	#[inline]
	fn poll_next(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
		if(self.damaged) {
			return Poll::Ready(None);
		}

		match self.current_future.as_mut() {
			Some(current_future) => match Pin::new(current_future).poll(ctx) {
				Poll::Pending => Poll::Pending,
				Poll::Ready(Ok(v)) => {
					self.queue_next_future();
					Poll::Ready(Some(Ok(v)))
				},
				Poll::Ready(Err(Error::Rar(RarError::EndArchive))) => {
					self.current_future = None;
					Poll::Ready(None)
				},
				Poll::Ready(Err(e)) => {
					self.damaged = true;
					self.current_future = None;
					Poll::Ready(Some(Err(e)))
				}
			},
			None => Poll::Ready(None)
		}
	}
}

impl Drop for OpenArchive {
	#[inline]
	fn drop(&mut self) {
		unsafe { unrar_sys::RARCloseArchive(self.handle.as_ffi()) };
		unsafe { Box::from_raw(self.userdata.0) };
	}
}

fn path_to_cstring(input: &Path) -> Result<CString, std::ffi::FromVecWithNulError> {
	let mut bytes = Vec::from(input.as_os_str().as_bytes());
	bytes.push(0);
	CString::from_vec_with_nul(bytes)
}