#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
Compressed { before: u64, after: u64 },
NoGain { before: u64, after: u64 },
AlreadyCompressed { before: u64 },
Unsupported { reason: UnsupportedReason },
Skipped { reason: SkipReason },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnsupportedReason {
Filesystem,
NetworkOrOverlay,
PlatformBuild,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkipReason {
PermissionDenied,
Busy,
Immutable,
Encrypted,
IntegrityRevert,
NotLoadable,
TooLarge,
GateExcluded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Support {
Supported,
AlreadyCompressed,
Unsupported(UnsupportedReason),
}
#[derive(Debug)]
pub enum Error {
Io {
context: &'static str,
source: std::io::Error,
},
NotFound(std::path::PathBuf),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Io { context, source } => write!(f, "io error at {context}: {source}"),
Error::NotFound(p) => write!(f, "file not found: {}", p.display()),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io { source, .. } => Some(source),
Error::NotFound(_) => None,
}
}
}
pub(crate) fn io(context: &'static str) -> Error {
Error::Io {
context,
source: std::io::Error::last_os_error(),
}
}
#[cfg(unix)]
pub(crate) fn cstring(path: &Path) -> Result<std::ffi::CString, Error> {
use std::os::unix::ffi::OsStrExt;
std::ffi::CString::new(path.as_os_str().as_bytes()).map_err(|_| Error::Io {
context: "path has interior NUL",
source: std::io::Error::from(std::io::ErrorKind::InvalidInput),
})
}
pub fn probe(path: &Path) -> Result<Support, Error> {
backend::detect(path)
}
pub fn compress_file(path: &Path) -> Result<Outcome, Error> {
compress_file_with(&Os, path)
}
fn compress_file_with<B: Backend>(backend: &B, path: &Path) -> Result<Outcome, Error> {
if !path.exists() {
return Err(Error::NotFound(path.to_path_buf()));
}
match backend.detect(path)? {
Support::Unsupported(reason) => Ok(Outcome::Unsupported { reason }),
Support::AlreadyCompressed => Ok(Outcome::AlreadyCompressed {
before: verify::on_disk_bytes(path)?,
}),
Support::Supported => safety::apply_guarded(backend, path),
}
}
pub fn compress_bytes(path: &Path, content: &[u8], gate: &Gate) -> Result<Outcome, Error> {
compress_bytes_with(&Os, path, content, gate)
}
fn compress_bytes_with<B: Backend>(
backend: &B,
path: &Path,
content: &[u8],
gate: &Gate,
) -> Result<Outcome, Error> {
let name = path.to_string_lossy();
let normalized = name.replace('\\', "/");
if !gate.matches(&normalized, content.len() as u64) {
plain_write(path, content)?;
return Ok(Outcome::Skipped {
reason: SkipReason::GateExcluded,
});
}
let probe_target = if path.exists() {
path.to_path_buf()
} else {
match path.parent() {
Some(dir) => dir.to_path_buf(),
None => path.to_path_buf(),
}
};
match backend.detect(&probe_target) {
Ok(Support::Supported) => match safety::compress_bytes_guarded(backend, path, content) {
Ok(Outcome::Skipped { .. }) | Err(_) => {
plain_write(path, content)?;
Ok(Outcome::Skipped {
reason: SkipReason::IntegrityRevert,
})
}
other => other,
},
Ok(Support::AlreadyCompressed) | Ok(Support::Unsupported(_)) | Err(_) => {
plain_write(path, content)?;
Ok(Outcome::Unsupported {
reason: UnsupportedReason::Filesystem,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CopyOutcome {
Cloned { compressed: bool },
CopiedCompressed { before: u64, after: u64 },
CopiedPlain { skipped: Option<SkipReason> },
}
pub fn try_clone_file(src: &Path, dest: &Path) -> Result<bool, Error> {
if !src.exists() {
return Err(Error::NotFound(src.to_path_buf()));
}
Os.clone_file(src, dest)
}
pub fn copy_file(src: &Path, dest: &Path) -> Result<CopyOutcome, Error> {
copy_file_with(&Os, src, dest)
}
fn copy_file_with<B: Backend>(backend: &B, src: &Path, dest: &Path) -> Result<CopyOutcome, Error> {
if !src.exists() {
return Err(Error::NotFound(src.to_path_buf()));
}
if dest.exists() {
if is_same_file(src, dest) {
return Ok(CopyOutcome::Cloned {
compressed: backend.is_already_compressed(src).unwrap_or(false),
});
}
std::fs::remove_file(dest).map_err(|source| Error::Io {
context: "replace existing destination",
source,
})?;
}
let compressed_src = backend.is_already_compressed(src).unwrap_or(false);
if backend.clone_file(src, dest)? {
return Ok(CopyOutcome::Cloned {
compressed: compressed_src,
});
}
let content = std::fs::read(src).map_err(|source| Error::Io {
context: "read copy source",
source,
})?;
let mode = std::fs::metadata(src).ok().map(|meta| meta.permissions());
if !compressed_src {
plain_write(dest, &content)?;
if let Some(mode) = mode {
let _ = std::fs::set_permissions(dest, mode);
}
return Ok(CopyOutcome::CopiedPlain { skipped: None });
}
let outcome = compress_bytes_with(backend, dest, &content, &Gate::any())?;
if let Some(mode) = mode {
let _ = std::fs::set_permissions(dest, mode);
}
Ok(match outcome {
Outcome::Compressed { before, after } | Outcome::NoGain { before, after } => {
CopyOutcome::CopiedCompressed { before, after }
}
Outcome::AlreadyCompressed { before } => CopyOutcome::CopiedCompressed {
before,
after: before,
},
Outcome::Unsupported { .. } => CopyOutcome::CopiedPlain { skipped: None },
Outcome::Skipped { reason } => CopyOutcome::CopiedPlain {
skipped: Some(reason),
},
})
}
#[cfg(unix)]
fn is_same_file(a: &Path, b: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
match (std::fs::metadata(a), std::fs::metadata(b)) {
(Ok(meta_a), Ok(meta_b)) => meta_a.dev() == meta_b.dev() && meta_a.ino() == meta_b.ino(),
_ => false,
}
}
#[cfg(windows)]
fn is_same_file(a: &Path, b: &Path) -> bool {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION,
};
fn identity(path: &Path) -> Option<(u32, u64)> {
let file = std::fs::File::open(path).ok()?;
let mut info = unsafe { std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>() };
if unsafe { GetFileInformationByHandle(file.as_raw_handle() as _, &mut info) } == 0 {
return None;
}
Some((
info.dwVolumeSerialNumber,
(u64::from(info.nFileIndexHigh) << 32) | u64::from(info.nFileIndexLow),
))
}
match (identity(a), identity(b)) {
(Some(id_a), Some(id_b)) => id_a == id_b,
_ => match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
(Ok(canon_a), Ok(canon_b)) => canon_a == canon_b,
_ => false,
},
}
}
#[cfg(not(any(unix, windows)))]
fn is_same_file(a: &Path, b: &Path) -> bool {
match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
(Ok(canon_a), Ok(canon_b)) => canon_a == canon_b,
_ => false,
}
}
fn plain_write(path: &Path, content: &[u8]) -> Result<(), Error> {
use std::io::Write;
let dir = path.parent().ok_or_else(|| Error::Io {
context: "no parent dir",
source: std::io::Error::from(std::io::ErrorKind::InvalidInput),
})?;
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "addon".to_string());
let tmp = dir.join(format!(".{name}.plain-{}.tmp", std::process::id()));
let res = (|| -> std::io::Result<()> {
let mut file = std::fs::File::create(&tmp)?;
file.write_all(content)?;
file.sync_all()
})();
if let Err(source) = res {
let _ = std::fs::remove_file(&tmp);
return Err(Error::Io {
context: "plain write temp",
source,
});
}
std::fs::rename(&tmp, path).map_err(|source| {
let _ = std::fs::remove_file(&tmp);
Error::Io {
context: "plain write rename",
source,
}
})
}
pub struct Stat {
pub compressed: bool,
pub logical: u64,
pub physical: u64,
}
pub fn stat(path: &Path) -> Result<Stat, Error> {
stat_with(&Os, path)
}
fn stat_with<B: Backend>(backend: &B, path: &Path) -> Result<Stat, Error> {
let meta = std::fs::metadata(path).map_err(|source| Error::Io {
context: "stat",
source,
})?;
let logical = meta.len();
#[cfg(unix)]
let physical = {
use std::os::unix::fs::MetadataExt;
meta.blocks().saturating_mul(512)
};
#[cfg(not(unix))]
let physical = verify::on_disk_bytes(path)?;
let compressed = match backend.compressed_on_disk(path) {
Ok(Some(signal)) => signal,
Ok(None) | Err(_) => logical > 0 && physical < logical,
};
Ok(Stat {
compressed,
logical,
physical,
})
}
#[cfg(feature = "addon")]
pub mod addon;
#[cfg(feature = "exe")]
pub mod exe;
mod gate;
mod remove;
mod safety;
mod stream;
mod verify;
pub use gate::{Gate, GateParseError, SizePredicate, DEFAULT_GLOB};
pub use remove::{rm, RmOptions};
pub use stream::DecmpfsWriter;
#[cfg(target_os = "linux")]
#[path = "linux.rs"]
mod backend;
#[cfg(target_os = "macos")]
#[path = "macos.rs"]
mod backend;
#[cfg(target_os = "windows")]
#[path = "windows.rs"]
mod backend;
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
#[path = "unsupported.rs"]
mod backend;
#[cfg_attr(test, mockall::automock)]
pub(crate) trait Backend {
fn detect(&self, path: &Path) -> Result<Support, Error>;
fn is_already_compressed(&self, path: &Path) -> Result<bool, Error>;
fn apply_inplace(&self, path: &Path, snapshot: &[u8]) -> Result<(), Error>;
fn apply_bytes(
&self,
path: &Path,
content: &[u8],
mode: Option<std::fs::Permissions>,
) -> Result<(), Error>;
fn compressed_on_disk(&self, path: &Path) -> Result<Option<bool>, Error>;
fn clone_file(&self, _src: &Path, _dest: &Path) -> Result<bool, Error> {
Ok(false)
}
}
pub(crate) struct Os;
impl Backend for Os {
fn detect(&self, path: &Path) -> Result<Support, Error> {
backend::detect(path)
}
fn is_already_compressed(&self, path: &Path) -> Result<bool, Error> {
backend::is_already_compressed(path)
}
fn apply_inplace(&self, path: &Path, snapshot: &[u8]) -> Result<(), Error> {
backend::apply_inplace(path, snapshot)
}
fn apply_bytes(
&self,
path: &Path,
content: &[u8],
mode: Option<std::fs::Permissions>,
) -> Result<(), Error> {
backend::apply_bytes(path, content, mode)
}
fn compressed_on_disk(&self, path: &Path) -> Result<Option<bool>, Error> {
backend::compressed_on_disk(path)
}
fn clone_file(&self, src: &Path, dest: &Path) -> Result<bool, Error> {
backend::clone_file(src, dest)
}
}
#[cfg(test)]
pub(crate) struct FakeBackend {
pub(crate) detect: Support,
pub(crate) apply_error: Option<std::io::ErrorKind>,
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
impl FakeBackend {
fn apply_result(&self) -> Result<(), Error> {
match self.apply_error {
None => Ok(()),
Some(kind) => Err(Error::Io {
context: "fake apply",
source: std::io::Error::from(kind),
}),
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
impl Backend for FakeBackend {
fn detect(&self, _path: &Path) -> Result<Support, Error> {
Ok(self.detect)
}
fn is_already_compressed(&self, _path: &Path) -> Result<bool, Error> {
Ok(false)
}
fn apply_inplace(&self, _path: &Path, _snapshot: &[u8]) -> Result<(), Error> {
self.apply_result()
}
fn apply_bytes(
&self,
_path: &Path,
_content: &[u8],
_mode: Option<std::fs::Permissions>,
) -> Result<(), Error> {
self.apply_result()
}
fn compressed_on_disk(&self, _path: &Path) -> Result<Option<bool>, Error> {
Ok(Some(false))
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests;