use std::os::fd::AsRawFd;
use std::path::Path;
use crate::{cstring, io, Error, Support, UnsupportedReason};
const BTRFS_SUPER_MAGIC: i64 = 0x9123_683E;
const FS_IOC_GETFLAGS: u32 = 0x8008_6601;
const FS_IOC_SETFLAGS: u32 = 0x4008_6602;
const FS_IOC_FIEMAP: u32 = 0xC020_660B;
const FIEMAP_FLAG_SYNC: u32 = 0x0001;
const FIEMAP_EXTENT_ENCODED: u32 = 0x0008;
const FS_COMPR_FL: libc::c_long = 0x0000_0004;
fn statfs_type(path: &Path) -> Result<i64, Error> {
let cpath = cstring(path)?;
let mut buf: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statfs(cpath.as_ptr(), &mut buf) } != 0 {
return Err(io("statfs"));
}
Ok(buf.f_type as i64)
}
fn get_flags(fd: libc::c_int) -> Result<libc::c_long, Error> {
let mut flags: libc::c_long = 0;
if unsafe { libc::ioctl(fd, FS_IOC_GETFLAGS as _, &mut flags) } != 0 {
return Err(io("FS_IOC_GETFLAGS"));
}
Ok(flags)
}
const PREFERRED_ALGOS: [&[u8]; 3] = [b"zstd", b"lzo", b"zlib"];
fn request_codec(fd: libc::c_int) -> Result<(), Error> {
for algo in PREFERRED_ALGOS {
let rc = unsafe {
libc::fsetxattr(
fd,
c"btrfs.compression".as_ptr(),
algo.as_ptr().cast(),
algo.len(),
0,
)
};
if rc == 0 {
return Ok(());
}
}
let flags = get_flags(fd)? | FS_COMPR_FL;
if unsafe { libc::ioctl(fd, FS_IOC_SETFLAGS as _, &flags) } != 0 {
return Err(io("FS_IOC_SETFLAGS"));
}
Ok(())
}
pub(crate) fn prepare_stream(file: &std::fs::File) -> Result<(), Error> {
request_codec(file.as_raw_fd())
}
pub(crate) fn detect(path: &Path) -> Result<Support, Error> {
if statfs_type(path)? == BTRFS_SUPER_MAGIC {
Ok(Support::Supported)
} else {
Ok(Support::Unsupported(UnsupportedReason::Filesystem))
}
}
#[repr(C)]
#[derive(Clone, Copy)]
struct FiemapExtent {
fe_logical: u64,
fe_physical: u64,
fe_length: u64,
fe_reserved64: [u64; 2],
fe_flags: u32,
fe_reserved: [u32; 3],
}
#[repr(C)]
struct FiemapHeader {
fm_start: u64,
fm_length: u64,
fm_flags: u32,
fm_mapped_extents: u32,
fm_extent_count: u32,
fm_reserved: u32,
}
const _: () = assert!(std::mem::size_of::<FiemapHeader>() == 32);
const _: () = assert!(std::mem::size_of::<FiemapExtent>() == 56);
fn compressed_via_fiemap(path: &Path) -> Result<bool, Error> {
const COUNT: usize = 64;
let file = std::fs::File::open(path).map_err(|source| Error::Io {
context: "open for fiemap",
source,
})?;
let mut buf =
vec![0u8; std::mem::size_of::<FiemapHeader>() + COUNT * std::mem::size_of::<FiemapExtent>()];
unsafe {
let header = buf.as_mut_ptr().cast::<FiemapHeader>();
(*header).fm_start = 0;
(*header).fm_length = u64::MAX;
(*header).fm_flags = FIEMAP_FLAG_SYNC;
(*header).fm_extent_count = COUNT as u32;
(*header).fm_mapped_extents = 0;
(*header).fm_reserved = 0;
if libc::ioctl(file.as_raw_fd(), FS_IOC_FIEMAP as _, buf.as_mut_ptr()) != 0 {
return Err(io("FS_IOC_FIEMAP"));
}
let mapped = (*header).fm_mapped_extents as usize;
let extents = std::slice::from_raw_parts(
buf
.as_ptr()
.add(std::mem::size_of::<FiemapHeader>())
.cast::<FiemapExtent>(),
mapped.min(COUNT),
);
Ok(
extents
.iter()
.any(|e| e.fe_flags & FIEMAP_EXTENT_ENCODED != 0),
)
}
}
pub(crate) fn compressed_on_disk(path: &Path) -> Result<Option<bool>, Error> {
Ok(Some(compressed_via_fiemap(path)?))
}
pub(crate) fn is_already_compressed(path: &Path) -> Result<bool, Error> {
let file = std::fs::File::open(path).map_err(|source| Error::Io {
context: "open",
source,
})?;
Ok(get_flags(file.as_raw_fd())? & FS_COMPR_FL != 0)
}
pub(crate) fn apply_inplace(path: &Path, snapshot: &[u8]) -> Result<(), Error> {
let mode = std::fs::metadata(path).map(|m| m.permissions()).ok();
apply_bytes(path, snapshot, mode)
}
fn unique_tmp(dir: &Path, name: &str) -> std::path::PathBuf {
static TMP_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = TMP_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
dir.join(format!(
".{name}.decmpfs-{}-{nanos}-{seq}.tmp",
std::process::id()
))
}
pub(crate) fn apply_bytes(
path: &Path,
content: &[u8],
mode: Option<std::fs::Permissions>,
) -> 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_or_else(
|| std::borrow::Cow::Borrowed("addon"),
|n| n.to_string_lossy(),
);
let tmp = unique_tmp(dir, &name);
let result = (|| {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&tmp)
.map_err(|source| Error::Io {
context: "create temp",
source,
})?;
let fd = file.as_raw_fd();
request_codec(fd)?;
file.write_all(content).map_err(|source| Error::Io {
context: "write temp",
source,
})?;
file.sync_all().map_err(|source| Error::Io {
context: "fsync temp",
source,
})?;
Ok(())
})();
if result.is_ok() {
if let Some(perm) = mode {
let _ = std::fs::set_permissions(&tmp, perm);
}
if let Ok(meta) = std::fs::metadata(path) {
use std::os::unix::fs::MetadataExt;
let _ = std::os::unix::fs::chown(&tmp, Some(meta.uid()), Some(meta.gid()));
}
}
match result {
Ok(()) => std::fs::rename(&tmp, path).map_err(|source| {
let _ = std::fs::remove_file(&tmp);
Error::Io {
context: "rename",
source,
}
}),
Err(err) => {
let _ = std::fs::remove_file(&tmp);
Err(err)
}
}
}
pub(crate) fn clone_file(src: &Path, dest: &Path) -> Result<bool, Error> {
use std::os::fd::AsRawFd;
let src_file = match std::fs::File::open(src) {
Ok(file) => file,
Err(_) => return Ok(false),
};
let dest_file = match std::fs::File::create(dest) {
Ok(file) => file,
Err(_) => return Ok(false),
};
const FICLONE: libc::c_ulong = 0x4004_9409;
let cloned = unsafe { libc::ioctl(dest_file.as_raw_fd(), FICLONE, src_file.as_raw_fd()) } == 0;
if !cloned {
drop(dest_file);
let _ = std::fs::remove_file(dest);
}
Ok(cloned)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
fn scratch(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("decmpfs-linux-{tag}-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn on_btrfs(dir: &Path) -> bool {
let probe = dir.join(".btrfs-probe");
std::fs::write(&probe, b"x").unwrap();
let yes = matches!(detect(&probe), Ok(Support::Supported));
std::fs::remove_file(&probe).ok();
yes
}
#[test]
fn unique_tmp_never_collides_and_is_well_formed() {
let dir = Path::new("/tmp");
let a = unique_tmp(dir, "addon.node");
let b = unique_tmp(dir, "addon.node");
assert_ne!(a, b, "successive temps must differ (the seq counter)");
for p in [&a, &b] {
let f = p.file_name().unwrap().to_string_lossy();
assert!(
f.starts_with(".addon.node.decmpfs-"),
"unexpected temp name: {f}"
);
assert!(f.ends_with(".tmp"), "unexpected temp name: {f}");
assert_eq!(p.parent().unwrap(), dir);
}
}
#[test]
fn detect_is_ok_on_a_regular_temp_path() {
assert!(detect(&std::env::temp_dir()).is_ok());
}
#[test]
fn a_fresh_plain_file_is_not_already_compressed() {
let dir = scratch("iac");
let path = dir.join("f");
std::fs::write(&path, b"plain").unwrap();
assert!(!is_already_compressed(&path).unwrap_or(false));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn clone_file_declines_a_missing_source() {
let dir = scratch("clone");
assert!(!clone_file(&dir.join("missing"), &dir.join("dest")).unwrap());
assert!(!dir.join("dest").exists(), "the empty dest is cleaned up");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn apply_bytes_round_trips_and_concurrent_writers_converge_on_btrfs() {
let dir = scratch("rt");
if !on_btrfs(&dir) {
std::fs::remove_dir_all(&dir).ok();
return;
}
let content = vec![0xEEu8; 128 * 1024];
let dest = dir.join("addon.node");
std::thread::scope(|s| {
for _ in 0..8 {
let dest = dest.clone();
let content = content.clone();
s.spawn(move || {
let _ = apply_bytes(&dest, &content, None);
});
}
});
assert_eq!(
std::fs::read(&dest).unwrap(),
content,
"bytes survive the race"
);
assert!(
is_already_compressed(&dest).unwrap(),
"landed btrfs-compressed"
);
std::fs::remove_dir_all(&dir).ok();
}
}