use crate::block::{BlockDevice, MemoryBackend};
use crate::fs::Filesystem;
use super::write::{DeviceKind, EntryMeta};
use super::{FormatOpts, Xfs};
fn fresh(size: u64) -> (MemoryBackend, Xfs) {
let mut dev = MemoryBackend::new(size);
let opts = FormatOpts::default();
let mut xfs = super::format(&mut dev, &opts).unwrap();
xfs.begin_writes([0u8; 16]);
(dev, xfs)
}
#[test]
fn device_nodes_round_trip_major_minor() {
let (mut dev, mut xfs) = fresh(64 * 1024 * 1024);
let root = xfs.superblock().rootino;
xfs.add_device(
&mut dev,
root,
"ttyS0",
DeviceKind::Char,
4,
64,
EntryMeta::default(),
)
.unwrap();
xfs.add_device(
&mut dev,
root,
"sda1",
DeviceKind::Block,
8,
1,
EntryMeta::default(),
)
.unwrap();
xfs.add_device(
&mut dev,
root,
"big",
DeviceKind::Char,
4095,
0x3_fffe,
EntryMeta::default(),
)
.unwrap();
xfs.flush_writes(&mut dev).unwrap();
let mut xfs = Xfs::open(&mut dev).unwrap();
for (path, kind, major, minor) in [
("/ttyS0", crate::fs::EntryKind::Char, 4u32, 64u32),
("/sda1", crate::fs::EntryKind::Block, 8, 1),
("/big", crate::fs::EntryKind::Char, 4095, 0x3_fffe),
] {
let a = xfs.getattr(&mut dev, std::path::Path::new(path)).unwrap();
assert_eq!(a.kind, kind, "{path}");
assert_eq!(
crate::fs::devnum::decode_devnum(a.rdev),
(major, minor),
"{path}"
);
}
}
#[test]
fn device_node_on_disk_word_matches_sysv_encoding() {
let (mut dev, mut xfs) = fresh(64 * 1024 * 1024);
let root = xfs.superblock().rootino;
let ino = xfs
.add_device(
&mut dev,
root,
"null",
DeviceKind::Char,
1,
3,
EntryMeta::default(),
)
.unwrap();
xfs.flush_writes(&mut dev).unwrap();
let off = xfs.ino_byte_offset(ino).unwrap();
let mut buf = vec![0u8; xfs.inode_size() as usize];
dev.read_at(off, &mut buf).unwrap();
assert_eq!(buf[5], 0);
let raw = u32::from_be_bytes(buf[176..180].try_into().unwrap());
assert_eq!(raw, 3 | (1 << 18));
assert_eq!(&buf[180..184], &[0u8; 4]);
}
#[test]
fn fifo_and_socket_report_zero_rdev() {
let (mut dev, mut xfs) = fresh(64 * 1024 * 1024);
let root = xfs.superblock().rootino;
xfs.add_device(
&mut dev,
root,
"pipe",
DeviceKind::Fifo,
0,
0,
EntryMeta::default(),
)
.unwrap();
xfs.add_device(
&mut dev,
root,
"sock",
DeviceKind::Socket,
0,
0,
EntryMeta::default(),
)
.unwrap();
xfs.flush_writes(&mut dev).unwrap();
let mut xfs = Xfs::open(&mut dev).unwrap();
for (path, kind) in [
("/pipe", crate::fs::EntryKind::Fifo),
("/sock", crate::fs::EntryKind::Socket),
] {
let a = xfs.getattr(&mut dev, std::path::Path::new(path)).unwrap();
assert_eq!(a.kind, kind, "{path}");
assert_eq!(a.rdev, 0, "{path}");
}
}
#[test]
fn xattr_flag_constants_match_xfs_da_format() {
use super::xattr::{XFS_ATTR_INCOMPLETE, XFS_ATTR_LOCAL, XFS_ATTR_ROOT, XFS_ATTR_SECURE};
assert_eq!(XFS_ATTR_LOCAL, 0x01);
assert_eq!(XFS_ATTR_ROOT, 0x02);
assert_eq!(XFS_ATTR_SECURE, 0x04);
assert_eq!(XFS_ATTR_INCOMPLETE, 0x80);
assert_eq!(super::xattr_leaf::XFS_ATTR_SECURE, XFS_ATTR_SECURE);
}
#[test]
fn shortform_xattr_namespaces_round_trip_on_disk() {
let (mut dev, mut xfs) = fresh(64 * 1024 * 1024);
let root = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(b"body".to_vec());
let ino = xfs
.add_file(&mut dev, root, "f", EntryMeta::default(), 4, &mut src)
.unwrap();
xfs.add_xattr(&mut dev, ino, "user.u", b"1").unwrap();
xfs.add_xattr(&mut dev, ino, "trusted.t", b"2").unwrap();
xfs.add_xattr(&mut dev, ino, "security.s", b"3").unwrap();
xfs.flush_writes(&mut dev).unwrap();
let off = xfs.ino_byte_offset(ino).unwrap();
let mut buf = vec![0u8; xfs.inode_size() as usize];
dev.read_at(off, &mut buf).unwrap();
let forkoff = buf[82] as usize;
assert_ne!(forkoff, 0);
assert_eq!(buf[83], 1, "aformat should be LOCAL (shortform)");
let sf = &buf[176 + forkoff * 8..];
let count = sf[2] as usize;
assert_eq!(count, 3);
let mut pos = 4usize;
let mut seen = Vec::new();
for _ in 0..count {
let namelen = sf[pos] as usize;
let valuelen = sf[pos + 1] as usize;
let flags = sf[pos + 2];
let name = std::str::from_utf8(&sf[pos + 3..pos + 3 + namelen])
.unwrap()
.to_string();
seen.push((name, flags));
pos += 3 + namelen + valuelen;
}
seen.sort();
assert_eq!(
seen,
vec![
("s".to_string(), 0x04u8),
("t".to_string(), 0x02u8),
("u".to_string(), 0x00u8),
]
);
let xfs = Xfs::open(&mut dev).unwrap();
let attrs = xfs.read_xattrs(&mut dev, ino).unwrap();
assert_eq!(attrs.get("user.u"), Some(&b"1".to_vec()));
assert_eq!(attrs.get("trusted.t"), Some(&b"2".to_vec()));
assert_eq!(attrs.get("security.s"), Some(&b"3".to_vec()));
}
#[test]
fn leaf_xattr_namespaces_round_trip_on_disk() {
let (mut dev, mut xfs) = fresh(64 * 1024 * 1024);
let root = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(b"body".to_vec());
let ino = xfs
.add_file(&mut dev, root, "f", EntryMeta::default(), 4, &mut src)
.unwrap();
let big = vec![b'x'; 200];
xfs.add_xattr(&mut dev, ino, "user.u", &big).unwrap();
xfs.add_xattr(&mut dev, ino, "trusted.t", &big).unwrap();
xfs.add_xattr(&mut dev, ino, "security.s", &big).unwrap();
xfs.flush_writes(&mut dev).unwrap();
let off = xfs.ino_byte_offset(ino).unwrap();
let mut buf = vec![0u8; xfs.inode_size() as usize];
dev.read_at(off, &mut buf).unwrap();
assert_eq!(buf[83], 2, "aformat should be EXTENTS (leaf spill)");
let xfs = Xfs::open(&mut dev).unwrap();
let attrs = xfs.read_xattrs(&mut dev, ino).unwrap();
assert_eq!(attrs.get("user.u"), Some(&big));
assert_eq!(attrs.get("trusted.t"), Some(&big));
assert_eq!(attrs.get("security.s"), Some(&big));
}
#[test]
fn file_xattr_survives_a_read_write_handle_writeback() {
use crate::fs::OpenFlags;
use std::io::{Seek as _, SeekFrom, Write as _};
let (mut dev, mut xfs) = fresh(64 * 1024 * 1024);
let root = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(vec![b'a'; 4096]);
let ino = xfs
.add_file(&mut dev, root, "f", EntryMeta::default(), 4096, &mut src)
.unwrap();
xfs.add_xattr(&mut dev, ino, "user.keep", b"me").unwrap();
xfs.add_xattr(&mut dev, ino, "security.selinux", b"ctx")
.unwrap();
xfs.flush_writes(&mut dev).unwrap();
let mut xfs = Xfs::open(&mut dev).unwrap();
{
let mut h = Filesystem::open_file_rw(
&mut xfs,
&mut dev,
std::path::Path::new("/f"),
OpenFlags::default(),
None,
)
.unwrap();
h.seek(SeekFrom::Start(0)).unwrap();
h.write_all(b"ZZZZ").unwrap();
h.sync().unwrap();
}
let xfs = Xfs::open(&mut dev).unwrap();
let attrs = xfs.read_xattrs(&mut dev, ino).unwrap();
assert_eq!(attrs.get("user.keep"), Some(&b"me".to_vec()));
assert_eq!(attrs.get("security.selinux"), Some(&b"ctx".to_vec()));
}
#[test]
fn leaf_form_file_xattr_survives_writeback_with_aformat_intact() {
use crate::fs::OpenFlags;
use std::io::{Seek as _, SeekFrom, Write as _};
let (mut dev, mut xfs) = fresh(64 * 1024 * 1024);
let root = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(vec![b'a'; 4096]);
let ino = xfs
.add_file(&mut dev, root, "f", EntryMeta::default(), 4096, &mut src)
.unwrap();
let big = vec![b'v'; 300];
xfs.add_xattr(&mut dev, ino, "user.big", &big).unwrap();
xfs.flush_writes(&mut dev).unwrap();
{
let off = xfs.ino_byte_offset(ino).unwrap();
let mut buf = vec![0u8; xfs.inode_size() as usize];
dev.read_at(off, &mut buf).unwrap();
assert_eq!(buf[83], 2, "aformat");
assert_eq!(u16::from_be_bytes(buf[80..82].try_into().unwrap()), 1);
}
let mut xfs = Xfs::open(&mut dev).unwrap();
{
let mut h = Filesystem::open_file_rw(
&mut xfs,
&mut dev,
std::path::Path::new("/f"),
OpenFlags::default(),
None,
)
.unwrap();
h.seek(SeekFrom::Start(10)).unwrap();
h.write_all(b"QQQQ").unwrap();
h.sync().unwrap();
}
let xfs = Xfs::open(&mut dev).unwrap();
let off = xfs.ino_byte_offset(ino).unwrap();
let mut buf = vec![0u8; xfs.inode_size() as usize];
dev.read_at(off, &mut buf).unwrap();
assert_eq!(buf[83], 2, "aformat clobbered to LOCAL");
assert_eq!(
u16::from_be_bytes(buf[80..82].try_into().unwrap()),
1,
"di_anextents zeroed"
);
let attrs = xfs.read_xattrs(&mut dev, ino).unwrap();
assert_eq!(attrs.get("user.big"), Some(&big));
}
#[test]
fn directory_xattr_survives_entry_add_and_remove() {
let (mut dev, mut xfs) = fresh(64 * 1024 * 1024);
let root = xfs.superblock().rootino;
let dir = xfs
.add_dir(&mut dev, root, "d", EntryMeta::default())
.unwrap();
xfs.add_xattr(&mut dev, dir, "user.dirattr", b"yes")
.unwrap();
xfs.add_xattr(&mut dev, dir, "trusted.t", b"1").unwrap();
xfs.flush_writes(&mut dev).unwrap();
for i in 0..8 {
let mut src = std::io::Cursor::new(vec![b'x'; 8]);
xfs.add_file(
&mut dev,
dir,
&format!("child{i}"),
EntryMeta::default(),
8,
&mut src,
)
.unwrap();
}
xfs.flush_writes(&mut dev).unwrap();
{
let attrs = xfs.read_xattrs(&mut dev, dir).unwrap();
assert_eq!(attrs.get("user.dirattr"), Some(&b"yes".to_vec()));
assert_eq!(attrs.get("trusted.t"), Some(&b"1".to_vec()));
}
xfs.remove(&mut dev, dir, "child3").unwrap();
xfs.flush_writes(&mut dev).unwrap();
let xfs = Xfs::open(&mut dev).unwrap();
let attrs = xfs.read_xattrs(&mut dev, dir).unwrap();
assert_eq!(attrs.get("user.dirattr"), Some(&b"yes".to_vec()));
assert_eq!(attrs.get("trusted.t"), Some(&b"1".to_vec()));
}
#[test]
fn bmdr_root_pointers_sit_after_the_full_key_array() {
use super::bmbt::{bmdr_ptrs_offset, decode_root};
for (size, want) in [(64usize, 28usize), (80, 36), (336, 164), (96, 44)] {
assert_eq!(bmdr_ptrs_offset(size), want, "dfork_size {size}");
}
let mut root = vec![0u8; 64];
root[0..2].copy_from_slice(&1u16.to_be_bytes()); root[2..4].copy_from_slice(&2u16.to_be_bytes()); root[4..12].copy_from_slice(&0u64.to_be_bytes());
root[12..20].copy_from_slice(&64u64.to_be_bytes());
root[20..28].copy_from_slice(&0xDEADu64.to_be_bytes()); root[48..56].copy_from_slice(&0xBEEFu64.to_be_bytes()); let pp = bmdr_ptrs_offset(64);
root[pp..pp + 8].copy_from_slice(&111u64.to_be_bytes());
root[pp + 8..pp + 16].copy_from_slice(&222u64.to_be_bytes());
let (level, numrecs, keys, ptrs) = decode_root(&root).unwrap();
assert_eq!((level, numrecs), (1, 2));
assert_eq!(keys, vec![0, 64]);
assert_eq!(ptrs, vec![111, 222]);
}
#[test]
fn bmdr_root_rejects_more_records_than_the_fork_can_hold() {
use super::bmbt::decode_root;
let mut root = vec![0u8; 64];
root[0..2].copy_from_slice(&1u16.to_be_bytes());
root[2..4].copy_from_slice(&4u16.to_be_bytes());
assert!(matches!(
decode_root(&root),
Err(crate::Error::InvalidImage(_))
));
}
#[test]
fn extent_encode_rejects_out_of_range_fields() {
use super::bmbt::{Extent, MAX_EXTENT_BLOCKS};
assert_eq!(MAX_EXTENT_BLOCKS, (1 << 21) - 1);
let ok = Extent {
offset: 0,
startblock: 1,
blockcount: MAX_EXTENT_BLOCKS,
unwritten: false,
};
assert_eq!(Extent::decode(&ok.encode().unwrap()).unwrap(), ok);
let too_many = Extent {
blockcount: MAX_EXTENT_BLOCKS + 1,
..ok
};
assert!(matches!(
too_many.encode(),
Err(crate::Error::InvalidArgument(_))
));
let wrapped = Extent {
blockcount: MAX_EXTENT_BLOCKS + 2,
..ok
};
assert!(wrapped.encode().is_err());
assert!(
Extent {
offset: 1 << 54,
..ok
}
.encode()
.is_err()
);
assert!(
Extent {
startblock: 1 << 52,
..ok
}
.encode()
.is_err()
);
assert!(
Extent {
blockcount: 0,
..ok
}
.encode()
.is_err()
);
}
fn nextents_of(xfs: &Xfs, dev: &mut MemoryBackend, ino: u64) -> u32 {
let off = xfs.ino_byte_offset(ino).unwrap();
let mut buf = vec![0u8; xfs.inode_size() as usize];
dev.read_at(off, &mut buf).unwrap();
u32::from_be_bytes(buf[76..80].try_into().unwrap())
}
fn fragment_free_space(dev: &mut MemoryBackend, xfs: &mut Xfs, chunk: u64) -> u64 {
let root = xfs.superblock().rootino;
let mut names = Vec::new();
for i in 0.. {
let name = format!("fill{i}");
let mut src = std::io::Cursor::new(vec![b'0' + (i as u8 % 10); chunk as usize]);
match xfs.add_file(
&mut *dev,
root,
&name,
EntryMeta::default(),
chunk,
&mut src,
) {
Ok(_) => names.push(name),
Err(_) => break,
}
}
assert!(names.len() > 8, "test image too small to fragment");
xfs.flush_writes(dev).unwrap();
for name in names.iter().step_by(2) {
xfs.remove(&mut *dev, root, name).unwrap();
}
xfs.flush_writes(dev).unwrap();
chunk
}
#[test]
fn file_larger_than_the_longest_free_run_spans_several_extents() {
let (mut dev, mut xfs) = fresh(16 * 1024 * 1024);
let chunk = 256 * 1024u64; let island = fragment_free_space(&mut dev, &mut xfs, chunk);
let want = island * 3;
let body: Vec<u8> = (0..want).map(|i| (i % 251) as u8).collect();
let root = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(body.clone());
let ino = xfs
.add_file(&mut dev, root, "big", EntryMeta::default(), want, &mut src)
.unwrap();
xfs.flush_writes(&mut dev).unwrap();
assert!(
nextents_of(&xfs, &mut dev, ino) > 1,
"expected the multi-extent path to be exercised"
);
let xfs = Xfs::open(&mut dev).unwrap();
let mut out = Vec::new();
{
let mut r = xfs.open_file_reader(&mut dev, "/big").unwrap();
std::io::Read::read_to_end(&mut r, &mut out).unwrap();
}
assert_eq!(out.len(), body.len());
assert!(out == body, "multi-extent file did not round-trip");
}
fn free_blocks(dev: &mut MemoryBackend) -> u64 {
let mut sb = [0u8; 512];
dev.read_at(0, &mut sb).unwrap();
u64::from_be_bytes(sb[144..152].try_into().unwrap())
}
#[test]
fn file_too_fragmented_for_the_inline_fork_reports_the_limit() {
let (mut dev, mut xfs) = fresh(16 * 1024 * 1024);
let bs = xfs.block_size() as u64;
let root = xfs.superblock().rootino;
let islands = super::rw::MAX_INLINE_EXTENTS + 5;
let small = 8 * bs;
let mut names = Vec::new();
for i in 0..islands * 2 {
let name = format!("s{i}");
let mut src = std::io::Cursor::new(vec![b'a'; small as usize]);
xfs.add_file(&mut dev, root, &name, EntryMeta::default(), small, &mut src)
.unwrap();
names.push(name);
}
xfs.flush_writes(&mut dev).unwrap();
let mut hog = free_blocks(&mut dev);
loop {
assert!(hog > 0, "could not hog the AG tail");
let bytes = hog * bs;
let mut src = std::io::Cursor::new(vec![b'h'; bytes as usize]);
if xfs
.add_file(&mut dev, root, "hog", EntryMeta::default(), bytes, &mut src)
.is_ok()
{
break;
}
hog = hog * 9 / 10;
}
xfs.flush_writes(&mut dev).unwrap();
for name in names.iter().step_by(2) {
xfs.remove(&mut dev, root, name).unwrap();
}
xfs.flush_writes(&mut dev).unwrap();
let want = 8 * bs * (islands as u64 - 1);
let mut src = std::io::Cursor::new(vec![b'z'; want as usize]);
let err = xfs
.add_file(
&mut dev,
root,
"toobig",
EntryMeta::default(),
want,
&mut src,
)
.unwrap_err();
match &err {
crate::Error::Unsupported(m) => assert!(
m.contains(&super::rw::MAX_INLINE_EXTENTS.to_string()),
"error should name the extent limit, got: {m}"
),
other => panic!("expected Unsupported naming the extent limit, got {other:?}"),
}
let mut src = std::io::Cursor::new(vec![b'z'; want as usize]);
assert!(matches!(
xfs.add_file(
&mut dev,
root,
"toobig2",
EntryMeta::default(),
want,
&mut src
),
Err(crate::Error::Unsupported(_))
));
}
fn patch_sb_incompat(dev: &mut MemoryBackend, add: u32) {
let mut sb = vec![0u8; 4096];
dev.read_at(0, &mut sb).unwrap();
let cur = u32::from_be_bytes(sb[216..220].try_into().unwrap());
sb[216..220].copy_from_slice(&(cur | add).to_be_bytes());
super::format::stamp_v5_superblock_crc(&mut sb);
dev.write_at(0, &sb).unwrap();
}
fn patch_inode_to_nrext64(xfs: &Xfs, dev: &mut MemoryBackend, ino: u64) {
let off = xfs.ino_byte_offset(ino).unwrap();
let mut buf = vec![0u8; xfs.inode_size() as usize];
dev.read_at(off, &mut buf).unwrap();
let nextents = u32::from_be_bytes(buf[76..80].try_into().unwrap());
let anextents = u16::from_be_bytes(buf[80..82].try_into().unwrap());
buf[24..32].copy_from_slice(&(nextents as u64).to_be_bytes());
buf[76..80].copy_from_slice(&(anextents as u32).to_be_bytes());
buf[80..82].copy_from_slice(&0u16.to_be_bytes());
super::inode::stamp_v3_inode_crc(&mut buf);
dev.write_at(off, &buf).unwrap();
}
#[test]
fn open_refuses_unknown_incompat_features() {
use super::superblock as sbf;
for (bit, label) in [
(sbf::XFS_SB_FEAT_INCOMPAT_NEEDSREPAIR, "needsrepair"),
(sbf::XFS_SB_FEAT_INCOMPAT_METADIR, "metadir"),
(1u32 << 20, "a bit from the future"),
] {
let (mut dev, _xfs) = fresh(16 * 1024 * 1024);
patch_sb_incompat(&mut dev, bit);
match Xfs::open(&mut dev) {
Err(crate::Error::Unsupported(m)) => {
assert!(m.contains("incompat"), "{label}: {m}");
}
Err(other) => panic!("{label}: expected Unsupported, got {other:?}"),
Ok(_) => panic!("{label}: open should have refused the volume"),
}
}
}
#[test]
fn open_accepts_the_incompat_features_we_understand() {
use super::superblock as sbf;
let known = sbf::XFS_SB_FEAT_INCOMPAT_FTYPE
| sbf::XFS_SB_FEAT_INCOMPAT_SPINODES
| sbf::XFS_SB_FEAT_INCOMPAT_META_UUID
| sbf::XFS_SB_FEAT_INCOMPAT_BIGTIME
| sbf::XFS_SB_FEAT_INCOMPAT_NREXT64
| sbf::XFS_SB_FEAT_INCOMPAT_EXCHRANGE
| sbf::XFS_SB_FEAT_INCOMPAT_PARENT;
let (mut dev, _xfs) = fresh(16 * 1024 * 1024);
patch_sb_incompat(&mut dev, known);
let xfs = Xfs::open(&mut dev).expect("known incompat bits must be accepted");
assert!(xfs.superblock().has_nrext64());
assert!(xfs.superblock().has_bigtime());
}
#[test]
fn bigtime_timestamp_codec_matches_the_kernel() {
use super::inode::{XFS_BIGTIME_EPOCH_OFFSET, XfsTimestamp};
let zero = XfsTimestamp::decode_bigtime(&0u64.to_be_bytes());
assert_eq!(zero.sec, -XFS_BIGTIME_EPOCH_OFFSET);
assert_eq!(zero.nsec, 0);
for ts in [
XfsTimestamp { sec: 0, nsec: 0 },
XfsTimestamp {
sec: 1_700_000_000,
nsec: 123_456_789,
},
XfsTimestamp {
sec: 4_000_000_000,
nsec: 1,
},
XfsTimestamp {
sec: -XFS_BIGTIME_EPOCH_OFFSET,
nsec: 999_999_999,
},
] {
let enc = ts.encode(true);
assert_eq!(XfsTimestamp::decode_bigtime(&enc), ts, "{ts:?}");
}
let legacy = XfsTimestamp { sec: -1, nsec: 5 };
assert_eq!(XfsTimestamp::decode(&legacy.encode(false)), legacy);
}
#[test]
fn bigtime_inode_timestamps_are_decoded() {
use super::inode::{XFS_DIFLAG2_BIGTIME, XfsTimestamp};
let (mut dev, mut xfs) = fresh(16 * 1024 * 1024);
let root = xfs.superblock().rootino;
let mut src = std::io::Cursor::new(b"hi".to_vec());
let ino = xfs
.add_file(&mut dev, root, "t", EntryMeta::default(), 2, &mut src)
.unwrap();
xfs.flush_writes(&mut dev).unwrap();
let want = XfsTimestamp {
sec: 4_000_000_000,
nsec: 500_000_000,
};
let off = xfs.ino_byte_offset(ino).unwrap();
let mut buf = vec![0u8; xfs.inode_size() as usize];
dev.read_at(off, &mut buf).unwrap();
let flags2 = u64::from_be_bytes(buf[120..128].try_into().unwrap()) | XFS_DIFLAG2_BIGTIME;
buf[120..128].copy_from_slice(&flags2.to_be_bytes());
for base in [32usize, 40, 48, 144] {
buf[base..base + 8].copy_from_slice(&want.encode(true));
}
super::inode::stamp_v3_inode_crc(&mut buf);
dev.write_at(off, &buf).unwrap();
patch_sb_incompat(&mut dev, super::superblock::XFS_SB_FEAT_INCOMPAT_BIGTIME);
let mut xfs = Xfs::open(&mut dev).unwrap();
let a = xfs
.getattr(&mut dev, std::path::Path::new("/t"))
.expect("getattr on a bigtime inode");
assert_eq!(a.mtime, 4_000_000_000);
assert_eq!(a.atime, 4_000_000_000);
let r = xfs.getattr(&mut dev, std::path::Path::new("/")).unwrap();
assert_eq!(r.mtime, 0);
}
#[test]
fn nrext64_inode_extent_counters_are_decoded() {
let (mut dev, mut xfs) = fresh(16 * 1024 * 1024);
let root = xfs.superblock().rootino;
let body = vec![b'q'; 9000];
let mut src = std::io::Cursor::new(body.clone());
let ino = xfs
.add_file(
&mut dev,
root,
"n",
EntryMeta::default(),
body.len() as u64,
&mut src,
)
.unwrap();
xfs.add_xattr(&mut dev, ino, "user.a", b"1").unwrap();
xfs.flush_writes(&mut dev).unwrap();
patch_inode_to_nrext64(&xfs, &mut dev, root);
patch_inode_to_nrext64(&xfs, &mut dev, ino);
patch_sb_incompat(&mut dev, super::superblock::XFS_SB_FEAT_INCOMPAT_NREXT64);
let xfs = Xfs::open(&mut dev).unwrap();
assert!(xfs.superblock().has_nrext64());
let mut out = Vec::new();
{
let mut r = xfs.open_file_reader(&mut dev, "/n").unwrap();
std::io::Read::read_to_end(&mut r, &mut out).unwrap();
}
assert_eq!(out, body, "nrext64 inode's extent list did not decode");
assert_eq!(xfs.read_xattrs(&mut dev, ino).unwrap().len(), 1);
}
#[test]
fn nrext64_inode_survives_a_writeback() {
use crate::fs::OpenFlags;
use std::io::{Seek as _, SeekFrom, Write as _};
let (mut dev, mut xfs) = fresh(16 * 1024 * 1024);
let root = xfs.superblock().rootino;
let body = vec![b'q'; 9000];
let mut src = std::io::Cursor::new(body.clone());
let ino = xfs
.add_file(
&mut dev,
root,
"n",
EntryMeta::default(),
body.len() as u64,
&mut src,
)
.unwrap();
xfs.flush_writes(&mut dev).unwrap();
patch_inode_to_nrext64(&xfs, &mut dev, root);
patch_inode_to_nrext64(&xfs, &mut dev, ino);
patch_sb_incompat(&mut dev, super::superblock::XFS_SB_FEAT_INCOMPAT_NREXT64);
let mut xfs = Xfs::open(&mut dev).unwrap();
{
let mut h = Filesystem::open_file_rw(
&mut xfs,
&mut dev,
std::path::Path::new("/n"),
OpenFlags::default(),
None,
)
.unwrap();
h.seek(SeekFrom::Start(0)).unwrap();
h.write_all(b"ABCD").unwrap();
h.sync().unwrap();
}
let xfs = Xfs::open(&mut dev).unwrap();
let off = xfs.ino_byte_offset(ino).unwrap();
let mut buf = vec![0u8; xfs.inode_size() as usize];
dev.read_at(off, &mut buf).unwrap();
assert_eq!(
u64::from_be_bytes(buf[24..32].try_into().unwrap()),
1,
"di_big_nextents should hold the data-fork extent count"
);
assert_eq!(
u32::from_be_bytes(buf[76..80].try_into().unwrap()),
0,
"di_big_anextents should be 0 for an inode with no attr fork"
);
let mut out = Vec::new();
{
let mut r = xfs.open_file_reader(&mut dev, "/n").unwrap();
std::io::Read::read_to_end(&mut r, &mut out).unwrap();
}
let mut want = body.clone();
want[0..4].copy_from_slice(b"ABCD");
assert_eq!(out, want);
}