use std::os::fd::AsRawFd;
use std::path::Path;
use crate::{cstring, io, Error, Support, UnsupportedReason};
const UF_COMPRESSED: u32 = 0x0000_0020;
const DECMPFS_MAGIC: u32 = 0x636d_7066; const BLOCK: usize = 0x1_0000; const XATTR_NOFOLLOW: libc::c_int = 0x0001;
const COMPRESSION_LZVN: i32 = 0x900;
const COMPRESSION_LZFSE: i32 = 0x801;
pub(crate) const STREAMING_THRESHOLD: usize = 64 * 1024 * 1024;
fn should_stream_resource_fork(raw_len: usize, threshold: usize) -> bool {
raw_len > threshold
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Codec {
Lzvn,
Lzfse,
}
impl Codec {
const fn compression_type(self) -> u32 {
match self {
Self::Lzvn => 8,
Self::Lzfse => 12,
}
}
const fn algorithm(self) -> i32 {
match self {
Self::Lzvn => COMPRESSION_LZVN,
Self::Lzfse => COMPRESSION_LZFSE,
}
}
}
#[link(name = "compression")]
extern "C" {
fn compression_decode_buffer(
dst_buffer: *mut u8,
dst_size: usize,
src_buffer: *const u8,
src_size: usize,
scratch_buffer: *mut u8,
algorithm: i32,
) -> usize;
fn compression_encode_buffer(
dst_buffer: *mut u8,
dst_size: usize,
src_buffer: *const u8,
src_size: usize,
scratch_buffer: *mut u8,
algorithm: i32,
) -> usize;
fn compression_encode_scratch_buffer_size(algorithm: i32) -> usize;
}
fn resource_fork_too_large() -> Error {
Error::Io {
context: "decmpfs resource fork exceeds u32 offsets",
source: std::io::Error::from_raw_os_error(libc::EFBIG),
}
}
fn statfs(path: &Path) -> Result<libc::statfs, 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)
}
pub(crate) fn detect(path: &Path) -> Result<Support, Error> {
let buf = statfs(path)?;
let name: Vec<u8> = buf
.f_fstypename
.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect();
Ok(classify_fs(
buf.f_flags & (libc::MNT_LOCAL as u32) != 0,
&name,
))
}
fn classify_fs(is_local: bool, fstype: &[u8]) -> Support {
if !is_local {
return Support::Unsupported(UnsupportedReason::NetworkOrOverlay);
}
if fstype == b"apfs" || fstype == b"hfs" {
Support::Supported
} else {
Support::Unsupported(UnsupportedReason::Filesystem)
}
}
fn st_flags(path: &Path) -> Result<u32, Error> {
let cpath = cstring(path)?;
let mut st: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::lstat(cpath.as_ptr(), &mut st) } != 0 {
return Err(io("lstat"));
}
Ok(st.st_flags)
}
pub(crate) fn is_already_compressed(path: &Path) -> Result<bool, Error> {
Ok(st_flags(path)? & UF_COMPRESSED != 0)
}
pub(crate) fn compressed_on_disk(path: &Path) -> Result<Option<bool>, Error> {
Ok(Some(is_already_compressed(path)?))
}
fn compress_block_with_codec(src: &[u8], scratch: &mut [u8], codec: Codec) -> Option<Vec<u8>> {
let mut dst = vec![0u8; src.len() + src.len() / 16 + 1024];
let n = unsafe {
compression_encode_buffer(
dst.as_mut_ptr(),
dst.len(),
src.as_ptr(),
src.len(),
scratch.as_mut_ptr(),
codec.algorithm(),
)
};
if n == 0 {
return None;
}
dst.truncate(n);
Some(dst)
}
#[cfg(test)]
fn compress_block(src: &[u8], scratch: &mut [u8]) -> Option<Vec<u8>> {
compress_block_with_codec(src, scratch, Codec::Lzvn)
}
#[derive(Debug, PartialEq, Eq)]
enum ResourceForkPlan {
Plain,
Compressed { table_len: usize, total_len: usize },
}
fn resource_fork_table_len(num_blocks: usize) -> Result<usize, Error> {
num_blocks
.checked_add(1)
.and_then(|entries| entries.checked_mul(std::mem::size_of::<u32>()))
.ok_or_else(resource_fork_too_large)
}
fn plan_resource_fork(
raw_len: usize,
num_blocks: usize,
encoded_len: usize,
) -> Result<ResourceForkPlan, Error> {
let table_len = resource_fork_table_len(num_blocks)?;
let total_len = table_len
.checked_add(encoded_len)
.ok_or_else(resource_fork_too_large)?;
if total_len >= raw_len {
return Ok(ResourceForkPlan::Plain);
}
if total_len > u32::MAX as usize {
return Err(resource_fork_too_large());
}
Ok(ResourceForkPlan::Compressed {
table_len,
total_len,
})
}
fn compress_blocks(raw: &[u8], codec: Codec) -> Option<Vec<Vec<u8>>> {
let num_blocks = raw.len().div_ceil(BLOCK).max(1);
let scratch_len = unsafe { compression_encode_scratch_buffer_size(codec.algorithm()) };
let workers = if std::env::var_os("DECMPFS_SERIAL").is_some() {
1
} else {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
.min(num_blocks)
};
if workers <= 1 || num_blocks < 8 {
let mut scratch = vec![0u8; scratch_len];
return raw
.chunks(BLOCK)
.map(|chunk| compress_block_with_codec(chunk, &mut scratch, codec))
.collect();
}
let bytes_per_worker = num_blocks.div_ceil(workers) * BLOCK;
let parts: Vec<Option<Vec<Vec<u8>>>> = std::thread::scope(|scope| {
let handles: Vec<_> = raw
.chunks(bytes_per_worker)
.map(|region| {
scope.spawn(move || {
let mut scratch = vec![0u8; scratch_len];
region
.chunks(BLOCK)
.map(|chunk| compress_block_with_codec(chunk, &mut scratch, codec))
.collect::<Option<Vec<Vec<u8>>>>()
})
})
.collect();
handles
.into_iter()
.map(|handle| handle.join().ok().flatten())
.collect()
});
let mut out = Vec::with_capacity(num_blocks);
for part in parts {
out.extend(part?);
}
Some(out)
}
fn build_resource_fork_with_codec(raw: &[u8], codec: Codec) -> Result<Option<Vec<u8>>, Error> {
let num_blocks = raw.len().div_ceil(BLOCK).max(1);
let Some(blocks) = compress_blocks(raw, codec) else {
return Ok(None);
};
let encoded_len = blocks
.iter()
.try_fold(0usize, |sum, block| sum.checked_add(block.len()))
.ok_or_else(resource_fork_too_large)?;
let ResourceForkPlan::Compressed {
table_len,
total_len,
} = plan_resource_fork(raw.len(), num_blocks, encoded_len)?
else {
return Ok(None);
};
let mut out = Vec::with_capacity(total_len);
let mut offset = u32::try_from(table_len).map_err(|_| resource_fork_too_large())?;
out.extend_from_slice(&offset.to_le_bytes());
for block in &blocks {
offset = offset
.checked_add(u32::try_from(block.len()).map_err(|_| resource_fork_too_large())?)
.ok_or_else(resource_fork_too_large)?;
out.extend_from_slice(&offset.to_le_bytes());
}
for block in &blocks {
out.extend_from_slice(block);
}
debug_assert_eq!(out.len(), total_len);
Ok(Some(out))
}
#[cfg(test)]
fn build_resource_fork(raw: &[u8]) -> Result<Option<Vec<u8>>, Error> {
build_resource_fork_with_codec(raw, Codec::Lzvn)
}
struct InMemoryResourceFork {
codec: Codec,
bytes: Vec<u8>,
}
fn build_in_memory_resource_fork(raw: &[u8]) -> Result<Option<InMemoryResourceFork>, Error> {
for codec in [Codec::Lzvn, Codec::Lzfse] {
if let Some(bytes) = build_resource_fork_with_codec(raw, codec)? {
return Ok(Some(InMemoryResourceFork { codec, bytes }));
}
}
Ok(None)
}
fn write_streaming_resource_fork(path: &Path, raw: &[u8], codec: Codec) -> Result<bool, Error> {
use std::io::{Seek, Write};
use std::sync::atomic::{AtomicBool, Ordering};
let num_blocks = raw.len().div_ceil(BLOCK).max(1);
let table_len = resource_fork_table_len(num_blocks)?;
if table_len >= raw.len() || table_len > u32::MAX as usize {
return Ok(false);
}
let fork_path = path.join("..namedfork").join("rsrc");
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(fork_path)
.map_err(|source| Error::Io {
context: "open resource fork",
source,
})?;
file.set_len(table_len as u64).map_err(|source| Error::Io {
context: "reserve resource-fork table",
source,
})?;
file
.seek(std::io::SeekFrom::Start(table_len as u64))
.map_err(|source| Error::Io {
context: "seek resource-fork payload",
source,
})?;
let mut writer = std::io::BufWriter::with_capacity(1 << 20, file);
let workers = if std::env::var_os("DECMPFS_SERIAL").is_some() {
1
} else {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
.min(num_blocks)
};
let scratch_len = unsafe { compression_encode_scratch_buffer_size(codec.algorithm()) };
let cancelled = AtomicBool::new(false);
let mut offsets = Vec::with_capacity(num_blocks + 1);
offsets.push(u32::try_from(table_len).map_err(|_| resource_fork_too_large())?);
let mut offset = table_len;
let won = std::thread::scope(|scope| -> Result<bool, Error> {
let mut receivers = Vec::with_capacity(workers);
for worker in 0..workers {
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
receivers.push(receiver);
let cancelled = &cancelled;
scope.spawn(move || {
let mut scratch = vec![0u8; scratch_len];
let mut block_index = worker;
while block_index < num_blocks && !cancelled.load(Ordering::Relaxed) {
let start = block_index * BLOCK;
let end = start.saturating_add(BLOCK).min(raw.len());
let encoded = compress_block_with_codec(&raw[start..end], &mut scratch, codec);
if sender.send(encoded).is_err() {
break;
}
block_index += workers;
}
});
}
let result = (|| -> Result<bool, Error> {
for block_index in 0..num_blocks {
let Some(block) = receivers[block_index % workers].recv().ok().flatten() else {
return Ok(false);
};
let Some(next_offset) = offset.checked_add(block.len()) else {
return Ok(false);
};
if next_offset >= raw.len() || next_offset > u32::MAX as usize {
return Ok(false);
}
writer.write_all(&block).map_err(|source| Error::Io {
context: "write resource-fork block",
source,
})?;
offset = next_offset;
offsets.push(u32::try_from(offset).map_err(|_| resource_fork_too_large())?);
}
Ok(true)
})();
cancelled.store(true, Ordering::Relaxed);
drop(receivers);
result
})?;
if !won {
return Ok(false);
}
debug_assert_eq!(offsets.len(), num_blocks + 1);
let mut table = Vec::with_capacity(table_len);
for offset in offsets {
table.extend_from_slice(&offset.to_le_bytes());
}
debug_assert_eq!(table.len(), table_len);
writer
.seek(std::io::SeekFrom::Start(0))
.and_then(|_| writer.write_all(&table))
.and_then(|_| writer.flush())
.map_err(|source| Error::Io {
context: "finish resource fork",
source,
})?;
writer.get_ref().sync_all().map_err(|source| Error::Io {
context: "sync resource fork",
source,
})?;
Ok(true)
}
fn build_streaming_resource_fork(path: &Path, raw: &[u8]) -> Result<Option<Codec>, Error> {
for codec in [Codec::Lzfse, Codec::Lzvn] {
if write_streaming_resource_fork(path, raw, codec)? {
return Ok(Some(codec));
}
}
Ok(None)
}
#[path = "macos/streaming.rs"]
mod streaming;
pub(crate) use streaming::StreamingWriter;
fn decmpfs_header(codec: Codec, raw_len: usize) -> [u8; 16] {
let mut header = [0u8; 16];
header[..4].copy_from_slice(&DECMPFS_MAGIC.to_le_bytes());
header[4..8].copy_from_slice(&codec.compression_type().to_le_bytes());
header[8..].copy_from_slice(&(raw_len as u64).to_le_bytes());
header
}
fn setxattr(path: &std::ffi::CStr, name: &std::ffi::CStr, value: &[u8]) -> Result<(), Error> {
let rc = unsafe {
libc::setxattr(
path.as_ptr(),
name.as_ptr(),
value.as_ptr().cast(),
value.len(),
0,
XATTR_NOFOLLOW,
)
};
if rc != 0 {
return Err(io("setxattr"));
}
Ok(())
}
pub(crate) fn apply_inplace(path: &Path, snapshot: &[u8]) -> Result<(), Error> {
let cpath = cstring(path)?;
if unsafe { libc::access(cpath.as_ptr(), libc::W_OK) } != 0 {
return Err(io("access"));
}
let mode = std::fs::metadata(path).map(|m| m.permissions()).ok();
apply_bytes(path, snapshot, mode)
}
pub(crate) fn apply_bytes(
path: &Path,
content: &[u8],
mode: Option<std::fs::Permissions>,
) -> Result<(), Error> {
apply_bytes_with_streaming_threshold(path, content, mode, STREAMING_THRESHOLD)
}
fn apply_bytes_with_streaming_threshold(
path: &Path,
content: &[u8],
mode: Option<std::fs::Permissions>,
streaming_threshold: usize,
) -> Result<(), Error> {
let stream = should_stream_resource_fork(content.len(), streaming_threshold);
let in_memory_resource_fork = if stream {
None
} else {
build_in_memory_resource_fork(content)?
};
let dir = path.parent().ok_or_else(|| io("parent"))?;
let name = path
.file_name()
.ok_or_else(|| io("file_name"))?
.to_string_lossy();
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);
let tmp = dir.join(format!(
".{name}.decmpfs-{}-{nanos}-{seq}.tmp",
std::process::id()
));
let build = (|| -> Result<(), Error> {
let create_temp = || {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&tmp)
.map_err(|source| Error::Io {
context: "create temp",
source,
})
};
let mut file = create_temp()?;
let ctmp = cstring(&tmp)?;
let codec = if stream {
build_streaming_resource_fork(&tmp, content)?
} else if let Some(resource_fork) = &in_memory_resource_fork {
setxattr(&ctmp, c"com.apple.ResourceFork", &resource_fork.bytes)?;
Some(resource_fork.codec)
} else {
None
};
if let Some(codec) = codec {
setxattr(
&ctmp,
c"com.apple.decmpfs",
&decmpfs_header(codec, content.len()),
)?;
if unsafe { libc::fchflags(file.as_raw_fd(), UF_COMPRESSED) } != 0 {
return Err(io("fchflags"));
}
} else {
if stream {
drop(file);
std::fs::remove_file(&tmp).map_err(|source| Error::Io {
context: "remove losing streamed temp",
source,
})?;
file = create_temp()?;
}
use std::io::Write;
file.write_all(content).map_err(|source| Error::Io {
context: "plain temp write",
source,
})?;
file.sync_all().map_err(|source| Error::Io {
context: "plain temp sync",
source,
})?;
}
Ok(())
})();
if let Err(e) = build {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
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()));
}
std::fs::rename(&tmp, path).map_err(|source| {
let _ = std::fs::remove_file(&tmp);
Error::Io {
context: "rename",
source,
}
})
}
pub(crate) fn clone_file(src: &Path, dest: &Path) -> Result<bool, Error> {
let csrc = cstring(src)?;
let cdest = cstring(dest)?;
Ok(unsafe { libc::clonefile(csrc.as_ptr(), cdest.as_ptr(), 0) } == 0)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
#[path = "macos/tests.rs"]
mod tests;