| use super::*; | ||
| pub(super) enum StreamingState { | ||
| Encoding(StreamingEncoding), | ||
| Plain(std::fs::File), | ||
| Closed, | ||
| } | ||
| pub(super) struct StreamingEncoding { | ||
| file: std::fs::File, | ||
| fork: std::io::BufWriter<std::fs::File>, | ||
| scratch: Vec<u8>, | ||
| partial: Vec<u8>, | ||
| offsets: Vec<u32>, | ||
| encoded_offset: usize, | ||
| } | ||
| /// Incremental macOS writer used by the public streaming API. Raw input is held | ||
| /// only until the current 64 KiB block is complete; winning LZFSE blocks land | ||
| /// directly in the named resource fork. If the fork stops winning, its completed | ||
| /// blocks are decoded into a plain sibling and subsequent input streams there. | ||
| pub(crate) struct StreamingWriter { | ||
| path: std::path::PathBuf, | ||
| expected_len: usize, | ||
| written: usize, | ||
| state: StreamingState, | ||
| complete: bool, | ||
| } | ||
| impl StreamingEncoding { | ||
| fn write_block(&mut self, raw: &[u8], expected_len: usize) -> Result<bool, Error> { | ||
| let Some(encoded) = compress_block_with_codec(raw, &mut self.scratch, Codec::Lzfse) else { | ||
| return Ok(false); | ||
| }; | ||
| // Verify every encoder result while the matching raw block is still in | ||
| // memory. The final kernel oracle then only has to prove the decmpfs layout. | ||
| let mut decoded = vec![0u8; raw.len()]; | ||
| let decoded_len = unsafe { | ||
| compression_decode_buffer( | ||
| decoded.as_mut_ptr(), | ||
| decoded.len(), | ||
| encoded.as_ptr(), | ||
| encoded.len(), | ||
| std::ptr::null_mut(), | ||
| Codec::Lzfse.algorithm(), | ||
| ) | ||
| }; | ||
| if decoded_len != raw.len() || decoded != raw { | ||
| return Ok(false); | ||
| } | ||
| let Some(next_offset) = self.encoded_offset.checked_add(encoded.len()) else { | ||
| return Ok(false); | ||
| }; | ||
| if next_offset >= expected_len || next_offset > u32::MAX as usize { | ||
| return Ok(false); | ||
| } | ||
| use std::io::Write; | ||
| self.fork.write_all(&encoded).map_err(|source| Error::Io { | ||
| context: "write streaming resource-fork block", | ||
| source, | ||
| })?; | ||
| self.encoded_offset = next_offset; | ||
| self | ||
| .offsets | ||
| .push(u32::try_from(next_offset).map_err(|_| resource_fork_too_large())?); | ||
| Ok(true) | ||
| } | ||
| } | ||
| pub(super) fn streaming_fallback_path(path: &Path) -> std::path::PathBuf { | ||
| static FALLBACK_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); | ||
| let seq = FALLBACK_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed); | ||
| let name = path.file_name().map_or_else( | ||
| || std::borrow::Cow::Borrowed("stream"), | ||
| |n| n.to_string_lossy(), | ||
| ); | ||
| path.with_file_name(format!(".{name}.plain-{}-{seq}.tmp", std::process::id())) | ||
| } | ||
| pub(super) fn decode_streaming_prefix( | ||
| path: &Path, | ||
| encoding: &mut StreamingEncoding, | ||
| current: &[u8], | ||
| expected_len: usize, | ||
| ) -> Result<(std::path::PathBuf, std::fs::File), Error> { | ||
| use std::io::{Read, Seek, Write}; | ||
| encoding.fork.flush().map_err(|source| Error::Io { | ||
| context: "flush streaming resource fork", | ||
| source, | ||
| })?; | ||
| encoding | ||
| .fork | ||
| .get_ref() | ||
| .sync_all() | ||
| .map_err(|source| Error::Io { | ||
| context: "sync streaming resource fork", | ||
| source, | ||
| })?; | ||
| let fork_path = path.join("..namedfork").join("rsrc"); | ||
| let mut fork = std::fs::File::open(fork_path).map_err(|source| Error::Io { | ||
| context: "open streaming resource fork for fallback", | ||
| source, | ||
| })?; | ||
| let fallback = streaming_fallback_path(path); | ||
| let mut plain = std::fs::OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create_new(true) | ||
| .open(&fallback) | ||
| .map_err(|source| Error::Io { | ||
| context: "create streaming plain fallback", | ||
| source, | ||
| })?; | ||
| let decoded = (|| -> Result<(), Error> { | ||
| for (block_index, pair) in encoding.offsets.windows(2).enumerate() { | ||
| let start = pair[0] as u64; | ||
| let encoded_len = (pair[1] - pair[0]) as usize; | ||
| let mut encoded = vec![0u8; encoded_len]; | ||
| fork | ||
| .seek(std::io::SeekFrom::Start(start)) | ||
| .and_then(|_| fork.read_exact(&mut encoded)) | ||
| .map_err(|source| Error::Io { | ||
| context: "read streaming resource fork for fallback", | ||
| source, | ||
| })?; | ||
| let raw_len = expected_len | ||
| .saturating_sub(block_index.saturating_mul(BLOCK)) | ||
| .min(BLOCK); | ||
| let mut raw = vec![0u8; raw_len]; | ||
| let raw_len = unsafe { | ||
| compression_decode_buffer( | ||
| raw.as_mut_ptr(), | ||
| raw.len(), | ||
| encoded.as_ptr(), | ||
| encoded.len(), | ||
| std::ptr::null_mut(), | ||
| Codec::Lzfse.algorithm(), | ||
| ) | ||
| }; | ||
| if raw_len != raw.len() { | ||
| return Err(Error::Io { | ||
| context: "decode streaming resource fork for fallback", | ||
| source: std::io::Error::from(std::io::ErrorKind::InvalidData), | ||
| }); | ||
| } | ||
| plain.write_all(&raw).map_err(|source| Error::Io { | ||
| context: "write streaming plain fallback", | ||
| source, | ||
| })?; | ||
| } | ||
| plain.write_all(current).map_err(|source| Error::Io { | ||
| context: "write current streaming fallback block", | ||
| source, | ||
| }) | ||
| })(); | ||
| if let Err(err) = decoded { | ||
| drop(plain); | ||
| let _ = std::fs::remove_file(&fallback); | ||
| return Err(err); | ||
| } | ||
| Ok((fallback, plain)) | ||
| } | ||
| pub(super) fn streaming_kernel_matches( | ||
| path: &Path, | ||
| encoding: &StreamingEncoding, | ||
| expected_len: usize, | ||
| ) -> Result<bool, Error> { | ||
| use std::io::{Read, Seek}; | ||
| let mut logical = match std::fs::File::open(path) { | ||
| Ok(file) => file, | ||
| Err(_) => return Ok(false), | ||
| }; | ||
| let fork_path = path.join("..namedfork").join("rsrc"); | ||
| let mut fork = std::fs::File::open(fork_path).map_err(|source| Error::Io { | ||
| context: "open finished streaming resource fork", | ||
| source, | ||
| })?; | ||
| for (block_index, pair) in encoding.offsets.windows(2).enumerate() { | ||
| let encoded_len = (pair[1] - pair[0]) as usize; | ||
| let mut encoded = vec![0u8; encoded_len]; | ||
| fork | ||
| .seek(std::io::SeekFrom::Start(pair[0] as u64)) | ||
| .and_then(|_| fork.read_exact(&mut encoded)) | ||
| .map_err(|source| Error::Io { | ||
| context: "read finished streaming resource fork", | ||
| source, | ||
| })?; | ||
| let raw_len = expected_len | ||
| .saturating_sub(block_index.saturating_mul(BLOCK)) | ||
| .min(BLOCK); | ||
| let mut decoded = vec![0u8; raw_len]; | ||
| let decoded_len = unsafe { | ||
| compression_decode_buffer( | ||
| decoded.as_mut_ptr(), | ||
| decoded.len(), | ||
| encoded.as_ptr(), | ||
| encoded.len(), | ||
| std::ptr::null_mut(), | ||
| Codec::Lzfse.algorithm(), | ||
| ) | ||
| }; | ||
| if decoded_len != raw_len { | ||
| return Ok(false); | ||
| } | ||
| let mut kernel = vec![0u8; raw_len]; | ||
| if logical.read_exact(&mut kernel).is_err() || kernel != decoded { | ||
| return Ok(false); | ||
| } | ||
| } | ||
| let mut extra = [0u8; 1]; | ||
| Ok(logical.read(&mut extra).is_ok_and(|len| len == 0)) | ||
| } | ||
| impl StreamingWriter { | ||
| pub(crate) fn new(path: &Path, expected_len: usize) -> Result<Self, Error> { | ||
| let num_blocks = expected_len.div_ceil(BLOCK).max(1); | ||
| let table_len = resource_fork_table_len(num_blocks)?; | ||
| if expected_len == 0 || table_len >= expected_len || table_len > u32::MAX as usize { | ||
| let file = std::fs::OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create_new(true) | ||
| .open(path) | ||
| .map_err(|source| Error::Io { | ||
| context: "create streaming plain temp", | ||
| source, | ||
| })?; | ||
| return Ok(Self { | ||
| path: path.to_path_buf(), | ||
| expected_len, | ||
| written: 0, | ||
| state: StreamingState::Plain(file), | ||
| complete: false, | ||
| }); | ||
| } | ||
| let file = std::fs::OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create_new(true) | ||
| .open(path) | ||
| .map_err(|source| Error::Io { | ||
| context: "create streaming decmpfs temp", | ||
| source, | ||
| })?; | ||
| let fork_file = (|| -> Result<std::fs::File, Error> { | ||
| let fork_path = path.join("..namedfork").join("rsrc"); | ||
| let mut fork_file = std::fs::OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create(true) | ||
| .truncate(true) | ||
| .open(fork_path) | ||
| .map_err(|source| Error::Io { | ||
| context: "open streaming resource fork", | ||
| source, | ||
| })?; | ||
| use std::io::Seek; | ||
| fork_file | ||
| .set_len(table_len as u64) | ||
| .map_err(|source| Error::Io { | ||
| context: "reserve streaming resource-fork table", | ||
| source, | ||
| })?; | ||
| fork_file | ||
| .seek(std::io::SeekFrom::Start(table_len as u64)) | ||
| .map_err(|source| Error::Io { | ||
| context: "seek streaming resource-fork payload", | ||
| source, | ||
| })?; | ||
| Ok(fork_file) | ||
| })(); | ||
| let fork_file = match fork_file { | ||
| Ok(fork_file) => fork_file, | ||
| Err(error) => { | ||
| drop(file); | ||
| let _ = std::fs::remove_file(path); | ||
| return Err(error); | ||
| } | ||
| }; | ||
| let scratch_len = unsafe { compression_encode_scratch_buffer_size(Codec::Lzfse.algorithm()) }; | ||
| Ok(Self { | ||
| path: path.to_path_buf(), | ||
| expected_len, | ||
| written: 0, | ||
| state: StreamingState::Encoding(StreamingEncoding { | ||
| file, | ||
| fork: std::io::BufWriter::with_capacity(1 << 20, fork_file), | ||
| scratch: vec![0u8; scratch_len], | ||
| partial: Vec::with_capacity(BLOCK), | ||
| offsets: vec![u32::try_from(table_len).map_err(|_| resource_fork_too_large())?], | ||
| encoded_offset: table_len, | ||
| }), | ||
| complete: false, | ||
| }) | ||
| } | ||
| fn switch_to_plain(&mut self, current: &[u8]) -> Result<(), Error> { | ||
| let StreamingState::Encoding(mut encoding) = | ||
| std::mem::replace(&mut self.state, StreamingState::Closed) | ||
| else { | ||
| return Err(Error::Io { | ||
| context: "switch streaming writer to plain", | ||
| source: std::io::Error::from(std::io::ErrorKind::InvalidInput), | ||
| }); | ||
| }; | ||
| let (fallback, mut plain) = | ||
| decode_streaming_prefix(&self.path, &mut encoding, current, self.expected_len)?; | ||
| drop(encoding); | ||
| if let Err(source) = std::fs::remove_file(&self.path) { | ||
| let _ = std::fs::remove_file(&fallback); | ||
| return Err(Error::Io { | ||
| context: "remove streaming decmpfs temp", | ||
| source, | ||
| }); | ||
| } | ||
| if let Err(source) = std::fs::rename(&fallback, &self.path) { | ||
| let _ = std::fs::remove_file(&fallback); | ||
| return Err(Error::Io { | ||
| context: "adopt streaming plain fallback", | ||
| source, | ||
| }); | ||
| } | ||
| use std::io::Seek; | ||
| plain | ||
| .seek(std::io::SeekFrom::End(0)) | ||
| .map_err(|source| Error::Io { | ||
| context: "seek streaming plain fallback", | ||
| source, | ||
| })?; | ||
| self.state = StreamingState::Plain(plain); | ||
| Ok(()) | ||
| } | ||
| pub(crate) fn write_all(&mut self, mut input: &[u8]) -> Result<(), Error> { | ||
| let next_written = self | ||
| .written | ||
| .checked_add(input.len()) | ||
| .filter(|&len| len <= self.expected_len) | ||
| .ok_or_else(|| Error::Io { | ||
| context: "stream exceeds expected length", | ||
| source: std::io::Error::from(std::io::ErrorKind::InvalidData), | ||
| })?; | ||
| while !input.is_empty() { | ||
| match &mut self.state { | ||
| StreamingState::Plain(file) => { | ||
| use std::io::Write; | ||
| file.write_all(input).map_err(|source| Error::Io { | ||
| context: "write streaming plain temp", | ||
| source, | ||
| })?; | ||
| input = &[]; | ||
| } | ||
| StreamingState::Encoding(encoding) => { | ||
| let take = (BLOCK - encoding.partial.len()).min(input.len()); | ||
| encoding.partial.extend_from_slice(&input[..take]); | ||
| input = &input[take..]; | ||
| if encoding.partial.len() == BLOCK { | ||
| let block = std::mem::replace(&mut encoding.partial, Vec::with_capacity(BLOCK)); | ||
| if !encoding.write_block(&block, self.expected_len)? { | ||
| self.switch_to_plain(&block)?; | ||
| } | ||
| } | ||
| } | ||
| StreamingState::Closed => { | ||
| return Err(Error::Io { | ||
| context: "write closed streaming writer", | ||
| source: std::io::Error::from(std::io::ErrorKind::BrokenPipe), | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| self.written = next_written; | ||
| Ok(()) | ||
| } | ||
| pub(crate) fn finish(&mut self) -> Result<bool, Error> { | ||
| if self.written != self.expected_len { | ||
| return Err(Error::Io { | ||
| context: "finish incomplete streaming writer", | ||
| source: std::io::Error::from(std::io::ErrorKind::UnexpectedEof), | ||
| }); | ||
| } | ||
| let partial = match &mut self.state { | ||
| StreamingState::Encoding(encoding) if !encoding.partial.is_empty() => Some( | ||
| std::mem::replace(&mut encoding.partial, Vec::with_capacity(BLOCK)), | ||
| ), | ||
| _ => None, | ||
| }; | ||
| if let Some(block) = partial { | ||
| let won = match &mut self.state { | ||
| StreamingState::Encoding(encoding) => encoding.write_block(&block, self.expected_len)?, | ||
| _ => false, | ||
| }; | ||
| if !won { | ||
| self.switch_to_plain(&block)?; | ||
| } | ||
| } | ||
| let compressed = match std::mem::replace(&mut self.state, StreamingState::Closed) { | ||
| StreamingState::Plain(file) => { | ||
| file.sync_all().map_err(|source| Error::Io { | ||
| context: "sync streaming plain temp", | ||
| source, | ||
| })?; | ||
| false | ||
| } | ||
| StreamingState::Encoding(mut encoding) => { | ||
| use std::io::{Seek, Write}; | ||
| let mut table = Vec::with_capacity(encoding.offsets.len() * std::mem::size_of::<u32>()); | ||
| for offset in &encoding.offsets { | ||
| table.extend_from_slice(&offset.to_le_bytes()); | ||
| } | ||
| encoding | ||
| .fork | ||
| .seek(std::io::SeekFrom::Start(0)) | ||
| .and_then(|_| encoding.fork.write_all(&table)) | ||
| .and_then(|_| encoding.fork.flush()) | ||
| .map_err(|source| Error::Io { | ||
| context: "finish streaming resource fork", | ||
| source, | ||
| })?; | ||
| encoding | ||
| .fork | ||
| .get_ref() | ||
| .sync_all() | ||
| .map_err(|source| Error::Io { | ||
| context: "sync finished streaming resource fork", | ||
| source, | ||
| })?; | ||
| let cpath = cstring(&self.path)?; | ||
| setxattr( | ||
| &cpath, | ||
| c"com.apple.decmpfs", | ||
| &decmpfs_header(Codec::Lzfse, self.expected_len), | ||
| )?; | ||
| if unsafe { libc::fchflags(encoding.file.as_raw_fd(), UF_COMPRESSED) } != 0 { | ||
| return Err(io("fchflags streaming temp")); | ||
| } | ||
| encoding.file.sync_all().map_err(|source| Error::Io { | ||
| context: "sync streaming decmpfs temp", | ||
| source, | ||
| })?; | ||
| if streaming_kernel_matches(&self.path, &encoding, self.expected_len)? { | ||
| true | ||
| } else { | ||
| let (fallback, plain) = | ||
| decode_streaming_prefix(&self.path, &mut encoding, &[], self.expected_len)?; | ||
| drop(encoding); | ||
| std::fs::remove_file(&self.path).map_err(|source| Error::Io { | ||
| context: "remove failed streaming decmpfs oracle", | ||
| source, | ||
| })?; | ||
| if let Err(source) = std::fs::rename(&fallback, &self.path) { | ||
| let _ = std::fs::remove_file(&fallback); | ||
| return Err(Error::Io { | ||
| context: "publish streaming oracle fallback", | ||
| source, | ||
| }); | ||
| } | ||
| plain.sync_all().map_err(|source| Error::Io { | ||
| context: "sync streaming oracle fallback", | ||
| source, | ||
| })?; | ||
| false | ||
| } | ||
| } | ||
| StreamingState::Closed => { | ||
| return Err(Error::Io { | ||
| context: "finish closed streaming writer", | ||
| source: std::io::Error::from(std::io::ErrorKind::BrokenPipe), | ||
| }); | ||
| } | ||
| }; | ||
| self.complete = true; | ||
| Ok(compressed) | ||
| } | ||
| } | ||
| impl Drop for StreamingWriter { | ||
| fn drop(&mut self) { | ||
| if !self.complete { | ||
| self.state = StreamingState::Closed; | ||
| let _ = std::fs::remove_file(&self.path); | ||
| } | ||
| } | ||
| } |
| use super::*; | ||
| // The kernel-roundtrip oracle. decmpfs is undocumented — the only proof the | ||
| // format is right is that a normal read() returns identical bytes after apply. | ||
| #[test] | ||
| fn kernel_roundtrips_decmpfs() { | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-oracle-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("f.bin"); | ||
| // > 1 block (64 KiB) of compressible data, so the offset table + LZVN blocks | ||
| // are both exercised. | ||
| let mut raw = Vec::new(); | ||
| let pat = b"the quick brown fox decmpfs lzvn resource-fork oracle line "; | ||
| while raw.len() < 2_000_000 { | ||
| raw.extend_from_slice(pat); | ||
| } | ||
| std::fs::write(&path, &raw).unwrap(); | ||
| assert!( | ||
| matches!(detect(&path).unwrap(), Support::Supported), | ||
| "temp dir is local APFS/HFS+" | ||
| ); | ||
| apply_inplace(&path, &raw).unwrap(); | ||
| assert!(is_already_compressed(&path).unwrap(), "UF_COMPRESSED set"); | ||
| assert_eq!( | ||
| compressed_on_disk(&path).unwrap(), | ||
| Some(true), | ||
| "reports compressed" | ||
| ); | ||
| // THE ORACLE: the kernel decompresses our resource fork on read(). | ||
| assert_eq!( | ||
| std::fs::read(&path).unwrap(), | ||
| raw, | ||
| "kernel read-back must equal the original bytes" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn incremental_writer_streams_lzfse_blocks_into_a_kernel_readable_file() { | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-incremental-oracle-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("model.bin"); | ||
| let raw = b"incremental lzfse resource fork ".repeat((2 << 20) / 34 + 1); | ||
| let mut writer = StreamingWriter::new(&path, raw.len()).unwrap(); | ||
| for chunk in raw.chunks(17_003) { | ||
| writer.write_all(chunk).unwrap(); | ||
| } | ||
| assert!(writer.finish().unwrap(), "compressible stream must win"); | ||
| assert!(is_already_compressed(&path).unwrap()); | ||
| assert_eq!(std::fs::read(&path).unwrap(), raw); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn incremental_writer_reconstructs_plain_bytes_when_compression_loses() { | ||
| let dir = std::env::temp_dir().join(format!( | ||
| "decmpfs-incremental-fallback-{}", | ||
| std::process::id() | ||
| )); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("random.bin"); | ||
| let mut raw = Vec::with_capacity(2 << 20); | ||
| let mut x: u64 = 0x9e37_79b9_7f4a_7c15; | ||
| while raw.len() < (2 << 20) { | ||
| x ^= x << 13; | ||
| x ^= x >> 7; | ||
| x ^= x << 17; | ||
| raw.extend_from_slice(&x.to_le_bytes()); | ||
| } | ||
| let mut writer = StreamingWriter::new(&path, raw.len()).unwrap(); | ||
| for chunk in raw.chunks(17_003) { | ||
| writer.write_all(chunk).unwrap(); | ||
| } | ||
| assert!( | ||
| !writer.finish().unwrap(), | ||
| "incompressible stream stays plain" | ||
| ); | ||
| assert!(!is_already_compressed(&path).unwrap()); | ||
| assert_eq!(std::fs::read(&path).unwrap(), raw); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // Opt-in perf probe (ignored in CI — timing is machine-specific). Reports the | ||
| // decmpfs write time for a ~40 MiB addon; run serial vs parallel with | ||
| // cargo test -p decmpfs write_time -- --ignored --nocapture | ||
| // DECMPFS_SERIAL=1 cargo test -p decmpfs write_time -- --ignored --nocapture | ||
| #[test] | ||
| #[ignore] | ||
| fn write_time_probe() { | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-time-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("addon.node"); | ||
| let mut raw: Vec<u8> = Vec::with_capacity(40 << 20); | ||
| let mut x: u64 = 0x9e37_79b9_7f4a_7c15; | ||
| while raw.len() < (40 << 20) { | ||
| x ^= x << 13; | ||
| x ^= x >> 7; | ||
| x ^= x << 17; | ||
| raw.extend_from_slice(&x.to_le_bytes()); | ||
| raw.extend_from_slice(b"native addon .node text segment padding "); | ||
| } | ||
| if !matches!(detect(&dir), Ok(Support::Supported)) { | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| return; | ||
| } | ||
| let cores = std::thread::available_parallelism() | ||
| .map(|n| n.get()) | ||
| .unwrap_or(1); | ||
| let serial = std::env::var_os("DECMPFS_SERIAL").is_some(); | ||
| let start = std::time::Instant::now(); | ||
| apply_bytes(&path, &raw, None).unwrap(); | ||
| let ms = start.elapsed().as_secs_f64() * 1e3; | ||
| eprintln!( | ||
| "decmpfs write {}MiB — {} ({} cores): {:.1} ms", | ||
| raw.len() >> 20, | ||
| if serial { "serial" } else { "parallel" }, | ||
| cores, | ||
| ms, | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn detect_and_flags_error_on_a_missing_path() { | ||
| let p = std::path::Path::new("/no/such/decmpfs/path/x.bin"); | ||
| assert!(detect(p).is_err(), "statfs of a missing path errors"); | ||
| assert!( | ||
| is_already_compressed(p).is_err(), | ||
| "lstat of a missing path errors" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn apply_inplace_errors_when_the_file_cannot_be_read() { | ||
| // A 0-perm file: apply_inplace's initial read fails before any apply. Root | ||
| // bypasses mode bits, so skip there. | ||
| if unsafe { libc::geteuid() } == 0 { | ||
| return; | ||
| } | ||
| use std::os::unix::fs::PermissionsExt; | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-noread-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("f.bin"); | ||
| let content = b"\x7fELF unreadable"; | ||
| std::fs::write(&path, content).unwrap(); | ||
| std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap(); | ||
| // apply_inplace no longer reads the file (the caller passes the snapshot it | ||
| // already holds); the fail-soft guard is now the W_OK access check, which | ||
| // rejects a file we cannot write before the temp+rename would replace it. | ||
| let out = apply_inplace(&path, content); | ||
| std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).ok(); | ||
| assert!(matches!( | ||
| out, | ||
| Err(Error::Io { | ||
| context: "access", | ||
| .. | ||
| }) | ||
| )); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn setxattr_errors_on_a_missing_path() { | ||
| let out = setxattr(c"/no/such/decmpfs/path", c"com.apple.decmpfs", b"x"); | ||
| assert!(matches!( | ||
| out, | ||
| Err(Error::Io { | ||
| context: "setxattr", | ||
| .. | ||
| }) | ||
| )); | ||
| } | ||
| #[test] | ||
| fn compress_block_returns_none_for_empty_input() { | ||
| // libcompression encodes zero bytes to nothing → the n == 0 guard returns None. | ||
| let scratch_len = unsafe { compression_encode_scratch_buffer_size(COMPRESSION_LZVN) }; | ||
| let mut scratch = vec![0u8; scratch_len]; | ||
| assert!(compress_block(b"", &mut scratch).is_none()); | ||
| } | ||
| #[test] | ||
| fn build_resource_fork_zero_length_is_no_gain() { | ||
| assert!( | ||
| build_resource_fork(&[]).unwrap().is_none(), | ||
| "a resource fork cannot make an empty file smaller" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn streaming_threshold_keeps_vite_native_addons_on_the_fast_path() { | ||
| // The largest Darwin ARM64 addon in the 2026-07-16 Vite-family sample was | ||
| // SWC at 36.563 MiB. The complete observed set must stay comfortably below | ||
| // the in-memory cutoff, while the first byte beyond it streams. | ||
| assert!(!should_stream_resource_fork(37 << 20, STREAMING_THRESHOLD)); | ||
| assert!(!should_stream_resource_fork( | ||
| STREAMING_THRESHOLD, | ||
| STREAMING_THRESHOLD | ||
| )); | ||
| assert!(should_stream_resource_fork( | ||
| STREAMING_THRESHOLD + 1, | ||
| STREAMING_THRESHOLD | ||
| )); | ||
| } | ||
| #[test] | ||
| fn kernel_roundtrips_forced_streaming_lzfse() { | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-streaming-oracle-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("f.bin"); | ||
| let raw = b"streamed lzfse decmpfs resource fork oracle ".repeat((2 << 20) / 46 + 1); | ||
| std::fs::write(&path, &raw).unwrap(); | ||
| if matches!(detect(&path).unwrap(), Support::Supported) { | ||
| apply_bytes_with_streaming_threshold(&path, &raw, None, 0).unwrap(); | ||
| assert!(is_already_compressed(&path).unwrap(), "UF_COMPRESSED set"); | ||
| assert_eq!( | ||
| std::fs::read(&path).unwrap(), | ||
| raw, | ||
| "kernel read-back must decode the streamed type-12 resource fork" | ||
| ); | ||
| let cpath = cstring(&path).unwrap(); | ||
| let mut header = [0u8; 16]; | ||
| let len = unsafe { | ||
| libc::getxattr( | ||
| cpath.as_ptr(), | ||
| c"com.apple.decmpfs".as_ptr(), | ||
| header.as_mut_ptr().cast(), | ||
| header.len(), | ||
| 0, | ||
| XATTR_NOFOLLOW | 0x0020, // XATTR_SHOWCOMPRESSION | ||
| ) | ||
| }; | ||
| assert_eq!(len, header.len() as isize); | ||
| assert_eq!(u32::from_le_bytes(header[4..8].try_into().unwrap()), 12); | ||
| } | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn in_memory_path_falls_back_to_lzfse_when_lzvn_has_no_gain() { | ||
| // Skewed symbol frequencies give LZFSE's entropy coder something to exploit | ||
| // without manufacturing the repeated strings that LZVN specializes in. | ||
| let mut raw = Vec::with_capacity(1 << 20); | ||
| let mut x: u64 = 0x9e37_79b9_7f4a_7c15; | ||
| while raw.len() < raw.capacity() { | ||
| x ^= x << 13; | ||
| x ^= x >> 7; | ||
| x ^= x << 17; | ||
| raw.push(if x.is_multiple_of(4) { | ||
| 0 | ||
| } else { | ||
| (x >> 32) as u8 | ||
| }); | ||
| } | ||
| assert!( | ||
| build_resource_fork_with_codec(&raw, Codec::Lzvn) | ||
| .unwrap() | ||
| .is_none(), | ||
| "fixture must reach the fallback" | ||
| ); | ||
| let candidate = build_in_memory_resource_fork(&raw) | ||
| .unwrap() | ||
| .expect("LZFSE should exploit the skewed symbols"); | ||
| assert_eq!(candidate.codec, Codec::Lzfse); | ||
| assert!(candidate.bytes.len() < raw.len()); | ||
| } | ||
| #[test] | ||
| fn build_resource_fork_last_offset_equals_length() { | ||
| // Invariant across sizes that actually encode: the final table offset equals | ||
| // the total blob length. (Tiny/incompressible inputs return None — the codec | ||
| // declines — which is a separate, correct path.) | ||
| for size in [512usize, BLOCK, BLOCK + 1, BLOCK * 3 + 7] { | ||
| let raw = vec![0x41u8; size]; | ||
| let Some(rf) = build_resource_fork(&raw).unwrap() else { | ||
| continue; | ||
| }; | ||
| let num_blocks = size.div_ceil(BLOCK); | ||
| let last_idx = num_blocks * 4; // offset[num_blocks] is the last entry | ||
| let last = u32::from_le_bytes(rf[last_idx..last_idx + 4].try_into().unwrap()) as usize; | ||
| assert_eq!(last, rf.len(), "size {size}: last offset != buffer length"); | ||
| } | ||
| } | ||
| #[test] | ||
| fn cstring_rejects_an_interior_nul() { | ||
| use std::os::unix::ffi::OsStrExt; | ||
| let p = std::path::Path::new(std::ffi::OsStr::from_bytes(b"a\0b")); | ||
| assert!(cstring(p).is_err()); | ||
| } | ||
| #[test] | ||
| fn detect_rejects_a_non_apfs_filesystem() { | ||
| // /dev is devfs (local, but not apfs/hfs) → Unsupported(Filesystem). | ||
| assert!(matches!( | ||
| detect(std::path::Path::new("/dev")), | ||
| Ok(Support::Unsupported(UnsupportedReason::Filesystem)) | ||
| )); | ||
| } | ||
| #[test] | ||
| fn classify_fs_covers_every_branch() { | ||
| // Non-local (e.g. a network mount) — no real mount needed. | ||
| assert!(matches!( | ||
| classify_fs(false, b"nfs"), | ||
| Support::Unsupported(UnsupportedReason::NetworkOrOverlay) | ||
| )); | ||
| assert!(matches!(classify_fs(true, b"apfs"), Support::Supported)); | ||
| assert!(matches!(classify_fs(true, b"hfs"), Support::Supported)); | ||
| assert!(matches!( | ||
| classify_fs(true, b"ext4"), | ||
| Support::Unsupported(UnsupportedReason::Filesystem) | ||
| )); | ||
| } | ||
| #[test] | ||
| fn resource_fork_plan_accepts_raw_files_beyond_the_old_limit() { | ||
| // The raw byte count is stored as u64. Only resource-fork offsets are u32, | ||
| // so a >3.9 GB input is valid whenever its encoded fork fits in u32. | ||
| let raw_len = 4_100_000_000usize; | ||
| let num_blocks = raw_len.div_ceil(BLOCK); | ||
| assert!(matches!( | ||
| plan_resource_fork(raw_len, num_blocks, 3_000_000_000).unwrap(), | ||
| ResourceForkPlan::Compressed { .. } | ||
| )); | ||
| } | ||
| #[test] | ||
| fn resource_fork_plan_accepts_raw_files_beyond_four_gib_when_the_fork_fits() { | ||
| let raw_len = 5_000_000_000usize; | ||
| let num_blocks = raw_len.div_ceil(BLOCK); | ||
| assert!(matches!( | ||
| plan_resource_fork(raw_len, num_blocks, 3_000_000_000).unwrap(), | ||
| ResourceForkPlan::Compressed { .. } | ||
| )); | ||
| } | ||
| #[test] | ||
| fn resource_fork_plan_rejects_a_compressed_fork_past_u32() { | ||
| let raw_len = 5_000_000_000usize; | ||
| let num_blocks = raw_len.div_ceil(BLOCK); | ||
| match plan_resource_fork(raw_len, num_blocks, 4_400_000_000).unwrap_err() { | ||
| Error::Io { source, .. } => assert_eq!(source.raw_os_error(), Some(libc::EFBIG)), | ||
| other => panic!("expected EFBIG Io, got {other:?}"), | ||
| } | ||
| } | ||
| #[test] | ||
| fn gemini_nano_lzvn_resource_fork_is_no_gain() { | ||
| // Chrome 150's v3Nano weights.bin measured with this exact 64 KiB LZVN | ||
| // encoder: the encoded blocks expand enough to cross the u32 fork ceiling. | ||
| assert_eq!( | ||
| plan_resource_fork(4_269_932_544, 65_154, 4_364_775_458).unwrap(), | ||
| ResourceForkPlan::Plain | ||
| ); | ||
| } | ||
| #[test] | ||
| fn gemini_nano_lzfse_resource_fork_fits_and_wins() { | ||
| // The streamed type-12 run encoded the same 65,154 blocks to this payload; | ||
| // with its 260,620-byte offset table the fork is safely below u32::MAX. | ||
| assert_eq!( | ||
| plan_resource_fork(4_269_932_544, 65_154, 3_598_249_560).unwrap(), | ||
| ResourceForkPlan::Compressed { | ||
| table_len: 260_620, | ||
| total_len: 3_598_510_180, | ||
| } | ||
| ); | ||
| } | ||
| // Incompressible data → LZVN would expand the resource fork, so keep an | ||
| // ordinary data fork. The bytes and compression-state signal must agree. | ||
| #[test] | ||
| fn kernel_roundtrips_incompressible_blocks() { | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-raw-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("f.bin"); | ||
| let mut raw = Vec::new(); | ||
| let mut x: u32 = 0x9e37_79b9; | ||
| while raw.len() < 200_000 { | ||
| x ^= x << 13; | ||
| x ^= x >> 17; | ||
| x ^= x << 5; | ||
| raw.extend_from_slice(&x.to_le_bytes()); | ||
| } | ||
| std::fs::write(&path, &raw).unwrap(); | ||
| if matches!(detect(&path).unwrap(), Support::Supported) { | ||
| assert!(matches!( | ||
| crate::compress_file(&path).unwrap(), | ||
| crate::Outcome::NoGain { .. } | ||
| )); | ||
| assert_eq!( | ||
| std::fs::read(&path).unwrap(), | ||
| raw, | ||
| "plain fallback reads back identically" | ||
| ); | ||
| assert!( | ||
| !is_already_compressed(&path).unwrap(), | ||
| "no-gain input must not carry UF_COMPRESSED" | ||
| ); | ||
| } | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn apply_bytes_preserves_ownership_of_an_overwritten_file() { | ||
| // Non-root exercises the chown path over an existing file — owner is our own | ||
| // uid, so preservation is a no-op we assert stays stable + non-corrupting. | ||
| // The root path (a file owned by a different uid) is verified in CI. | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-own-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("f"); | ||
| std::fs::write(&path, vec![0u8; 4096]).unwrap(); | ||
| if !matches!(detect(&path), Ok(Support::Supported)) { | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| return; | ||
| } | ||
| use std::os::unix::fs::MetadataExt; | ||
| let before_uid = std::fs::metadata(&path).unwrap().uid(); | ||
| let content = vec![0xABu8; 8192]; | ||
| apply_bytes(&path, &content, None).unwrap(); | ||
| let meta = std::fs::metadata(&path).unwrap(); | ||
| assert_eq!(meta.uid(), before_uid, "owner preserved across the rewrite"); | ||
| assert_eq!(std::fs::read(&path).unwrap(), content, "content intact"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } |
+661
| use super::*; | ||
| fn scratch(tag: &str) -> std::path::PathBuf { | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-{tag}-{}", std::process::id())); | ||
| // A pid-recycled leftover FILE at this path makes create_dir_all fail | ||
| // with AlreadyExists; clear it so the scratch dir always materializes. | ||
| let _ = std::fs::remove_file(&dir); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| dir | ||
| } | ||
| // A minimal native-magic payload (ELF header) so a backend will attempt to | ||
| // compress it rather than skip a trivially-small file. | ||
| fn fake_addon() -> Vec<u8> { | ||
| let mut raw = vec![0x7f, 0x45, 0x4c, 0x46]; | ||
| raw.extend_from_slice(&[7u8; 9000]); | ||
| raw | ||
| } | ||
| #[test] | ||
| fn compress_file_errors_when_missing() { | ||
| let p = std::path::Path::new("/no/such/addon.node"); | ||
| assert!(matches!(compress_file(p), Err(Error::NotFound(_)))); | ||
| } | ||
| #[test] | ||
| fn plain_write_errors_when_the_path_has_no_parent() { | ||
| // "/" has no parent directory → the no-parent guard fires before any write. | ||
| let out = plain_write(std::path::Path::new("/"), b"x"); | ||
| assert!(matches!( | ||
| out, | ||
| Err(Error::Io { | ||
| context: "no parent dir", | ||
| .. | ||
| }) | ||
| )); | ||
| } | ||
| #[test] | ||
| fn error_display_and_source() { | ||
| let nf = Error::NotFound(std::path::PathBuf::from("/x")); | ||
| assert!(nf.to_string().contains("not found")); | ||
| assert!(std::error::Error::source(&nf).is_none()); | ||
| let io = Error::Io { | ||
| context: "ctx", | ||
| source: std::io::Error::from(std::io::ErrorKind::PermissionDenied), | ||
| }; | ||
| assert!(io.to_string().contains("ctx")); | ||
| assert!(std::error::Error::source(&io).is_some()); | ||
| } | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn probe_reports_a_support_variant_without_mutating() { | ||
| // probe never errors on an existing path — it returns a Support. | ||
| assert!(matches!( | ||
| probe(std::path::Path::new("/dev/null")), | ||
| Ok(Support::Supported | Support::AlreadyCompressed | Support::Unsupported(_)) | ||
| )); | ||
| } | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn compress_file_reports_unsupported_on_a_non_compressing_fs() { | ||
| // /dev/null exists but devfs has no compression backend → Unsupported. | ||
| let out = compress_file(std::path::Path::new("/dev/null")); | ||
| assert!( | ||
| matches!(out, Ok(Outcome::Unsupported { .. })), | ||
| "devfs → Unsupported, got {out:?}" | ||
| ); | ||
| } | ||
| // APFS is always a compressing FS, so macOS exercises the full success path: | ||
| // compress_file → apply_guarded → backend::apply_inplace → verify → classify. | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn compress_file_compresses_then_is_idempotent_and_transparent() { | ||
| let dir = scratch("ok"); | ||
| let path = dir.join("addon.node"); | ||
| std::fs::write(&path, fake_addon()).unwrap(); | ||
| let out = compress_file(&path); | ||
| assert!( | ||
| matches!( | ||
| out, | ||
| Ok(Outcome::Compressed { .. } | Outcome::NoGain { .. } | Outcome::AlreadyCompressed { .. }) | ||
| ), | ||
| "writable addon on APFS → applied, got {out:?}" | ||
| ); | ||
| // Transparent: the kernel hands back the exact original bytes. | ||
| assert_eq!(std::fs::read(&path).unwrap(), fake_addon()); | ||
| // Idempotent: a second pass detects it's already compressed. | ||
| assert!(matches!( | ||
| compress_file(&path), | ||
| Ok(Outcome::AlreadyCompressed { .. }) | ||
| )); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // compress_bytes one-pass: write bytes directly as an APFS-compressed file with | ||
| // no pre-existing original, then prove the kernel hands the exact bytes back. | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn compress_bytes_one_pass_writes_compressed_and_reads_back_identical() { | ||
| let dir = scratch("bytes"); | ||
| let path = dir.join("fresh.node"); | ||
| let content = fake_addon(); | ||
| // No file at `path` yet — compress_bytes creates it in one pass. | ||
| let out = compress_bytes(&path, &content, &Gate::any()); | ||
| assert!( | ||
| matches!(out, Ok(Outcome::Compressed { .. } | Outcome::NoGain { .. })), | ||
| "one-pass APFS write → applied, got {out:?}" | ||
| ); | ||
| assert!(path.exists(), "file was created"); | ||
| // Transparent: kernel read-back equals the bytes we asked to store. | ||
| assert_eq!(std::fs::read(&path).unwrap(), content); | ||
| // It really carries the compression flag (not a plain fallback write). | ||
| assert!(matches!( | ||
| compress_file(&path), | ||
| Ok(Outcome::AlreadyCompressed { .. }) | ||
| )); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // A file the gate excludes is written PLAIN (never compressed) and reports | ||
| // Skipped(GateExcluded) — the install still gets the file. | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn compress_bytes_gate_excluded_writes_plain() { | ||
| let dir = scratch("gate"); | ||
| let path = dir.join("not-an-addon.txt"); | ||
| let content = b"plain text, not a .node".to_vec(); | ||
| let gate = Gate::default(); // **/*.node | ||
| let out = compress_bytes(&path, &content, &gate); | ||
| assert!( | ||
| matches!( | ||
| out, | ||
| Ok(Outcome::Skipped { | ||
| reason: SkipReason::GateExcluded | ||
| }) | ||
| ), | ||
| "non-.node → GateExcluded, got {out:?}" | ||
| ); | ||
| assert_eq!(std::fs::read(&path).unwrap(), content); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn compress_bytes_falls_back_to_plain_on_unsupported_fs() { | ||
| // A non-compressing FS (devfs) → plain write, Unsupported Outcome, file lands. | ||
| // /dev isn't writable by us, so target a temp path but force the gate to pass; | ||
| // temp on macOS is APFS (compresses) — instead assert the API never errors and | ||
| // the bytes land for the supported case is covered above. Here just exercise | ||
| // the gate-passing path lands bytes on any unix temp. | ||
| let dir = scratch("fallback"); | ||
| let path = dir.join("x.node"); | ||
| let content = fake_addon(); | ||
| let out = compress_bytes(&path, &content, &Gate::any()); | ||
| assert!(out.is_ok(), "never errors on a normal temp, got {out:?}"); | ||
| assert_eq!(std::fs::read(&path).unwrap(), content, "bytes always land"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn compress_file_skips_a_read_only_file() { | ||
| // On a compressing FS a read-only file can't be opened rw → fail-soft turns the | ||
| // EACCES into Skipped(PermissionDenied). Root bypasses mode bits, so skip there. | ||
| if unsafe { libc::geteuid() } == 0 { | ||
| return; | ||
| } | ||
| let dir = scratch("ro"); | ||
| let path = dir.join("addon.node"); | ||
| std::fs::write(&path, fake_addon()).unwrap(); | ||
| if !matches!(probe(&path), Ok(Support::Supported)) { | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| return; | ||
| } | ||
| let mut perm = std::fs::metadata(&path).unwrap().permissions(); | ||
| perm.set_readonly(true); | ||
| std::fs::set_permissions(&path, perm).unwrap(); | ||
| let outcome = compress_file(&path); | ||
| use std::os::unix::fs::PermissionsExt; | ||
| std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).ok(); | ||
| assert!( | ||
| matches!( | ||
| outcome, | ||
| Ok(Outcome::Skipped { | ||
| reason: SkipReason::PermissionDenied | ||
| }) | ||
| ), | ||
| "read-only → Skipped(PermissionDenied), got {outcome:?}" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // An existing target exercises the `path.exists()` probe-target branch and the | ||
| // fresh-inode rename that replaces the old contents. | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn compress_bytes_overwrites_an_existing_file() { | ||
| let dir = scratch("overwrite"); | ||
| let path = dir.join("addon.node"); | ||
| std::fs::write(&path, b"stale contents").unwrap(); | ||
| let content = fake_addon(); | ||
| let out = compress_bytes(&path, &content, &Gate::any()); | ||
| assert!(out.is_ok(), "overwrite never errors, got {out:?}"); | ||
| assert_eq!( | ||
| std::fs::read(&path).unwrap(), | ||
| content, | ||
| "new bytes replace the old" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // `path` is an existing directory: the backend builds its temp then can't rename | ||
| // a file over a directory, and the plain-write fallback can't either → a hard | ||
| // `Err` (genuine I/O failure), never a corrupt success. Exercises the backend | ||
| // rename-error cleanup and the `Err(_)` fallback arm of compress_bytes. | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn compress_bytes_onto_a_directory_path_is_a_hard_error() { | ||
| let dir = scratch("dir-target"); | ||
| let target = dir.join("a-dir"); | ||
| std::fs::create_dir_all(&target).unwrap(); | ||
| let out = compress_bytes(&target, &fake_addon(), &Gate::any()); | ||
| assert!( | ||
| out.is_err(), | ||
| "cannot write a file over a directory, got {out:?}" | ||
| ); | ||
| assert!(target.is_dir(), "the directory is left intact"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn stat_reports_size_and_uncompressed_for_a_plain_file() { | ||
| let dir = scratch("stat-plain"); | ||
| let path = dir.join("f"); | ||
| std::fs::write(&path, vec![0u8; 4096]).unwrap(); | ||
| let s = stat(&path).unwrap(); | ||
| assert_eq!(s.logical, 4096, "logical == the written bytes"); | ||
| assert!(s.physical > 0, "allocated bytes reported"); | ||
| assert!( | ||
| !s.compressed, | ||
| "a freshly-written plain file is not FS-compressed" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn stat_reflects_a_compressed_file_where_supported() { | ||
| let dir = scratch("stat-comp"); | ||
| let path = dir.join("addon.node"); | ||
| let content = vec![0xABu8; 128 * 1024]; | ||
| let outcome = compress_bytes(&path, &content, &Gate::any()).unwrap(); | ||
| let s = stat(&path).unwrap(); | ||
| assert_eq!( | ||
| s.logical, | ||
| content.len() as u64, | ||
| "logical == the written bytes" | ||
| ); | ||
| assert_eq!( | ||
| std::fs::read(&path).unwrap(), | ||
| content, | ||
| "content round-trips" | ||
| ); | ||
| // Where the FS actually compressed (APFS / btrfs / NTFS), stat must reflect | ||
| // it; on an unsupported FS the outcome isn't Compressed and we only assert | ||
| // the size + content invariants above. | ||
| if matches!(outcome, Outcome::Compressed { .. }) { | ||
| assert!( | ||
| s.compressed, | ||
| "a Compressed outcome → stat reports compressed" | ||
| ); | ||
| assert!( | ||
| s.physical < s.logical, | ||
| "allocation shrank below the logical size" | ||
| ); | ||
| } | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // A read-only parent dir: the guarded backend write hits EACCES (classify_skip → | ||
| // Skipped), then the plain-write fallback also can't write → `Err`. Root bypasses | ||
| // mode bits, so skip there. | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn compress_bytes_into_a_read_only_dir_is_fail_soft() { | ||
| if unsafe { libc::geteuid() } == 0 { | ||
| return; | ||
| } | ||
| use std::os::unix::fs::PermissionsExt; | ||
| let dir = scratch("ro-dir"); | ||
| let locked = dir.join("locked"); | ||
| std::fs::create_dir_all(&locked).unwrap(); | ||
| std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o555)).unwrap(); | ||
| let out = compress_bytes(&locked.join("x.node"), &fake_addon(), &Gate::any()); | ||
| // Restore write perms so the tree can be cleaned up. | ||
| std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).ok(); | ||
| assert!(out.is_err(), "a read-only dir admits no write, got {out:?}"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // The `Support::AlreadyCompressed`-from-detect arm: a real macOS detect never | ||
| // returns it (it reports already-compressed via the apply path), so a fake drives | ||
| // it. Needs a real file for the on-disk-bytes read. | ||
| #[test] | ||
| fn compress_file_reports_already_compressed_from_detect() { | ||
| let dir = scratch("already-detect"); | ||
| let path = dir.join("f.node"); | ||
| std::fs::write(&path, fake_addon()).unwrap(); | ||
| let backend = FakeBackend { | ||
| detect: Support::AlreadyCompressed, | ||
| apply_error: None, | ||
| }; | ||
| assert!(matches!( | ||
| compress_file_with(&backend, &path), | ||
| Ok(Outcome::AlreadyCompressed { .. }) | ||
| )); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // detect → Unsupported: the bytes still land via a plain write, Outcome::Unsupported. | ||
| #[test] | ||
| fn compress_bytes_falls_back_to_plain_on_an_unsupported_fs() { | ||
| let dir = scratch("unsup"); | ||
| let path = dir.join("x.node"); | ||
| let content = fake_addon(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Unsupported(UnsupportedReason::Filesystem), | ||
| apply_error: None, | ||
| }; | ||
| let out = compress_bytes_with(&backend, &path, &content, &Gate::any()); | ||
| assert!( | ||
| matches!(out, Ok(Outcome::Unsupported { .. })), | ||
| "got {out:?}" | ||
| ); | ||
| assert_eq!(std::fs::read(&path).unwrap(), content, "bytes landed plain"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // detect → Supported but the guarded apply is skipped (faked permission failure): | ||
| // the bytes land via a plain write, Outcome::Skipped(IntegrityRevert). | ||
| #[test] | ||
| fn compress_bytes_falls_back_to_plain_on_a_guarded_skip() { | ||
| let dir = scratch("guard-skip"); | ||
| let path = dir.join("x.node"); | ||
| let content = fake_addon(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Supported, | ||
| apply_error: Some(std::io::ErrorKind::PermissionDenied), | ||
| }; | ||
| let out = compress_bytes_with(&backend, &path, &content, &Gate::any()); | ||
| assert!( | ||
| matches!( | ||
| out, | ||
| Ok(Outcome::Skipped { | ||
| reason: SkipReason::IntegrityRevert | ||
| }) | ||
| ), | ||
| "got {out:?}" | ||
| ); | ||
| assert_eq!(std::fs::read(&path).unwrap(), content, "bytes landed plain"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_errors_when_the_source_is_missing() { | ||
| let dir = scratch("copy-missing"); | ||
| let out = copy_file(&dir.join("absent.node"), &dir.join("dest.node")); | ||
| assert!(matches!(out, Err(Error::NotFound(_)))); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| /// A fallback fake: no clone path (trait default), reports the source | ||
| /// compressed, and its apply actually writes — so the guarded one-pass copy | ||
| /// arm runs end to end and classifies via the backend signal. | ||
| struct RecompressingFake; | ||
| impl Backend for RecompressingFake { | ||
| fn detect(&self, _path: &Path) -> Result<Support, Error> { | ||
| Ok(Support::Supported) | ||
| } | ||
| fn is_already_compressed(&self, _path: &Path) -> Result<bool, Error> { | ||
| Ok(true) | ||
| } | ||
| fn apply_inplace(&self, _path: &Path, _snapshot: &[u8]) -> Result<(), Error> { | ||
| Ok(()) | ||
| } | ||
| fn apply_bytes( | ||
| &self, | ||
| path: &Path, | ||
| content: &[u8], | ||
| _mode: Option<std::fs::Permissions>, | ||
| ) -> Result<(), Error> { | ||
| std::fs::write(path, content).map_err(|source| Error::Io { | ||
| context: "fake write", | ||
| source, | ||
| }) | ||
| } | ||
| fn compressed_on_disk(&self, _path: &Path) -> Result<Option<bool>, Error> { | ||
| Ok(Some(true)) | ||
| } | ||
| } | ||
| #[test] | ||
| fn copy_file_recompresses_at_the_destination_when_it_cannot_clone() { | ||
| let dir = scratch("copy-recompress"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| let content = fake_addon(); | ||
| std::fs::write(&src, &content).unwrap(); | ||
| let out = copy_file_with(&RecompressingFake, &src, &dest).unwrap(); | ||
| assert!( | ||
| matches!(out, CopyOutcome::CopiedCompressed { .. }), | ||
| "got {out:?}" | ||
| ); | ||
| assert_eq!( | ||
| std::fs::read(&dest).unwrap(), | ||
| content, | ||
| "bytes are identical" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_with_mock_backend_takes_the_clone_fast_path() { | ||
| // mockall MockBackend mocks the fs backend seam (no real syscalls); tempfile | ||
| // gives a real, isolated, auto-cleaned src fixture. clone_file → true | ||
| // short-circuits copy_file_with to the zero-cost Cloned outcome. | ||
| let dir = tempfile::tempdir().unwrap(); | ||
| let src = dir.path().join("a.node"); | ||
| std::fs::write(&src, b"native").unwrap(); | ||
| let dest = dir.path().join("b.node"); | ||
| let mut backend = MockBackend::new(); | ||
| backend | ||
| .expect_is_already_compressed() | ||
| .returning(|_| Ok(true)); | ||
| backend.expect_clone_file().returning(|_, _| Ok(true)); | ||
| let out = copy_file_with(&backend, &src, &dest).unwrap(); | ||
| assert!( | ||
| matches!(out, CopyOutcome::Cloned { compressed: true }), | ||
| "clone fast-path → Cloned; got {out:?}" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn copy_file_copies_a_plain_source_plain_and_replaces_the_destination() { | ||
| let dir = scratch("copy-plain"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| let content = fake_addon(); | ||
| std::fs::write(&src, &content).unwrap(); | ||
| std::fs::write(&dest, b"stale destination").unwrap(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Supported, | ||
| apply_error: None, | ||
| }; | ||
| let out = copy_file_with(&backend, &src, &dest).unwrap(); | ||
| assert_eq!(out, CopyOutcome::CopiedPlain { skipped: None }); | ||
| assert_eq!( | ||
| std::fs::read(&dest).unwrap(), | ||
| content, | ||
| "destination replaced" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_lands_plain_and_reports_the_skip_when_recompression_fails() { | ||
| struct SkippingFake; | ||
| impl Backend for SkippingFake { | ||
| fn detect(&self, _path: &Path) -> Result<Support, Error> { | ||
| Ok(Support::Supported) | ||
| } | ||
| fn is_already_compressed(&self, _path: &Path) -> Result<bool, Error> { | ||
| Ok(true) | ||
| } | ||
| fn apply_inplace(&self, _path: &Path, _snapshot: &[u8]) -> Result<(), Error> { | ||
| Ok(()) | ||
| } | ||
| fn apply_bytes( | ||
| &self, | ||
| _path: &Path, | ||
| _content: &[u8], | ||
| _mode: Option<std::fs::Permissions>, | ||
| ) -> Result<(), Error> { | ||
| Err(Error::Io { | ||
| context: "fake apply", | ||
| source: std::io::Error::from(std::io::ErrorKind::PermissionDenied), | ||
| }) | ||
| } | ||
| fn compressed_on_disk(&self, _path: &Path) -> Result<Option<bool>, Error> { | ||
| Ok(Some(false)) | ||
| } | ||
| } | ||
| let dir = scratch("copy-skip"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| let content = fake_addon(); | ||
| std::fs::write(&src, &content).unwrap(); | ||
| let out = copy_file_with(&SkippingFake, &src, &dest).unwrap(); | ||
| assert!( | ||
| matches!(out, CopyOutcome::CopiedPlain { skipped: Some(_) }), | ||
| "got {out:?}" | ||
| ); | ||
| assert_eq!(std::fs::read(&dest).unwrap(), content, "bytes still landed"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_onto_itself_is_a_noop_reported_as_cloned() { | ||
| let dir = scratch("copy-self"); | ||
| let src = dir.join("src.node"); | ||
| let content = fake_addon(); | ||
| std::fs::write(&src, &content).unwrap(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Supported, | ||
| apply_error: None, | ||
| }; | ||
| let out = copy_file_with(&backend, &src, &src).unwrap(); | ||
| assert!(matches!(out, CopyOutcome::Cloned { .. }), "got {out:?}"); | ||
| assert_eq!(std::fs::read(&src).unwrap(), content, "source untouched"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn is_same_file_sees_hardlinks_and_distinct_files() { | ||
| let dir = scratch("same-file"); | ||
| let a = dir.join("a.node"); | ||
| let b = dir.join("b.node"); | ||
| std::fs::write(&a, b"bytes").unwrap(); | ||
| std::fs::write(&b, b"bytes").unwrap(); | ||
| assert!(is_same_file(&a, &a), "identical path"); | ||
| assert!(!is_same_file(&a, &b), "distinct files"); | ||
| let link = dir.join("a-link.node"); | ||
| std::fs::hard_link(&a, &link).unwrap(); | ||
| assert!(is_same_file(&a, &link), "hardlink shares the inode"); | ||
| assert!( | ||
| !is_same_file(&a, &dir.join("absent.node")), | ||
| "a missing path is never the same file" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_onto_a_hardlink_is_a_noop_reported_as_cloned() { | ||
| let dir = scratch("copy-hardlink"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| let content = fake_addon(); | ||
| std::fs::write(&src, &content).unwrap(); | ||
| std::fs::hard_link(&src, &dest).unwrap(); | ||
| let out = copy_file(&src, &dest).unwrap(); | ||
| assert!(matches!(out, CopyOutcome::Cloned { .. }), "got {out:?}"); | ||
| assert_eq!(std::fs::read(&src).unwrap(), content, "source untouched"); | ||
| assert_eq!(std::fs::read(&dest).unwrap(), content, "hardlink untouched"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_errors_when_the_destination_cannot_be_replaced() { | ||
| let dir = scratch("copy-dest-dir"); | ||
| let src = dir.join("src.node"); | ||
| std::fs::write(&src, fake_addon()).unwrap(); | ||
| // A directory at `dest` makes the replace step's remove_file fail. | ||
| let dest = dir.join("dest.node"); | ||
| std::fs::create_dir(&dest).unwrap(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Supported, | ||
| apply_error: None, | ||
| }; | ||
| let out = copy_file_with(&backend, &src, &dest); | ||
| assert!( | ||
| matches!( | ||
| out, | ||
| Err(Error::Io { | ||
| context: "replace existing destination", | ||
| .. | ||
| }) | ||
| ), | ||
| "got {out:?}" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn copy_file_errors_when_the_source_is_unreadable() { | ||
| use std::os::unix::fs::PermissionsExt; | ||
| let dir = scratch("copy-unreadable"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| std::fs::write(&src, fake_addon()).unwrap(); | ||
| std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o000)).unwrap(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Supported, | ||
| apply_error: None, | ||
| }; | ||
| let out = copy_file_with(&backend, &src, &dest); | ||
| std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o644)).ok(); | ||
| assert!( | ||
| matches!( | ||
| out, | ||
| Err(Error::Io { | ||
| context: "read copy source", | ||
| .. | ||
| }) | ||
| ), | ||
| "got {out:?}" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn try_clone_file_errors_when_the_source_is_missing() { | ||
| let dir = scratch("clone-missing"); | ||
| let out = try_clone_file(&dir.join("absent.node"), &dir.join("dest.node")); | ||
| assert!(matches!(out, Err(Error::NotFound(_)))); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn try_clone_file_clones_on_apfs_and_declines_an_existing_destination() { | ||
| let dir = scratch("clone-try"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| std::fs::write(&src, fake_addon()).unwrap(); | ||
| assert!(try_clone_file(&src, &dest).unwrap(), "fresh clone"); | ||
| // clonefile refuses an existing destination — reported as cannot-clone, | ||
| // never an error. | ||
| assert!(!try_clone_file(&src, &dest).unwrap()); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn copy_file_clones_a_compressed_source_on_apfs() { | ||
| let dir = scratch("copy-clone"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| let content = fake_addon(); | ||
| let wrote = compress_bytes(&src, &content, &Gate::any()).unwrap(); | ||
| // Only meaningful when the scratch volume actually compressed the source. | ||
| if !matches!(wrote, Outcome::Compressed { .. }) { | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| return; | ||
| } | ||
| let out = copy_file(&src, &dest).unwrap(); | ||
| assert_eq!(out, CopyOutcome::Cloned { compressed: true }); | ||
| assert!(backend::is_already_compressed(&dest).unwrap()); | ||
| assert_eq!( | ||
| std::fs::read(&dest).unwrap(), | ||
| content, | ||
| "bytes are identical" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } |
| { | ||
| "git": { | ||
| "sha1": "9e4134a23d4805e2e2c4b739f33847728f600dd4" | ||
| "sha1": "57e9795d90bcd9acbe224a968318e759f05d396b" | ||
| }, | ||
| "path_in_vcs": "crates/decmpfs" | ||
| } |
+1
-1
@@ -86,3 +86,3 @@ # This file is automatically @generated by Cargo. | ||
| name = "decmpfs" | ||
| version = "0.1.2" | ||
| version = "0.1.3" | ||
| dependencies = [ | ||
@@ -89,0 +89,0 @@ "libc", |
+2
-2
@@ -15,3 +15,3 @@ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO | ||
| name = "decmpfs" | ||
| version = "0.1.2" | ||
| version = "0.1.3" | ||
| build = "build.rs" | ||
@@ -34,3 +34,3 @@ autolib = false | ||
| license = "MIT" | ||
| repository = "https://github.com/decmpfs/decmpfs" | ||
| repository = "https://github.com/SocketDev/decmpfs" | ||
@@ -37,0 +37,0 @@ [features] |
+1
-663
@@ -620,664 +620,2 @@ //! `decmpfs` — apply the operating system's transparent per-file compression to a file | ||
| #[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-{tag}-{}", std::process::id())); | ||
| // A pid-recycled leftover FILE at this path makes create_dir_all fail | ||
| // with AlreadyExists; clear it so the scratch dir always materializes. | ||
| let _ = std::fs::remove_file(&dir); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| dir | ||
| } | ||
| // A minimal native-magic payload (ELF header) so a backend will attempt to | ||
| // compress it rather than skip a trivially-small file. | ||
| fn fake_addon() -> Vec<u8> { | ||
| let mut raw = vec![0x7f, 0x45, 0x4c, 0x46]; | ||
| raw.extend_from_slice(&[7u8; 9000]); | ||
| raw | ||
| } | ||
| #[test] | ||
| fn compress_file_errors_when_missing() { | ||
| let p = std::path::Path::new("/no/such/addon.node"); | ||
| assert!(matches!(compress_file(p), Err(Error::NotFound(_)))); | ||
| } | ||
| #[test] | ||
| fn plain_write_errors_when_the_path_has_no_parent() { | ||
| // "/" has no parent directory → the no-parent guard fires before any write. | ||
| let out = plain_write(std::path::Path::new("/"), b"x"); | ||
| assert!(matches!( | ||
| out, | ||
| Err(Error::Io { | ||
| context: "no parent dir", | ||
| .. | ||
| }) | ||
| )); | ||
| } | ||
| #[test] | ||
| fn error_display_and_source() { | ||
| let nf = Error::NotFound(std::path::PathBuf::from("/x")); | ||
| assert!(nf.to_string().contains("not found")); | ||
| assert!(std::error::Error::source(&nf).is_none()); | ||
| let io = Error::Io { | ||
| context: "ctx", | ||
| source: std::io::Error::from(std::io::ErrorKind::PermissionDenied), | ||
| }; | ||
| assert!(io.to_string().contains("ctx")); | ||
| assert!(std::error::Error::source(&io).is_some()); | ||
| } | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn probe_reports_a_support_variant_without_mutating() { | ||
| // probe never errors on an existing path — it returns a Support. | ||
| assert!(matches!( | ||
| probe(std::path::Path::new("/dev/null")), | ||
| Ok(Support::Supported | Support::AlreadyCompressed | Support::Unsupported(_)) | ||
| )); | ||
| } | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn compress_file_reports_unsupported_on_a_non_compressing_fs() { | ||
| // /dev/null exists but devfs has no compression backend → Unsupported. | ||
| let out = compress_file(std::path::Path::new("/dev/null")); | ||
| assert!( | ||
| matches!(out, Ok(Outcome::Unsupported { .. })), | ||
| "devfs → Unsupported, got {out:?}" | ||
| ); | ||
| } | ||
| // APFS is always a compressing FS, so macOS exercises the full success path: | ||
| // compress_file → apply_guarded → backend::apply_inplace → verify → classify. | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn compress_file_compresses_then_is_idempotent_and_transparent() { | ||
| let dir = scratch("ok"); | ||
| let path = dir.join("addon.node"); | ||
| std::fs::write(&path, fake_addon()).unwrap(); | ||
| let out = compress_file(&path); | ||
| assert!( | ||
| matches!( | ||
| out, | ||
| Ok(Outcome::Compressed { .. } | Outcome::NoGain { .. } | Outcome::AlreadyCompressed { .. }) | ||
| ), | ||
| "writable addon on APFS → applied, got {out:?}" | ||
| ); | ||
| // Transparent: the kernel hands back the exact original bytes. | ||
| assert_eq!(std::fs::read(&path).unwrap(), fake_addon()); | ||
| // Idempotent: a second pass detects it's already compressed. | ||
| assert!(matches!( | ||
| compress_file(&path), | ||
| Ok(Outcome::AlreadyCompressed { .. }) | ||
| )); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // compress_bytes one-pass: write bytes directly as an APFS-compressed file with | ||
| // no pre-existing original, then prove the kernel hands the exact bytes back. | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn compress_bytes_one_pass_writes_compressed_and_reads_back_identical() { | ||
| let dir = scratch("bytes"); | ||
| let path = dir.join("fresh.node"); | ||
| let content = fake_addon(); | ||
| // No file at `path` yet — compress_bytes creates it in one pass. | ||
| let out = compress_bytes(&path, &content, &Gate::any()); | ||
| assert!( | ||
| matches!(out, Ok(Outcome::Compressed { .. } | Outcome::NoGain { .. })), | ||
| "one-pass APFS write → applied, got {out:?}" | ||
| ); | ||
| assert!(path.exists(), "file was created"); | ||
| // Transparent: kernel read-back equals the bytes we asked to store. | ||
| assert_eq!(std::fs::read(&path).unwrap(), content); | ||
| // It really carries the compression flag (not a plain fallback write). | ||
| assert!(matches!( | ||
| compress_file(&path), | ||
| Ok(Outcome::AlreadyCompressed { .. }) | ||
| )); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // A file the gate excludes is written PLAIN (never compressed) and reports | ||
| // Skipped(GateExcluded) — the install still gets the file. | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn compress_bytes_gate_excluded_writes_plain() { | ||
| let dir = scratch("gate"); | ||
| let path = dir.join("not-an-addon.txt"); | ||
| let content = b"plain text, not a .node".to_vec(); | ||
| let gate = Gate::default(); // **/*.node | ||
| let out = compress_bytes(&path, &content, &gate); | ||
| assert!( | ||
| matches!( | ||
| out, | ||
| Ok(Outcome::Skipped { | ||
| reason: SkipReason::GateExcluded | ||
| }) | ||
| ), | ||
| "non-.node → GateExcluded, got {out:?}" | ||
| ); | ||
| assert_eq!(std::fs::read(&path).unwrap(), content); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn compress_bytes_falls_back_to_plain_on_unsupported_fs() { | ||
| // A non-compressing FS (devfs) → plain write, Unsupported Outcome, file lands. | ||
| // /dev isn't writable by us, so target a temp path but force the gate to pass; | ||
| // temp on macOS is APFS (compresses) — instead assert the API never errors and | ||
| // the bytes land for the supported case is covered above. Here just exercise | ||
| // the gate-passing path lands bytes on any unix temp. | ||
| let dir = scratch("fallback"); | ||
| let path = dir.join("x.node"); | ||
| let content = fake_addon(); | ||
| let out = compress_bytes(&path, &content, &Gate::any()); | ||
| assert!(out.is_ok(), "never errors on a normal temp, got {out:?}"); | ||
| assert_eq!(std::fs::read(&path).unwrap(), content, "bytes always land"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn compress_file_skips_a_read_only_file() { | ||
| // On a compressing FS a read-only file can't be opened rw → fail-soft turns the | ||
| // EACCES into Skipped(PermissionDenied). Root bypasses mode bits, so skip there. | ||
| if unsafe { libc::geteuid() } == 0 { | ||
| return; | ||
| } | ||
| let dir = scratch("ro"); | ||
| let path = dir.join("addon.node"); | ||
| std::fs::write(&path, fake_addon()).unwrap(); | ||
| if !matches!(probe(&path), Ok(Support::Supported)) { | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| return; | ||
| } | ||
| let mut perm = std::fs::metadata(&path).unwrap().permissions(); | ||
| perm.set_readonly(true); | ||
| std::fs::set_permissions(&path, perm).unwrap(); | ||
| let outcome = compress_file(&path); | ||
| use std::os::unix::fs::PermissionsExt; | ||
| std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).ok(); | ||
| assert!( | ||
| matches!( | ||
| outcome, | ||
| Ok(Outcome::Skipped { | ||
| reason: SkipReason::PermissionDenied | ||
| }) | ||
| ), | ||
| "read-only → Skipped(PermissionDenied), got {outcome:?}" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // An existing target exercises the `path.exists()` probe-target branch and the | ||
| // fresh-inode rename that replaces the old contents. | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn compress_bytes_overwrites_an_existing_file() { | ||
| let dir = scratch("overwrite"); | ||
| let path = dir.join("addon.node"); | ||
| std::fs::write(&path, b"stale contents").unwrap(); | ||
| let content = fake_addon(); | ||
| let out = compress_bytes(&path, &content, &Gate::any()); | ||
| assert!(out.is_ok(), "overwrite never errors, got {out:?}"); | ||
| assert_eq!( | ||
| std::fs::read(&path).unwrap(), | ||
| content, | ||
| "new bytes replace the old" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // `path` is an existing directory: the backend builds its temp then can't rename | ||
| // a file over a directory, and the plain-write fallback can't either → a hard | ||
| // `Err` (genuine I/O failure), never a corrupt success. Exercises the backend | ||
| // rename-error cleanup and the `Err(_)` fallback arm of compress_bytes. | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn compress_bytes_onto_a_directory_path_is_a_hard_error() { | ||
| let dir = scratch("dir-target"); | ||
| let target = dir.join("a-dir"); | ||
| std::fs::create_dir_all(&target).unwrap(); | ||
| let out = compress_bytes(&target, &fake_addon(), &Gate::any()); | ||
| assert!( | ||
| out.is_err(), | ||
| "cannot write a file over a directory, got {out:?}" | ||
| ); | ||
| assert!(target.is_dir(), "the directory is left intact"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn stat_reports_size_and_uncompressed_for_a_plain_file() { | ||
| let dir = scratch("stat-plain"); | ||
| let path = dir.join("f"); | ||
| std::fs::write(&path, vec![0u8; 4096]).unwrap(); | ||
| let s = stat(&path).unwrap(); | ||
| assert_eq!(s.logical, 4096, "logical == the written bytes"); | ||
| assert!(s.physical > 0, "allocated bytes reported"); | ||
| assert!( | ||
| !s.compressed, | ||
| "a freshly-written plain file is not FS-compressed" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn stat_reflects_a_compressed_file_where_supported() { | ||
| let dir = scratch("stat-comp"); | ||
| let path = dir.join("addon.node"); | ||
| let content = vec![0xABu8; 128 * 1024]; | ||
| let outcome = compress_bytes(&path, &content, &Gate::any()).unwrap(); | ||
| let s = stat(&path).unwrap(); | ||
| assert_eq!( | ||
| s.logical, | ||
| content.len() as u64, | ||
| "logical == the written bytes" | ||
| ); | ||
| assert_eq!( | ||
| std::fs::read(&path).unwrap(), | ||
| content, | ||
| "content round-trips" | ||
| ); | ||
| // Where the FS actually compressed (APFS / btrfs / NTFS), stat must reflect | ||
| // it; on an unsupported FS the outcome isn't Compressed and we only assert | ||
| // the size + content invariants above. | ||
| if matches!(outcome, Outcome::Compressed { .. }) { | ||
| assert!( | ||
| s.compressed, | ||
| "a Compressed outcome → stat reports compressed" | ||
| ); | ||
| assert!( | ||
| s.physical < s.logical, | ||
| "allocation shrank below the logical size" | ||
| ); | ||
| } | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // A read-only parent dir: the guarded backend write hits EACCES (classify_skip → | ||
| // Skipped), then the plain-write fallback also can't write → `Err`. Root bypasses | ||
| // mode bits, so skip there. | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn compress_bytes_into_a_read_only_dir_is_fail_soft() { | ||
| if unsafe { libc::geteuid() } == 0 { | ||
| return; | ||
| } | ||
| use std::os::unix::fs::PermissionsExt; | ||
| let dir = scratch("ro-dir"); | ||
| let locked = dir.join("locked"); | ||
| std::fs::create_dir_all(&locked).unwrap(); | ||
| std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o555)).unwrap(); | ||
| let out = compress_bytes(&locked.join("x.node"), &fake_addon(), &Gate::any()); | ||
| // Restore write perms so the tree can be cleaned up. | ||
| std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).ok(); | ||
| assert!(out.is_err(), "a read-only dir admits no write, got {out:?}"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // The `Support::AlreadyCompressed`-from-detect arm: a real macOS detect never | ||
| // returns it (it reports already-compressed via the apply path), so a fake drives | ||
| // it. Needs a real file for the on-disk-bytes read. | ||
| #[test] | ||
| fn compress_file_reports_already_compressed_from_detect() { | ||
| let dir = scratch("already-detect"); | ||
| let path = dir.join("f.node"); | ||
| std::fs::write(&path, fake_addon()).unwrap(); | ||
| let backend = FakeBackend { | ||
| detect: Support::AlreadyCompressed, | ||
| apply_error: None, | ||
| }; | ||
| assert!(matches!( | ||
| compress_file_with(&backend, &path), | ||
| Ok(Outcome::AlreadyCompressed { .. }) | ||
| )); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // detect → Unsupported: the bytes still land via a plain write, Outcome::Unsupported. | ||
| #[test] | ||
| fn compress_bytes_falls_back_to_plain_on_an_unsupported_fs() { | ||
| let dir = scratch("unsup"); | ||
| let path = dir.join("x.node"); | ||
| let content = fake_addon(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Unsupported(UnsupportedReason::Filesystem), | ||
| apply_error: None, | ||
| }; | ||
| let out = compress_bytes_with(&backend, &path, &content, &Gate::any()); | ||
| assert!( | ||
| matches!(out, Ok(Outcome::Unsupported { .. })), | ||
| "got {out:?}" | ||
| ); | ||
| assert_eq!(std::fs::read(&path).unwrap(), content, "bytes landed plain"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // detect → Supported but the guarded apply is skipped (faked permission failure): | ||
| // the bytes land via a plain write, Outcome::Skipped(IntegrityRevert). | ||
| #[test] | ||
| fn compress_bytes_falls_back_to_plain_on_a_guarded_skip() { | ||
| let dir = scratch("guard-skip"); | ||
| let path = dir.join("x.node"); | ||
| let content = fake_addon(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Supported, | ||
| apply_error: Some(std::io::ErrorKind::PermissionDenied), | ||
| }; | ||
| let out = compress_bytes_with(&backend, &path, &content, &Gate::any()); | ||
| assert!( | ||
| matches!( | ||
| out, | ||
| Ok(Outcome::Skipped { | ||
| reason: SkipReason::IntegrityRevert | ||
| }) | ||
| ), | ||
| "got {out:?}" | ||
| ); | ||
| assert_eq!(std::fs::read(&path).unwrap(), content, "bytes landed plain"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_errors_when_the_source_is_missing() { | ||
| let dir = scratch("copy-missing"); | ||
| let out = copy_file(&dir.join("absent.node"), &dir.join("dest.node")); | ||
| assert!(matches!(out, Err(Error::NotFound(_)))); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| /// A fallback fake: no clone path (trait default), reports the source | ||
| /// compressed, and its apply actually writes — so the guarded one-pass copy | ||
| /// arm runs end to end and classifies via the backend signal. | ||
| struct RecompressingFake; | ||
| impl Backend for RecompressingFake { | ||
| fn detect(&self, _path: &Path) -> Result<Support, Error> { | ||
| Ok(Support::Supported) | ||
| } | ||
| fn is_already_compressed(&self, _path: &Path) -> Result<bool, Error> { | ||
| Ok(true) | ||
| } | ||
| fn apply_inplace(&self, _path: &Path, _snapshot: &[u8]) -> Result<(), Error> { | ||
| Ok(()) | ||
| } | ||
| fn apply_bytes( | ||
| &self, | ||
| path: &Path, | ||
| content: &[u8], | ||
| _mode: Option<std::fs::Permissions>, | ||
| ) -> Result<(), Error> { | ||
| std::fs::write(path, content).map_err(|source| Error::Io { | ||
| context: "fake write", | ||
| source, | ||
| }) | ||
| } | ||
| fn compressed_on_disk(&self, _path: &Path) -> Result<Option<bool>, Error> { | ||
| Ok(Some(true)) | ||
| } | ||
| } | ||
| #[test] | ||
| fn copy_file_recompresses_at_the_destination_when_it_cannot_clone() { | ||
| let dir = scratch("copy-recompress"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| let content = fake_addon(); | ||
| std::fs::write(&src, &content).unwrap(); | ||
| let out = copy_file_with(&RecompressingFake, &src, &dest).unwrap(); | ||
| assert!( | ||
| matches!(out, CopyOutcome::CopiedCompressed { .. }), | ||
| "got {out:?}" | ||
| ); | ||
| assert_eq!( | ||
| std::fs::read(&dest).unwrap(), | ||
| content, | ||
| "bytes are identical" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_with_mock_backend_takes_the_clone_fast_path() { | ||
| // mockall MockBackend mocks the fs backend seam (no real syscalls); tempfile | ||
| // gives a real, isolated, auto-cleaned src fixture. clone_file → true | ||
| // short-circuits copy_file_with to the zero-cost Cloned outcome. | ||
| let dir = tempfile::tempdir().unwrap(); | ||
| let src = dir.path().join("a.node"); | ||
| std::fs::write(&src, b"native").unwrap(); | ||
| let dest = dir.path().join("b.node"); | ||
| let mut backend = MockBackend::new(); | ||
| backend | ||
| .expect_is_already_compressed() | ||
| .returning(|_| Ok(true)); | ||
| backend.expect_clone_file().returning(|_, _| Ok(true)); | ||
| let out = copy_file_with(&backend, &src, &dest).unwrap(); | ||
| assert!( | ||
| matches!(out, CopyOutcome::Cloned { compressed: true }), | ||
| "clone fast-path → Cloned; got {out:?}" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn copy_file_copies_a_plain_source_plain_and_replaces_the_destination() { | ||
| let dir = scratch("copy-plain"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| let content = fake_addon(); | ||
| std::fs::write(&src, &content).unwrap(); | ||
| std::fs::write(&dest, b"stale destination").unwrap(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Supported, | ||
| apply_error: None, | ||
| }; | ||
| let out = copy_file_with(&backend, &src, &dest).unwrap(); | ||
| assert_eq!(out, CopyOutcome::CopiedPlain { skipped: None }); | ||
| assert_eq!( | ||
| std::fs::read(&dest).unwrap(), | ||
| content, | ||
| "destination replaced" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_lands_plain_and_reports_the_skip_when_recompression_fails() { | ||
| struct SkippingFake; | ||
| impl Backend for SkippingFake { | ||
| fn detect(&self, _path: &Path) -> Result<Support, Error> { | ||
| Ok(Support::Supported) | ||
| } | ||
| fn is_already_compressed(&self, _path: &Path) -> Result<bool, Error> { | ||
| Ok(true) | ||
| } | ||
| fn apply_inplace(&self, _path: &Path, _snapshot: &[u8]) -> Result<(), Error> { | ||
| Ok(()) | ||
| } | ||
| fn apply_bytes( | ||
| &self, | ||
| _path: &Path, | ||
| _content: &[u8], | ||
| _mode: Option<std::fs::Permissions>, | ||
| ) -> Result<(), Error> { | ||
| Err(Error::Io { | ||
| context: "fake apply", | ||
| source: std::io::Error::from(std::io::ErrorKind::PermissionDenied), | ||
| }) | ||
| } | ||
| fn compressed_on_disk(&self, _path: &Path) -> Result<Option<bool>, Error> { | ||
| Ok(Some(false)) | ||
| } | ||
| } | ||
| let dir = scratch("copy-skip"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| let content = fake_addon(); | ||
| std::fs::write(&src, &content).unwrap(); | ||
| let out = copy_file_with(&SkippingFake, &src, &dest).unwrap(); | ||
| assert!( | ||
| matches!(out, CopyOutcome::CopiedPlain { skipped: Some(_) }), | ||
| "got {out:?}" | ||
| ); | ||
| assert_eq!(std::fs::read(&dest).unwrap(), content, "bytes still landed"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_onto_itself_is_a_noop_reported_as_cloned() { | ||
| let dir = scratch("copy-self"); | ||
| let src = dir.join("src.node"); | ||
| let content = fake_addon(); | ||
| std::fs::write(&src, &content).unwrap(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Supported, | ||
| apply_error: None, | ||
| }; | ||
| let out = copy_file_with(&backend, &src, &src).unwrap(); | ||
| assert!(matches!(out, CopyOutcome::Cloned { .. }), "got {out:?}"); | ||
| assert_eq!(std::fs::read(&src).unwrap(), content, "source untouched"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn is_same_file_sees_hardlinks_and_distinct_files() { | ||
| let dir = scratch("same-file"); | ||
| let a = dir.join("a.node"); | ||
| let b = dir.join("b.node"); | ||
| std::fs::write(&a, b"bytes").unwrap(); | ||
| std::fs::write(&b, b"bytes").unwrap(); | ||
| assert!(is_same_file(&a, &a), "identical path"); | ||
| assert!(!is_same_file(&a, &b), "distinct files"); | ||
| let link = dir.join("a-link.node"); | ||
| std::fs::hard_link(&a, &link).unwrap(); | ||
| assert!(is_same_file(&a, &link), "hardlink shares the inode"); | ||
| assert!( | ||
| !is_same_file(&a, &dir.join("absent.node")), | ||
| "a missing path is never the same file" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_onto_a_hardlink_is_a_noop_reported_as_cloned() { | ||
| let dir = scratch("copy-hardlink"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| let content = fake_addon(); | ||
| std::fs::write(&src, &content).unwrap(); | ||
| std::fs::hard_link(&src, &dest).unwrap(); | ||
| let out = copy_file(&src, &dest).unwrap(); | ||
| assert!(matches!(out, CopyOutcome::Cloned { .. }), "got {out:?}"); | ||
| assert_eq!(std::fs::read(&src).unwrap(), content, "source untouched"); | ||
| assert_eq!(std::fs::read(&dest).unwrap(), content, "hardlink untouched"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn copy_file_errors_when_the_destination_cannot_be_replaced() { | ||
| let dir = scratch("copy-dest-dir"); | ||
| let src = dir.join("src.node"); | ||
| std::fs::write(&src, fake_addon()).unwrap(); | ||
| // A directory at `dest` makes the replace step's remove_file fail. | ||
| let dest = dir.join("dest.node"); | ||
| std::fs::create_dir(&dest).unwrap(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Supported, | ||
| apply_error: None, | ||
| }; | ||
| let out = copy_file_with(&backend, &src, &dest); | ||
| assert!( | ||
| matches!( | ||
| out, | ||
| Err(Error::Io { | ||
| context: "replace existing destination", | ||
| .. | ||
| }) | ||
| ), | ||
| "got {out:?}" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[cfg(unix)] | ||
| #[test] | ||
| fn copy_file_errors_when_the_source_is_unreadable() { | ||
| use std::os::unix::fs::PermissionsExt; | ||
| let dir = scratch("copy-unreadable"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| std::fs::write(&src, fake_addon()).unwrap(); | ||
| std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o000)).unwrap(); | ||
| let backend = FakeBackend { | ||
| detect: Support::Supported, | ||
| apply_error: None, | ||
| }; | ||
| let out = copy_file_with(&backend, &src, &dest); | ||
| std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o644)).ok(); | ||
| assert!( | ||
| matches!( | ||
| out, | ||
| Err(Error::Io { | ||
| context: "read copy source", | ||
| .. | ||
| }) | ||
| ), | ||
| "got {out:?}" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn try_clone_file_errors_when_the_source_is_missing() { | ||
| let dir = scratch("clone-missing"); | ||
| let out = try_clone_file(&dir.join("absent.node"), &dir.join("dest.node")); | ||
| assert!(matches!(out, Err(Error::NotFound(_)))); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn try_clone_file_clones_on_apfs_and_declines_an_existing_destination() { | ||
| let dir = scratch("clone-try"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| std::fs::write(&src, fake_addon()).unwrap(); | ||
| assert!(try_clone_file(&src, &dest).unwrap(), "fresh clone"); | ||
| // clonefile refuses an existing destination — reported as cannot-clone, | ||
| // never an error. | ||
| assert!(!try_clone_file(&src, &dest).unwrap()); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[cfg(target_os = "macos")] | ||
| #[test] | ||
| fn copy_file_clones_a_compressed_source_on_apfs() { | ||
| let dir = scratch("copy-clone"); | ||
| let src = dir.join("src.node"); | ||
| let dest = dir.join("dest.node"); | ||
| let content = fake_addon(); | ||
| let wrote = compress_bytes(&src, &content, &Gate::any()).unwrap(); | ||
| // Only meaningful when the scratch volume actually compressed the source. | ||
| if !matches!(wrote, Outcome::Compressed { .. }) { | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| return; | ||
| } | ||
| let out = copy_file(&src, &dest).unwrap(); | ||
| assert_eq!(out, CopyOutcome::Cloned { compressed: true }); | ||
| assert!(backend::is_already_compressed(&dest).unwrap()); | ||
| assert_eq!( | ||
| std::fs::read(&dest).unwrap(), | ||
| content, | ||
| "bytes are identical" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| } | ||
| mod tests; |
+5
-922
@@ -472,493 +472,6 @@ //! macOS backend — APFS/HFS+ decmpfs transparent compression. | ||
| enum StreamingState { | ||
| Encoding(StreamingEncoding), | ||
| Plain(std::fs::File), | ||
| Closed, | ||
| } | ||
| #[path = "macos/streaming.rs"] | ||
| mod streaming; | ||
| pub(crate) use streaming::StreamingWriter; | ||
| struct StreamingEncoding { | ||
| file: std::fs::File, | ||
| fork: std::io::BufWriter<std::fs::File>, | ||
| scratch: Vec<u8>, | ||
| partial: Vec<u8>, | ||
| offsets: Vec<u32>, | ||
| encoded_offset: usize, | ||
| } | ||
| /// Incremental macOS writer used by the public streaming API. Raw input is held | ||
| /// only until the current 64 KiB block is complete; winning LZFSE blocks land | ||
| /// directly in the named resource fork. If the fork stops winning, its completed | ||
| /// blocks are decoded into a plain sibling and subsequent input streams there. | ||
| pub(crate) struct StreamingWriter { | ||
| path: std::path::PathBuf, | ||
| expected_len: usize, | ||
| written: usize, | ||
| state: StreamingState, | ||
| complete: bool, | ||
| } | ||
| impl StreamingEncoding { | ||
| fn write_block(&mut self, raw: &[u8], expected_len: usize) -> Result<bool, Error> { | ||
| let Some(encoded) = compress_block_with_codec(raw, &mut self.scratch, Codec::Lzfse) else { | ||
| return Ok(false); | ||
| }; | ||
| // Verify every encoder result while the matching raw block is still in | ||
| // memory. The final kernel oracle then only has to prove the decmpfs layout. | ||
| let mut decoded = vec![0u8; raw.len()]; | ||
| let decoded_len = unsafe { | ||
| compression_decode_buffer( | ||
| decoded.as_mut_ptr(), | ||
| decoded.len(), | ||
| encoded.as_ptr(), | ||
| encoded.len(), | ||
| std::ptr::null_mut(), | ||
| Codec::Lzfse.algorithm(), | ||
| ) | ||
| }; | ||
| if decoded_len != raw.len() || decoded != raw { | ||
| return Ok(false); | ||
| } | ||
| let Some(next_offset) = self.encoded_offset.checked_add(encoded.len()) else { | ||
| return Ok(false); | ||
| }; | ||
| if next_offset >= expected_len || next_offset > u32::MAX as usize { | ||
| return Ok(false); | ||
| } | ||
| use std::io::Write; | ||
| self.fork.write_all(&encoded).map_err(|source| Error::Io { | ||
| context: "write streaming resource-fork block", | ||
| source, | ||
| })?; | ||
| self.encoded_offset = next_offset; | ||
| self | ||
| .offsets | ||
| .push(u32::try_from(next_offset).map_err(|_| resource_fork_too_large())?); | ||
| Ok(true) | ||
| } | ||
| } | ||
| fn streaming_fallback_path(path: &Path) -> std::path::PathBuf { | ||
| static FALLBACK_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); | ||
| let seq = FALLBACK_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed); | ||
| let name = path.file_name().map_or_else( | ||
| || std::borrow::Cow::Borrowed("stream"), | ||
| |n| n.to_string_lossy(), | ||
| ); | ||
| path.with_file_name(format!(".{name}.plain-{}-{seq}.tmp", std::process::id())) | ||
| } | ||
| fn decode_streaming_prefix( | ||
| path: &Path, | ||
| encoding: &mut StreamingEncoding, | ||
| current: &[u8], | ||
| expected_len: usize, | ||
| ) -> Result<(std::path::PathBuf, std::fs::File), Error> { | ||
| use std::io::{Read, Seek, Write}; | ||
| encoding.fork.flush().map_err(|source| Error::Io { | ||
| context: "flush streaming resource fork", | ||
| source, | ||
| })?; | ||
| encoding | ||
| .fork | ||
| .get_ref() | ||
| .sync_all() | ||
| .map_err(|source| Error::Io { | ||
| context: "sync streaming resource fork", | ||
| source, | ||
| })?; | ||
| let fork_path = path.join("..namedfork").join("rsrc"); | ||
| let mut fork = std::fs::File::open(fork_path).map_err(|source| Error::Io { | ||
| context: "open streaming resource fork for fallback", | ||
| source, | ||
| })?; | ||
| let fallback = streaming_fallback_path(path); | ||
| let mut plain = std::fs::OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create_new(true) | ||
| .open(&fallback) | ||
| .map_err(|source| Error::Io { | ||
| context: "create streaming plain fallback", | ||
| source, | ||
| })?; | ||
| let decoded = (|| -> Result<(), Error> { | ||
| for (block_index, pair) in encoding.offsets.windows(2).enumerate() { | ||
| let start = pair[0] as u64; | ||
| let encoded_len = (pair[1] - pair[0]) as usize; | ||
| let mut encoded = vec![0u8; encoded_len]; | ||
| fork | ||
| .seek(std::io::SeekFrom::Start(start)) | ||
| .and_then(|_| fork.read_exact(&mut encoded)) | ||
| .map_err(|source| Error::Io { | ||
| context: "read streaming resource fork for fallback", | ||
| source, | ||
| })?; | ||
| let raw_len = expected_len | ||
| .saturating_sub(block_index.saturating_mul(BLOCK)) | ||
| .min(BLOCK); | ||
| let mut raw = vec![0u8; raw_len]; | ||
| let raw_len = unsafe { | ||
| compression_decode_buffer( | ||
| raw.as_mut_ptr(), | ||
| raw.len(), | ||
| encoded.as_ptr(), | ||
| encoded.len(), | ||
| std::ptr::null_mut(), | ||
| Codec::Lzfse.algorithm(), | ||
| ) | ||
| }; | ||
| if raw_len != raw.len() { | ||
| return Err(Error::Io { | ||
| context: "decode streaming resource fork for fallback", | ||
| source: std::io::Error::from(std::io::ErrorKind::InvalidData), | ||
| }); | ||
| } | ||
| plain.write_all(&raw).map_err(|source| Error::Io { | ||
| context: "write streaming plain fallback", | ||
| source, | ||
| })?; | ||
| } | ||
| plain.write_all(current).map_err(|source| Error::Io { | ||
| context: "write current streaming fallback block", | ||
| source, | ||
| }) | ||
| })(); | ||
| if let Err(err) = decoded { | ||
| drop(plain); | ||
| let _ = std::fs::remove_file(&fallback); | ||
| return Err(err); | ||
| } | ||
| Ok((fallback, plain)) | ||
| } | ||
| fn streaming_kernel_matches( | ||
| path: &Path, | ||
| encoding: &StreamingEncoding, | ||
| expected_len: usize, | ||
| ) -> Result<bool, Error> { | ||
| use std::io::{Read, Seek}; | ||
| let mut logical = match std::fs::File::open(path) { | ||
| Ok(file) => file, | ||
| Err(_) => return Ok(false), | ||
| }; | ||
| let fork_path = path.join("..namedfork").join("rsrc"); | ||
| let mut fork = std::fs::File::open(fork_path).map_err(|source| Error::Io { | ||
| context: "open finished streaming resource fork", | ||
| source, | ||
| })?; | ||
| for (block_index, pair) in encoding.offsets.windows(2).enumerate() { | ||
| let encoded_len = (pair[1] - pair[0]) as usize; | ||
| let mut encoded = vec![0u8; encoded_len]; | ||
| fork | ||
| .seek(std::io::SeekFrom::Start(pair[0] as u64)) | ||
| .and_then(|_| fork.read_exact(&mut encoded)) | ||
| .map_err(|source| Error::Io { | ||
| context: "read finished streaming resource fork", | ||
| source, | ||
| })?; | ||
| let raw_len = expected_len | ||
| .saturating_sub(block_index.saturating_mul(BLOCK)) | ||
| .min(BLOCK); | ||
| let mut decoded = vec![0u8; raw_len]; | ||
| let decoded_len = unsafe { | ||
| compression_decode_buffer( | ||
| decoded.as_mut_ptr(), | ||
| decoded.len(), | ||
| encoded.as_ptr(), | ||
| encoded.len(), | ||
| std::ptr::null_mut(), | ||
| Codec::Lzfse.algorithm(), | ||
| ) | ||
| }; | ||
| if decoded_len != raw_len { | ||
| return Ok(false); | ||
| } | ||
| let mut kernel = vec![0u8; raw_len]; | ||
| if logical.read_exact(&mut kernel).is_err() || kernel != decoded { | ||
| return Ok(false); | ||
| } | ||
| } | ||
| let mut extra = [0u8; 1]; | ||
| Ok(logical.read(&mut extra).is_ok_and(|len| len == 0)) | ||
| } | ||
| impl StreamingWriter { | ||
| pub(crate) fn new(path: &Path, expected_len: usize) -> Result<Self, Error> { | ||
| let num_blocks = expected_len.div_ceil(BLOCK).max(1); | ||
| let table_len = resource_fork_table_len(num_blocks)?; | ||
| if expected_len == 0 || table_len >= expected_len || table_len > u32::MAX as usize { | ||
| let file = std::fs::OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create_new(true) | ||
| .open(path) | ||
| .map_err(|source| Error::Io { | ||
| context: "create streaming plain temp", | ||
| source, | ||
| })?; | ||
| return Ok(Self { | ||
| path: path.to_path_buf(), | ||
| expected_len, | ||
| written: 0, | ||
| state: StreamingState::Plain(file), | ||
| complete: false, | ||
| }); | ||
| } | ||
| let file = std::fs::OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create_new(true) | ||
| .open(path) | ||
| .map_err(|source| Error::Io { | ||
| context: "create streaming decmpfs temp", | ||
| source, | ||
| })?; | ||
| let fork_file = (|| -> Result<std::fs::File, Error> { | ||
| let fork_path = path.join("..namedfork").join("rsrc"); | ||
| let mut fork_file = std::fs::OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create(true) | ||
| .truncate(true) | ||
| .open(fork_path) | ||
| .map_err(|source| Error::Io { | ||
| context: "open streaming resource fork", | ||
| source, | ||
| })?; | ||
| use std::io::Seek; | ||
| fork_file | ||
| .set_len(table_len as u64) | ||
| .map_err(|source| Error::Io { | ||
| context: "reserve streaming resource-fork table", | ||
| source, | ||
| })?; | ||
| fork_file | ||
| .seek(std::io::SeekFrom::Start(table_len as u64)) | ||
| .map_err(|source| Error::Io { | ||
| context: "seek streaming resource-fork payload", | ||
| source, | ||
| })?; | ||
| Ok(fork_file) | ||
| })(); | ||
| let fork_file = match fork_file { | ||
| Ok(fork_file) => fork_file, | ||
| Err(error) => { | ||
| drop(file); | ||
| let _ = std::fs::remove_file(path); | ||
| return Err(error); | ||
| } | ||
| }; | ||
| let scratch_len = unsafe { compression_encode_scratch_buffer_size(Codec::Lzfse.algorithm()) }; | ||
| Ok(Self { | ||
| path: path.to_path_buf(), | ||
| expected_len, | ||
| written: 0, | ||
| state: StreamingState::Encoding(StreamingEncoding { | ||
| file, | ||
| fork: std::io::BufWriter::with_capacity(1 << 20, fork_file), | ||
| scratch: vec![0u8; scratch_len], | ||
| partial: Vec::with_capacity(BLOCK), | ||
| offsets: vec![u32::try_from(table_len).map_err(|_| resource_fork_too_large())?], | ||
| encoded_offset: table_len, | ||
| }), | ||
| complete: false, | ||
| }) | ||
| } | ||
| fn switch_to_plain(&mut self, current: &[u8]) -> Result<(), Error> { | ||
| let StreamingState::Encoding(mut encoding) = | ||
| std::mem::replace(&mut self.state, StreamingState::Closed) | ||
| else { | ||
| return Err(Error::Io { | ||
| context: "switch streaming writer to plain", | ||
| source: std::io::Error::from(std::io::ErrorKind::InvalidInput), | ||
| }); | ||
| }; | ||
| let (fallback, mut plain) = | ||
| decode_streaming_prefix(&self.path, &mut encoding, current, self.expected_len)?; | ||
| drop(encoding); | ||
| if let Err(source) = std::fs::remove_file(&self.path) { | ||
| let _ = std::fs::remove_file(&fallback); | ||
| return Err(Error::Io { | ||
| context: "remove streaming decmpfs temp", | ||
| source, | ||
| }); | ||
| } | ||
| if let Err(source) = std::fs::rename(&fallback, &self.path) { | ||
| let _ = std::fs::remove_file(&fallback); | ||
| return Err(Error::Io { | ||
| context: "adopt streaming plain fallback", | ||
| source, | ||
| }); | ||
| } | ||
| use std::io::Seek; | ||
| plain | ||
| .seek(std::io::SeekFrom::End(0)) | ||
| .map_err(|source| Error::Io { | ||
| context: "seek streaming plain fallback", | ||
| source, | ||
| })?; | ||
| self.state = StreamingState::Plain(plain); | ||
| Ok(()) | ||
| } | ||
| pub(crate) fn write_all(&mut self, mut input: &[u8]) -> Result<(), Error> { | ||
| let next_written = self | ||
| .written | ||
| .checked_add(input.len()) | ||
| .filter(|&len| len <= self.expected_len) | ||
| .ok_or_else(|| Error::Io { | ||
| context: "stream exceeds expected length", | ||
| source: std::io::Error::from(std::io::ErrorKind::InvalidData), | ||
| })?; | ||
| while !input.is_empty() { | ||
| match &mut self.state { | ||
| StreamingState::Plain(file) => { | ||
| use std::io::Write; | ||
| file.write_all(input).map_err(|source| Error::Io { | ||
| context: "write streaming plain temp", | ||
| source, | ||
| })?; | ||
| input = &[]; | ||
| } | ||
| StreamingState::Encoding(encoding) => { | ||
| let take = (BLOCK - encoding.partial.len()).min(input.len()); | ||
| encoding.partial.extend_from_slice(&input[..take]); | ||
| input = &input[take..]; | ||
| if encoding.partial.len() == BLOCK { | ||
| let block = std::mem::replace(&mut encoding.partial, Vec::with_capacity(BLOCK)); | ||
| if !encoding.write_block(&block, self.expected_len)? { | ||
| self.switch_to_plain(&block)?; | ||
| } | ||
| } | ||
| } | ||
| StreamingState::Closed => { | ||
| return Err(Error::Io { | ||
| context: "write closed streaming writer", | ||
| source: std::io::Error::from(std::io::ErrorKind::BrokenPipe), | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| self.written = next_written; | ||
| Ok(()) | ||
| } | ||
| pub(crate) fn finish(&mut self) -> Result<bool, Error> { | ||
| if self.written != self.expected_len { | ||
| return Err(Error::Io { | ||
| context: "finish incomplete streaming writer", | ||
| source: std::io::Error::from(std::io::ErrorKind::UnexpectedEof), | ||
| }); | ||
| } | ||
| let partial = match &mut self.state { | ||
| StreamingState::Encoding(encoding) if !encoding.partial.is_empty() => Some( | ||
| std::mem::replace(&mut encoding.partial, Vec::with_capacity(BLOCK)), | ||
| ), | ||
| _ => None, | ||
| }; | ||
| if let Some(block) = partial { | ||
| let won = match &mut self.state { | ||
| StreamingState::Encoding(encoding) => encoding.write_block(&block, self.expected_len)?, | ||
| _ => false, | ||
| }; | ||
| if !won { | ||
| self.switch_to_plain(&block)?; | ||
| } | ||
| } | ||
| let compressed = match std::mem::replace(&mut self.state, StreamingState::Closed) { | ||
| StreamingState::Plain(file) => { | ||
| file.sync_all().map_err(|source| Error::Io { | ||
| context: "sync streaming plain temp", | ||
| source, | ||
| })?; | ||
| false | ||
| } | ||
| StreamingState::Encoding(mut encoding) => { | ||
| use std::io::{Seek, Write}; | ||
| let mut table = Vec::with_capacity(encoding.offsets.len() * std::mem::size_of::<u32>()); | ||
| for offset in &encoding.offsets { | ||
| table.extend_from_slice(&offset.to_le_bytes()); | ||
| } | ||
| encoding | ||
| .fork | ||
| .seek(std::io::SeekFrom::Start(0)) | ||
| .and_then(|_| encoding.fork.write_all(&table)) | ||
| .and_then(|_| encoding.fork.flush()) | ||
| .map_err(|source| Error::Io { | ||
| context: "finish streaming resource fork", | ||
| source, | ||
| })?; | ||
| encoding | ||
| .fork | ||
| .get_ref() | ||
| .sync_all() | ||
| .map_err(|source| Error::Io { | ||
| context: "sync finished streaming resource fork", | ||
| source, | ||
| })?; | ||
| let cpath = cstring(&self.path)?; | ||
| setxattr( | ||
| &cpath, | ||
| c"com.apple.decmpfs", | ||
| &decmpfs_header(Codec::Lzfse, self.expected_len), | ||
| )?; | ||
| if unsafe { libc::fchflags(encoding.file.as_raw_fd(), UF_COMPRESSED) } != 0 { | ||
| return Err(io("fchflags streaming temp")); | ||
| } | ||
| encoding.file.sync_all().map_err(|source| Error::Io { | ||
| context: "sync streaming decmpfs temp", | ||
| source, | ||
| })?; | ||
| if streaming_kernel_matches(&self.path, &encoding, self.expected_len)? { | ||
| true | ||
| } else { | ||
| let (fallback, plain) = | ||
| decode_streaming_prefix(&self.path, &mut encoding, &[], self.expected_len)?; | ||
| drop(encoding); | ||
| std::fs::remove_file(&self.path).map_err(|source| Error::Io { | ||
| context: "remove failed streaming decmpfs oracle", | ||
| source, | ||
| })?; | ||
| if let Err(source) = std::fs::rename(&fallback, &self.path) { | ||
| let _ = std::fs::remove_file(&fallback); | ||
| return Err(Error::Io { | ||
| context: "publish streaming oracle fallback", | ||
| source, | ||
| }); | ||
| } | ||
| plain.sync_all().map_err(|source| Error::Io { | ||
| context: "sync streaming oracle fallback", | ||
| source, | ||
| })?; | ||
| false | ||
| } | ||
| } | ||
| StreamingState::Closed => { | ||
| return Err(Error::Io { | ||
| context: "finish closed streaming writer", | ||
| source: std::io::Error::from(std::io::ErrorKind::BrokenPipe), | ||
| }); | ||
| } | ||
| }; | ||
| self.complete = true; | ||
| Ok(compressed) | ||
| } | ||
| } | ||
| impl Drop for StreamingWriter { | ||
| fn drop(&mut self) { | ||
| if !self.complete { | ||
| self.state = StreamingState::Closed; | ||
| let _ = std::fs::remove_file(&self.path); | ||
| } | ||
| } | ||
| } | ||
| fn decmpfs_header(codec: Codec, raw_len: usize) -> [u8; 16] { | ||
@@ -1151,433 +664,3 @@ let mut header = [0u8; 16]; | ||
| #[cfg_attr(coverage_nightly, coverage(off))] | ||
| mod tests { | ||
| use super::*; | ||
| // The kernel-roundtrip oracle. decmpfs is undocumented — the only proof the | ||
| // format is right is that a normal read() returns identical bytes after apply. | ||
| #[test] | ||
| fn kernel_roundtrips_decmpfs() { | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-oracle-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("f.bin"); | ||
| // > 1 block (64 KiB) of compressible data, so the offset table + LZVN blocks | ||
| // are both exercised. | ||
| let mut raw = Vec::new(); | ||
| let pat = b"the quick brown fox decmpfs lzvn resource-fork oracle line "; | ||
| while raw.len() < 2_000_000 { | ||
| raw.extend_from_slice(pat); | ||
| } | ||
| std::fs::write(&path, &raw).unwrap(); | ||
| assert!( | ||
| matches!(detect(&path).unwrap(), Support::Supported), | ||
| "temp dir is local APFS/HFS+" | ||
| ); | ||
| apply_inplace(&path, &raw).unwrap(); | ||
| assert!(is_already_compressed(&path).unwrap(), "UF_COMPRESSED set"); | ||
| assert_eq!( | ||
| compressed_on_disk(&path).unwrap(), | ||
| Some(true), | ||
| "reports compressed" | ||
| ); | ||
| // THE ORACLE: the kernel decompresses our resource fork on read(). | ||
| assert_eq!( | ||
| std::fs::read(&path).unwrap(), | ||
| raw, | ||
| "kernel read-back must equal the original bytes" | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn incremental_writer_streams_lzfse_blocks_into_a_kernel_readable_file() { | ||
| let dir = | ||
| std::env::temp_dir().join(format!("decmpfs-incremental-oracle-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("model.bin"); | ||
| let raw = b"incremental lzfse resource fork ".repeat((2 << 20) / 34 + 1); | ||
| let mut writer = StreamingWriter::new(&path, raw.len()).unwrap(); | ||
| for chunk in raw.chunks(17_003) { | ||
| writer.write_all(chunk).unwrap(); | ||
| } | ||
| assert!(writer.finish().unwrap(), "compressible stream must win"); | ||
| assert!(is_already_compressed(&path).unwrap()); | ||
| assert_eq!(std::fs::read(&path).unwrap(), raw); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn incremental_writer_reconstructs_plain_bytes_when_compression_loses() { | ||
| let dir = std::env::temp_dir().join(format!( | ||
| "decmpfs-incremental-fallback-{}", | ||
| std::process::id() | ||
| )); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("random.bin"); | ||
| let mut raw = Vec::with_capacity(2 << 20); | ||
| let mut x: u64 = 0x9e37_79b9_7f4a_7c15; | ||
| while raw.len() < (2 << 20) { | ||
| x ^= x << 13; | ||
| x ^= x >> 7; | ||
| x ^= x << 17; | ||
| raw.extend_from_slice(&x.to_le_bytes()); | ||
| } | ||
| let mut writer = StreamingWriter::new(&path, raw.len()).unwrap(); | ||
| for chunk in raw.chunks(17_003) { | ||
| writer.write_all(chunk).unwrap(); | ||
| } | ||
| assert!( | ||
| !writer.finish().unwrap(), | ||
| "incompressible stream stays plain" | ||
| ); | ||
| assert!(!is_already_compressed(&path).unwrap()); | ||
| assert_eq!(std::fs::read(&path).unwrap(), raw); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| // Opt-in perf probe (ignored in CI — timing is machine-specific). Reports the | ||
| // decmpfs write time for a ~40 MiB addon; run serial vs parallel with | ||
| // cargo test -p decmpfs write_time -- --ignored --nocapture | ||
| // DECMPFS_SERIAL=1 cargo test -p decmpfs write_time -- --ignored --nocapture | ||
| #[test] | ||
| #[ignore] | ||
| fn write_time_probe() { | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-time-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("addon.node"); | ||
| let mut raw: Vec<u8> = Vec::with_capacity(40 << 20); | ||
| let mut x: u64 = 0x9e37_79b9_7f4a_7c15; | ||
| while raw.len() < (40 << 20) { | ||
| x ^= x << 13; | ||
| x ^= x >> 7; | ||
| x ^= x << 17; | ||
| raw.extend_from_slice(&x.to_le_bytes()); | ||
| raw.extend_from_slice(b"native addon .node text segment padding "); | ||
| } | ||
| if !matches!(detect(&dir), Ok(Support::Supported)) { | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| return; | ||
| } | ||
| let cores = std::thread::available_parallelism() | ||
| .map(|n| n.get()) | ||
| .unwrap_or(1); | ||
| let serial = std::env::var_os("DECMPFS_SERIAL").is_some(); | ||
| let start = std::time::Instant::now(); | ||
| apply_bytes(&path, &raw, None).unwrap(); | ||
| let ms = start.elapsed().as_secs_f64() * 1e3; | ||
| eprintln!( | ||
| "decmpfs write {}MiB — {} ({} cores): {:.1} ms", | ||
| raw.len() >> 20, | ||
| if serial { "serial" } else { "parallel" }, | ||
| cores, | ||
| ms, | ||
| ); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn detect_and_flags_error_on_a_missing_path() { | ||
| let p = std::path::Path::new("/no/such/decmpfs/path/x.bin"); | ||
| assert!(detect(p).is_err(), "statfs of a missing path errors"); | ||
| assert!( | ||
| is_already_compressed(p).is_err(), | ||
| "lstat of a missing path errors" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn apply_inplace_errors_when_the_file_cannot_be_read() { | ||
| // A 0-perm file: apply_inplace's initial read fails before any apply. Root | ||
| // bypasses mode bits, so skip there. | ||
| if unsafe { libc::geteuid() } == 0 { | ||
| return; | ||
| } | ||
| use std::os::unix::fs::PermissionsExt; | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-noread-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("f.bin"); | ||
| let content = b"\x7fELF unreadable"; | ||
| std::fs::write(&path, content).unwrap(); | ||
| std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap(); | ||
| // apply_inplace no longer reads the file (the caller passes the snapshot it | ||
| // already holds); the fail-soft guard is now the W_OK access check, which | ||
| // rejects a file we cannot write before the temp+rename would replace it. | ||
| let out = apply_inplace(&path, content); | ||
| std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).ok(); | ||
| assert!(matches!( | ||
| out, | ||
| Err(Error::Io { | ||
| context: "access", | ||
| .. | ||
| }) | ||
| )); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn setxattr_errors_on_a_missing_path() { | ||
| let out = setxattr(c"/no/such/decmpfs/path", c"com.apple.decmpfs", b"x"); | ||
| assert!(matches!( | ||
| out, | ||
| Err(Error::Io { | ||
| context: "setxattr", | ||
| .. | ||
| }) | ||
| )); | ||
| } | ||
| #[test] | ||
| fn compress_block_returns_none_for_empty_input() { | ||
| // libcompression encodes zero bytes to nothing → the n == 0 guard returns None. | ||
| let scratch_len = unsafe { compression_encode_scratch_buffer_size(COMPRESSION_LZVN) }; | ||
| let mut scratch = vec![0u8; scratch_len]; | ||
| assert!(compress_block(b"", &mut scratch).is_none()); | ||
| } | ||
| #[test] | ||
| fn build_resource_fork_zero_length_is_no_gain() { | ||
| assert!( | ||
| build_resource_fork(&[]).unwrap().is_none(), | ||
| "a resource fork cannot make an empty file smaller" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn streaming_threshold_keeps_vite_native_addons_on_the_fast_path() { | ||
| // The largest Darwin ARM64 addon in the 2026-07-16 Vite-family sample was | ||
| // SWC at 36.563 MiB. The complete observed set must stay comfortably below | ||
| // the in-memory cutoff, while the first byte beyond it streams. | ||
| assert!(!should_stream_resource_fork(37 << 20, STREAMING_THRESHOLD)); | ||
| assert!(!should_stream_resource_fork( | ||
| STREAMING_THRESHOLD, | ||
| STREAMING_THRESHOLD | ||
| )); | ||
| assert!(should_stream_resource_fork( | ||
| STREAMING_THRESHOLD + 1, | ||
| STREAMING_THRESHOLD | ||
| )); | ||
| } | ||
| #[test] | ||
| fn kernel_roundtrips_forced_streaming_lzfse() { | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-streaming-oracle-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("f.bin"); | ||
| let raw = b"streamed lzfse decmpfs resource fork oracle ".repeat((2 << 20) / 46 + 1); | ||
| std::fs::write(&path, &raw).unwrap(); | ||
| if matches!(detect(&path).unwrap(), Support::Supported) { | ||
| apply_bytes_with_streaming_threshold(&path, &raw, None, 0).unwrap(); | ||
| assert!(is_already_compressed(&path).unwrap(), "UF_COMPRESSED set"); | ||
| assert_eq!( | ||
| std::fs::read(&path).unwrap(), | ||
| raw, | ||
| "kernel read-back must decode the streamed type-12 resource fork" | ||
| ); | ||
| let cpath = cstring(&path).unwrap(); | ||
| let mut header = [0u8; 16]; | ||
| let len = unsafe { | ||
| libc::getxattr( | ||
| cpath.as_ptr(), | ||
| c"com.apple.decmpfs".as_ptr(), | ||
| header.as_mut_ptr().cast(), | ||
| header.len(), | ||
| 0, | ||
| XATTR_NOFOLLOW | 0x0020, // XATTR_SHOWCOMPRESSION | ||
| ) | ||
| }; | ||
| assert_eq!(len, header.len() as isize); | ||
| assert_eq!(u32::from_le_bytes(header[4..8].try_into().unwrap()), 12); | ||
| } | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn in_memory_path_falls_back_to_lzfse_when_lzvn_has_no_gain() { | ||
| // Skewed symbol frequencies give LZFSE's entropy coder something to exploit | ||
| // without manufacturing the repeated strings that LZVN specializes in. | ||
| let mut raw = Vec::with_capacity(1 << 20); | ||
| let mut x: u64 = 0x9e37_79b9_7f4a_7c15; | ||
| while raw.len() < raw.capacity() { | ||
| x ^= x << 13; | ||
| x ^= x >> 7; | ||
| x ^= x << 17; | ||
| raw.push(if x.is_multiple_of(4) { | ||
| 0 | ||
| } else { | ||
| (x >> 32) as u8 | ||
| }); | ||
| } | ||
| assert!( | ||
| build_resource_fork_with_codec(&raw, Codec::Lzvn) | ||
| .unwrap() | ||
| .is_none(), | ||
| "fixture must reach the fallback" | ||
| ); | ||
| let candidate = build_in_memory_resource_fork(&raw) | ||
| .unwrap() | ||
| .expect("LZFSE should exploit the skewed symbols"); | ||
| assert_eq!(candidate.codec, Codec::Lzfse); | ||
| assert!(candidate.bytes.len() < raw.len()); | ||
| } | ||
| #[test] | ||
| fn build_resource_fork_last_offset_equals_length() { | ||
| // Invariant across sizes that actually encode: the final table offset equals | ||
| // the total blob length. (Tiny/incompressible inputs return None — the codec | ||
| // declines — which is a separate, correct path.) | ||
| for size in [512usize, BLOCK, BLOCK + 1, BLOCK * 3 + 7] { | ||
| let raw = vec![0x41u8; size]; | ||
| let Some(rf) = build_resource_fork(&raw).unwrap() else { | ||
| continue; | ||
| }; | ||
| let num_blocks = size.div_ceil(BLOCK); | ||
| let last_idx = num_blocks * 4; // offset[num_blocks] is the last entry | ||
| let last = u32::from_le_bytes(rf[last_idx..last_idx + 4].try_into().unwrap()) as usize; | ||
| assert_eq!(last, rf.len(), "size {size}: last offset != buffer length"); | ||
| } | ||
| } | ||
| #[test] | ||
| fn cstring_rejects_an_interior_nul() { | ||
| use std::os::unix::ffi::OsStrExt; | ||
| let p = std::path::Path::new(std::ffi::OsStr::from_bytes(b"a\0b")); | ||
| assert!(cstring(p).is_err()); | ||
| } | ||
| #[test] | ||
| fn detect_rejects_a_non_apfs_filesystem() { | ||
| // /dev is devfs (local, but not apfs/hfs) → Unsupported(Filesystem). | ||
| assert!(matches!( | ||
| detect(std::path::Path::new("/dev")), | ||
| Ok(Support::Unsupported(UnsupportedReason::Filesystem)) | ||
| )); | ||
| } | ||
| #[test] | ||
| fn classify_fs_covers_every_branch() { | ||
| // Non-local (e.g. a network mount) — no real mount needed. | ||
| assert!(matches!( | ||
| classify_fs(false, b"nfs"), | ||
| Support::Unsupported(UnsupportedReason::NetworkOrOverlay) | ||
| )); | ||
| assert!(matches!(classify_fs(true, b"apfs"), Support::Supported)); | ||
| assert!(matches!(classify_fs(true, b"hfs"), Support::Supported)); | ||
| assert!(matches!( | ||
| classify_fs(true, b"ext4"), | ||
| Support::Unsupported(UnsupportedReason::Filesystem) | ||
| )); | ||
| } | ||
| #[test] | ||
| fn resource_fork_plan_accepts_raw_files_beyond_the_old_limit() { | ||
| // The raw byte count is stored as u64. Only resource-fork offsets are u32, | ||
| // so a >3.9 GB input is valid whenever its encoded fork fits in u32. | ||
| let raw_len = 4_100_000_000usize; | ||
| let num_blocks = raw_len.div_ceil(BLOCK); | ||
| assert!(matches!( | ||
| plan_resource_fork(raw_len, num_blocks, 3_000_000_000).unwrap(), | ||
| ResourceForkPlan::Compressed { .. } | ||
| )); | ||
| } | ||
| #[test] | ||
| fn resource_fork_plan_accepts_raw_files_beyond_four_gib_when_the_fork_fits() { | ||
| let raw_len = 5_000_000_000usize; | ||
| let num_blocks = raw_len.div_ceil(BLOCK); | ||
| assert!(matches!( | ||
| plan_resource_fork(raw_len, num_blocks, 3_000_000_000).unwrap(), | ||
| ResourceForkPlan::Compressed { .. } | ||
| )); | ||
| } | ||
| #[test] | ||
| fn resource_fork_plan_rejects_a_compressed_fork_past_u32() { | ||
| let raw_len = 5_000_000_000usize; | ||
| let num_blocks = raw_len.div_ceil(BLOCK); | ||
| match plan_resource_fork(raw_len, num_blocks, 4_400_000_000).unwrap_err() { | ||
| Error::Io { source, .. } => assert_eq!(source.raw_os_error(), Some(libc::EFBIG)), | ||
| other => panic!("expected EFBIG Io, got {other:?}"), | ||
| } | ||
| } | ||
| #[test] | ||
| fn gemini_nano_lzvn_resource_fork_is_no_gain() { | ||
| // Chrome 150's v3Nano weights.bin measured with this exact 64 KiB LZVN | ||
| // encoder: the encoded blocks expand enough to cross the u32 fork ceiling. | ||
| assert_eq!( | ||
| plan_resource_fork(4_269_932_544, 65_154, 4_364_775_458).unwrap(), | ||
| ResourceForkPlan::Plain | ||
| ); | ||
| } | ||
| #[test] | ||
| fn gemini_nano_lzfse_resource_fork_fits_and_wins() { | ||
| // The streamed type-12 run encoded the same 65,154 blocks to this payload; | ||
| // with its 260,620-byte offset table the fork is safely below u32::MAX. | ||
| assert_eq!( | ||
| plan_resource_fork(4_269_932_544, 65_154, 3_598_249_560).unwrap(), | ||
| ResourceForkPlan::Compressed { | ||
| table_len: 260_620, | ||
| total_len: 3_598_510_180, | ||
| } | ||
| ); | ||
| } | ||
| // Incompressible data → LZVN would expand the resource fork, so keep an | ||
| // ordinary data fork. The bytes and compression-state signal must agree. | ||
| #[test] | ||
| fn kernel_roundtrips_incompressible_blocks() { | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-raw-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("f.bin"); | ||
| let mut raw = Vec::new(); | ||
| let mut x: u32 = 0x9e37_79b9; | ||
| while raw.len() < 200_000 { | ||
| x ^= x << 13; | ||
| x ^= x >> 17; | ||
| x ^= x << 5; | ||
| raw.extend_from_slice(&x.to_le_bytes()); | ||
| } | ||
| std::fs::write(&path, &raw).unwrap(); | ||
| if matches!(detect(&path).unwrap(), Support::Supported) { | ||
| assert!(matches!( | ||
| crate::compress_file(&path).unwrap(), | ||
| crate::Outcome::NoGain { .. } | ||
| )); | ||
| assert_eq!( | ||
| std::fs::read(&path).unwrap(), | ||
| raw, | ||
| "plain fallback reads back identically" | ||
| ); | ||
| assert!( | ||
| !is_already_compressed(&path).unwrap(), | ||
| "no-gain input must not carry UF_COMPRESSED" | ||
| ); | ||
| } | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| #[test] | ||
| fn apply_bytes_preserves_ownership_of_an_overwritten_file() { | ||
| // Non-root exercises the chown path over an existing file — owner is our own | ||
| // uid, so preservation is a no-op we assert stays stable + non-corrupting. | ||
| // The root path (a file owned by a different uid) is verified in CI. | ||
| let dir = std::env::temp_dir().join(format!("decmpfs-own-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).unwrap(); | ||
| let path = dir.join("f"); | ||
| std::fs::write(&path, vec![0u8; 4096]).unwrap(); | ||
| if !matches!(detect(&path), Ok(Support::Supported)) { | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| return; | ||
| } | ||
| use std::os::unix::fs::MetadataExt; | ||
| let before_uid = std::fs::metadata(&path).unwrap().uid(); | ||
| let content = vec![0xABu8; 8192]; | ||
| apply_bytes(&path, &content, None).unwrap(); | ||
| let meta = std::fs::metadata(&path).unwrap(); | ||
| assert_eq!(meta.uid(), before_uid, "owner preserved across the rewrite"); | ||
| assert_eq!(std::fs::read(&path).unwrap(), content, "content intact"); | ||
| std::fs::remove_dir_all(&dir).ok(); | ||
| } | ||
| } | ||
| #[path = "macos/tests.rs"] | ||
| mod tests; |
Sorry, the diff of this file is not supported yet