use btrfs_fs::Filesystem;
use std::{
fs::{self, File},
path::{Path, PathBuf},
sync::OnceLock,
};
const ZEROS_LEN: usize = 1024 * 1024;
const RANDOM_LEN: usize = 1024 * 1024;
const PATTERN_LEN: usize = 16 * 1024 * 1024;
const INLINE_PAYLOAD: &[u8] =
b"compress me. compress me. compress me. compress me. \
compress me. compress me. compress me. compress me. \
compress me. compress me. compress me. compress me. \
compress me. compress me. compress me. compress me. \
compress me.";
fn random_bytes() -> Vec<u8> {
let mut out = vec![0u8; RANDOM_LEN];
let mut state: u32 = 0x1234_5678;
for byte in &mut out {
state = state.wrapping_mul(1_103_515_245).wrapping_add(12345);
*byte = (state >> 16) as u8;
}
out
}
fn pattern_bytes() -> Vec<u8> {
let mut out = Vec::with_capacity(PATTERN_LEN);
for i in 0..16u8 {
out.extend(std::iter::repeat_n(i, 1024 * 1024));
}
out
}
fn build_compression_fixture(base: &Path, algo: &str) -> PathBuf {
let src = base.join("src");
fs::create_dir(&src).unwrap();
fs::write(src.join("inline.txt"), INLINE_PAYLOAD).unwrap();
fs::write(src.join("zeros_1m.bin"), vec![0u8; ZEROS_LEN]).unwrap();
fs::write(src.join("random_1m.bin"), random_bytes()).unwrap();
fs::write(src.join("pattern_16m.bin"), pattern_bytes()).unwrap();
let img = base.join("test.img");
File::create(&img)
.unwrap()
.set_len(256 * 1024 * 1024)
.unwrap();
btrfs_test_utils::run(
"mkfs.btrfs",
&[
"-f",
"--rootdir",
src.to_str().unwrap(),
"--compress",
algo,
img.to_str().unwrap(),
],
);
img
}
fn fixture_for(algo: &str) -> &'static Path {
static ZLIB: OnceLock<(tempfile::TempDir, PathBuf)> = OnceLock::new();
static ZSTD: OnceLock<(tempfile::TempDir, PathBuf)> = OnceLock::new();
static LZO: OnceLock<(tempfile::TempDir, PathBuf)> = OnceLock::new();
let lock = match algo {
"zlib" => &ZLIB,
"zstd" => &ZSTD,
"lzo" => &LZO,
_ => panic!("unknown compression algorithm: {algo}"),
};
let (_td, path) = lock.get_or_init(|| {
let td = tempfile::tempdir().unwrap();
let img = build_compression_fixture(td.path(), algo);
(td, img)
});
path
}
fn open(algo: &str) -> Filesystem<File> {
Filesystem::open(File::open(fixture_for(algo)).unwrap()).unwrap()
}
macro_rules! compression_suite {
($module:ident, $algo:literal) => {
mod $module {
use super::*;
#[tokio::test]
async fn read_inline_full() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"inline.txt").await.unwrap().unwrap();
let data = fs
.read(ino, 0, INLINE_PAYLOAD.len() as u32 + 16)
.await
.unwrap();
assert_eq!(data, INLINE_PAYLOAD);
}
#[tokio::test]
async fn read_inline_partial_offset() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"inline.txt").await.unwrap().unwrap();
let data = fs.read(ino, 10, 20).await.unwrap();
assert_eq!(data, &INLINE_PAYLOAD[10..30]);
}
#[tokio::test]
async fn read_zeros_full() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"zeros_1m.bin").await.unwrap().unwrap();
#[allow(clippy::cast_possible_truncation)]
let data = fs.read(ino, 0, ZEROS_LEN as u32).await.unwrap();
assert_eq!(data.len(), ZEROS_LEN);
assert!(data.iter().all(|&b| b == 0));
}
#[tokio::test]
async fn read_zeros_partial_offset() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"zeros_1m.bin").await.unwrap().unwrap();
let data = fs.read(ino, 500_000, 100_000).await.unwrap();
assert_eq!(data.len(), 100_000);
assert!(data.iter().all(|&b| b == 0));
}
#[tokio::test]
async fn read_random_full() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"random_1m.bin").await.unwrap().unwrap();
#[allow(clippy::cast_possible_truncation)]
let data = fs.read(ino, 0, RANDOM_LEN as u32).await.unwrap();
assert_eq!(data, random_bytes());
}
#[tokio::test]
async fn read_random_partial_offset() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"random_1m.bin").await.unwrap().unwrap();
let data = fs.read(ino, 200_000, 50_000).await.unwrap();
assert_eq!(data, &random_bytes()[200_000..250_000]);
}
#[tokio::test]
async fn read_pattern_full() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"pattern_16m.bin").await.unwrap().unwrap();
#[allow(clippy::cast_possible_truncation)]
let data = fs.read(ino, 0, PATTERN_LEN as u32).await.unwrap();
assert_eq!(data, pattern_bytes());
}
#[tokio::test]
async fn read_pattern_straddle_boundary() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"pattern_16m.bin").await.unwrap().unwrap();
let off = 1_048_576 - 100_000;
let data = fs.read(ino, off as u64, 200_000).await.unwrap();
assert_eq!(data.len(), 200_000);
let pattern = pattern_bytes();
assert_eq!(&data[..], &pattern[off..off + 200_000]);
}
#[tokio::test]
async fn read_pattern_last_byte() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"pattern_16m.bin").await.unwrap().unwrap();
let data =
fs.read(ino, (PATTERN_LEN - 1) as u64, 16).await.unwrap();
assert_eq!(data, &[0x0fu8]);
}
#[tokio::test]
async fn read_at_eof_returns_empty() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"zeros_1m.bin").await.unwrap().unwrap();
#[allow(clippy::cast_possible_truncation)]
let data = fs.read(ino, ZEROS_LEN as u64, 1024).await.unwrap();
assert!(data.is_empty());
}
#[tokio::test]
async fn read_past_eof_returns_empty() {
let fs = open($algo);
let root = fs.root();
let (ino, _) =
fs.lookup(root, b"zeros_1m.bin").await.unwrap().unwrap();
let data = fs
.read(ino, (ZEROS_LEN as u64) + 4096, 1024)
.await
.unwrap();
assert!(data.is_empty());
}
}
};
}
compression_suite!(zlib, "zlib");
compression_suite!(zstd, "zstd");
compression_suite!(lzo, "lzo");