use std::path::Path;
use crate::{verify, Backend, Error, Outcome, SkipReason};
pub(crate) fn apply_guarded<B: Backend>(backend: &B, path: &Path) -> Result<Outcome, Error> {
if backend.is_already_compressed(path)? {
return Ok(Outcome::AlreadyCompressed {
before: verify::on_disk_bytes(path)?,
});
}
let before = verify::on_disk_bytes(path)?;
let magic_before = verify::magic_prefix(path)?;
let snapshot = std::fs::read(path).map_err(|source| Error::Io {
context: "snapshot",
source,
})?;
if let Err(err) = backend.apply_inplace(path, &snapshot) {
if let Error::Io { source, .. } = &err {
if let Some(reason) = classify_skip(source) {
return Ok(Outcome::Skipped { reason });
}
}
return Err(err);
}
verify_loadable_or_restore(backend, path, before, magic_before, &snapshot)
}
fn verify_loadable_or_restore<B: Backend>(
backend: &B,
path: &Path,
before: u64,
magic_before: [u8; 4],
snapshot: &[u8],
) -> Result<Outcome, Error> {
if verify::magic_prefix(path)? != magic_before {
restore(path, snapshot)?;
return Ok(classify_outcome(false, before, before, None));
}
let after = verify::on_disk_bytes(path)?;
Ok(classify_outcome(
true,
before,
after,
backend.compressed_on_disk(path)?,
))
}
fn classify_outcome(loadable: bool, before: u64, after: u64, signal: Option<bool>) -> Outcome {
if !loadable {
return Outcome::Skipped {
reason: SkipReason::NotLoadable,
};
}
if signal.unwrap_or(after < before) {
Outcome::Compressed { before, after }
} else {
Outcome::NoGain { before, after }
}
}
fn classify_skip(err: &std::io::Error) -> Option<SkipReason> {
if err.kind() == std::io::ErrorKind::PermissionDenied {
return Some(SkipReason::PermissionDenied);
}
classify_errno(err.raw_os_error()?)
}
#[cfg(not(windows))]
fn classify_errno(code: i32) -> Option<SkipReason> {
match code {
1 | 13 | 30 => Some(SkipReason::PermissionDenied), 16 | 26 => Some(SkipReason::Busy), 27 => Some(SkipReason::TooLarge), _ => None,
}
}
#[cfg(windows)]
fn classify_errno(code: i32) -> Option<SkipReason> {
match code {
5 | 19 => Some(SkipReason::PermissionDenied), 32 | 33 => Some(SkipReason::Busy), _ => None,
}
}
pub(crate) fn compress_bytes_guarded<B: Backend>(
backend: &B,
path: &Path,
content: &[u8],
) -> Result<Outcome, Error> {
if let Err(err) = backend.apply_bytes(path, content, None) {
if let Error::Io { source, .. } = &err {
if let Some(reason) = classify_skip(source) {
return Ok(Outcome::Skipped { reason });
}
}
return Err(err);
}
verify_readback_or_restore(backend, path, content)
}
fn verify_readback_or_restore<B: Backend>(
backend: &B,
path: &Path,
content: &[u8],
) -> Result<Outcome, Error> {
let after = verify::on_disk_bytes(path)?;
if !verify::readback_matches(path, content)? {
restore(path, content)?;
return Ok(Outcome::Skipped {
reason: SkipReason::IntegrityRevert,
});
}
let before = content.len() as u64;
Ok(classify_outcome(
true,
before,
after,
backend.compressed_on_disk(path)?,
))
}
fn restore(path: &Path, bytes: &[u8]) -> Result<(), Error> {
use std::io::Write;
let dir = path.parent().ok_or_else(|| Error::Io {
context: "rollback restore: path has no parent",
source: std::io::Error::from(std::io::ErrorKind::InvalidInput),
})?;
let tmp = dir.join(format!(".decmpfs-restore-{}.tmp", std::process::id()));
let wrote = std::fs::File::create(&tmp)
.and_then(|mut file| {
file.write_all(bytes)?;
file.sync_all()
})
.and_then(|()| std::fs::rename(&tmp, path));
wrote.map_err(|source| {
let _ = std::fs::remove_file(&tmp);
Error::Io {
context: "rollback restore",
source,
}
})
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::{FakeBackend, Os, Support};
fn err(kind: std::io::ErrorKind) -> std::io::Error {
std::io::Error::from(kind)
}
#[test]
fn apply_guarded_propagates_an_unclassifiable_apply_error() {
let dir = std::env::temp_dir().join(format!("decmpfs-broken-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("f.bin");
std::fs::write(&path, b"\x7fELF readable original").unwrap();
let backend = FakeBackend {
detect: Support::Supported,
apply_error: Some(std::io::ErrorKind::NotFound),
};
let out = apply_guarded(&backend, &path);
assert!(matches!(out, Err(Error::Io { .. })), "got {out:?}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn permission_errors_become_skipped() {
assert_eq!(
classify_skip(&err(std::io::ErrorKind::PermissionDenied)),
Some(SkipReason::PermissionDenied)
);
#[cfg(not(windows))]
let os_errors = [1, 13, 30]; #[cfg(windows)]
let os_errors = [5, 19]; for code in os_errors {
assert_eq!(
classify_skip(&std::io::Error::from_raw_os_error(code)),
Some(SkipReason::PermissionDenied),
"OS error {code}"
);
}
}
#[test]
fn busy_errors_become_skipped() {
#[cfg(not(windows))]
let os_errors = [16, 26]; #[cfg(windows)]
let os_errors = [32, 33]; for code in os_errors {
assert_eq!(
classify_skip(&std::io::Error::from_raw_os_error(code)),
Some(SkipReason::Busy),
"OS error {code}"
);
}
}
#[cfg(not(windows))]
#[test]
fn efbig_becomes_too_large() {
assert_eq!(
classify_skip(&std::io::Error::from_raw_os_error(27)), Some(SkipReason::TooLarge)
);
}
#[test]
fn classify_outcome_covers_every_branch() {
use crate::Outcome;
assert!(matches!(
classify_outcome(false, 100, 50, None),
Outcome::Skipped {
reason: SkipReason::NotLoadable
}
));
assert!(matches!(
classify_outcome(true, 100, 40, None),
Outcome::Compressed {
before: 100,
after: 40
}
));
assert!(matches!(
classify_outcome(true, 100, 100, None),
Outcome::NoGain { .. }
));
assert!(matches!(
classify_outcome(true, 100, 100, Some(true)),
Outcome::Compressed { .. }
));
assert!(matches!(
classify_outcome(true, 100, 40, Some(false)),
Outcome::NoGain { .. }
));
}
#[test]
fn restore_writes_the_snapshot_back() {
let dir = std::env::temp_dir().join(format!("decmpfs-restore-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("f");
std::fs::write(&path, b"corrupted-by-a-broken-backend").unwrap();
restore(&path, b"the original loadable bytes").unwrap();
assert_eq!(
std::fs::read(&path).unwrap(),
b"the original loadable bytes"
);
std::fs::remove_dir_all(&dir).ok();
}
#[cfg(target_os = "macos")]
#[test]
fn compress_bytes_guarded_propagates_an_unclassifiable_error() {
let out = compress_bytes_guarded(
&Os,
std::path::Path::new("/no/such/decmpfs/dir/x.node"),
b"data",
);
assert!(matches!(out, Err(Error::Io { .. })));
}
#[test]
fn compress_bytes_guarded_success_classifies_via_the_backend_signal() {
let dir = std::env::temp_dir().join(format!("decmpfs-ok-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("f.bin");
let content = b"the stored content bytes, pre-seeded";
std::fs::write(&path, content).unwrap();
let backend = FakeBackend {
detect: Support::Supported,
apply_error: None,
};
let out = compress_bytes_guarded(&backend, &path, content).unwrap();
assert!(matches!(out, Outcome::NoGain { .. }), "got {out:?}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn unrelated_errors_propagate() {
assert_eq!(classify_skip(&err(std::io::ErrorKind::NotFound)), None);
assert_eq!(classify_skip(&std::io::Error::from_raw_os_error(2)), None); }
#[test]
fn restore_errors_when_the_path_has_no_parent() {
assert!(restore(std::path::Path::new("/"), b"x").is_err());
}
#[test]
fn not_loadable_result_is_restored_and_skipped() {
let dir = std::env::temp_dir().join(format!("decmpfs-notload-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("f");
std::fs::write(&path, b"\x7fELF garbage the backend supposedly produced").unwrap();
let out = verify_loadable_or_restore(
&Os,
&path,
100,
[0xde, 0xad, 0xbe, 0xef],
b"the original bytes",
)
.unwrap();
assert!(matches!(
out,
Outcome::Skipped {
reason: SkipReason::NotLoadable
}
));
assert_eq!(
std::fs::read(&path).unwrap(),
b"the original bytes",
"snapshot restored"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn read_back_mismatch_is_restored_and_skipped() {
let dir = std::env::temp_dir().join(format!("decmpfs-mismatch-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("f");
std::fs::write(&path, b"what the broken backend actually wrote").unwrap();
let intended = b"the bytes the caller asked to store";
let out = verify_readback_or_restore(&Os, &path, intended).unwrap();
assert!(matches!(
out,
Outcome::Skipped {
reason: SkipReason::IntegrityRevert
}
));
assert_eq!(std::fs::read(&path).unwrap(), intended, "content restored");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn restore_cleans_up_its_temp_when_the_rename_fails() {
let dir = std::env::temp_dir().join(format!("decmpfs-rr-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let target = dir.join("a-dir");
std::fs::create_dir_all(&target).unwrap();
assert!(
restore(&target, b"bytes").is_err(),
"rename-over-dir must Err"
);
let tmp = dir.join(format!(".decmpfs-restore-{}.tmp", std::process::id()));
assert!(!tmp.exists(), "temp left behind");
assert!(target.is_dir(), "directory target untouched");
std::fs::remove_dir_all(&dir).ok();
}
}