pub mod checkpoint;
pub mod constants;
pub mod dir;
pub mod file;
pub mod format;
pub mod inode;
pub mod nat;
pub mod superblock;
pub mod write;
use std::io::Read;
use crate::Result;
use crate::block::BlockDevice;
pub use file::FileReader;
pub use format::{FormatOpts, Geometry, plan_geometry};
pub use superblock::{F2FS_MAGIC, SB_OFFSET_BACKUP, SB_OFFSET_PRIMARY, Superblock};
pub use write::Writer;
use checkpoint::Checkpoint;
use constants::{F2FS_BLKSIZE, S_IFDIR, S_IFMT, S_IFREG};
use dir::{RawDentry, decode_dentry_block, decode_inline_dentries};
use inode::{F2fsInode, decode_inode_block};
pub fn probe(dev: &mut dyn BlockDevice) -> Result<bool> {
if dev.total_size() < SB_OFFSET_BACKUP + 4 {
return Ok(false);
}
let mut head = [0u8; 4];
dev.read_at(SB_OFFSET_PRIMARY, &mut head)?;
if u32::from_le_bytes(head) == F2FS_MAGIC {
return Ok(true);
}
dev.read_at(SB_OFFSET_BACKUP, &mut head)?;
Ok(u32::from_le_bytes(head) == F2FS_MAGIC)
}
pub struct F2fs {
sb: Superblock,
cp: Checkpoint,
pub(crate) writer: Option<Writer>,
}
impl F2fs {
pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
let sb = superblock::load(dev)?;
let cp = Checkpoint::load(dev, &sb)?;
Ok(Self {
sb,
cp,
writer: None,
})
}
pub fn format(dev: &mut dyn BlockDevice, opts: &FormatOpts) -> Result<Self> {
let bs = F2FS_BLKSIZE as u64;
let total_blocks = dev.total_size() / bs;
let geom = format::plan_geometry(total_blocks, opts)?;
dev.zero_range(0, geom.main_blkaddr as u64 * bs)?;
format::write_superblocks(dev, &geom, opts)?;
format::wipe_metadata_region(dev, &geom)?;
let sb = superblock::load(dev)?;
let writer = Writer::new(geom, sb.clone());
let mut me = Self {
sb,
cp: Checkpoint {
version: 0,
user_block_count: total_blocks,
valid_block_count: 0,
rsvd_segment_count: 0,
overprov_segment_count: 0,
flags: 0,
cp_pack_start_sum: 1,
cp_pack_total_block_count: 2,
cp_payload: 0,
head_blkaddr: geom.cp_blkaddr,
nat_ver_bitmap_bytesize: 64,
sit_ver_bitmap_bytesize: 64,
cur_nat_pack: 0,
cur_sit_pack: 0,
nat_journal: Vec::new(),
cur_node_segno: [0, 1, 2],
cur_node_blkoff: [0, 0, 0],
cur_data_segno: [3, 4, 5],
cur_data_blkoff: [0, 0, 0],
free_segment_count: 0,
valid_node_count: 0,
valid_inode_count: 0,
next_free_nid: 0,
},
writer: Some(writer),
};
me.flush(dev)?;
me.cp = Checkpoint::load(dev, &me.sb)?;
Ok(me)
}
pub fn create_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
src: crate::fs::FileSource,
meta: crate::fs::FileMeta,
) -> Result<u32> {
self.writer_mut()?.add_file(dev, path, src, meta)
}
pub fn create_dir(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
meta: crate::fs::FileMeta,
) -> Result<u32> {
self.writer_mut()?.add_dir(dev, path, meta)
}
pub fn create_symlink(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
target: &std::path::Path,
meta: crate::fs::FileMeta,
) -> Result<u32> {
self.writer_mut()?.add_symlink(dev, path, target, meta)
}
pub fn create_hardlink(
&mut self,
dev: &mut dyn BlockDevice,
src: &std::path::Path,
dst: &std::path::Path,
) -> Result<u32> {
self.writer_mut()?.add_hardlink(dev, src, dst)
}
pub fn create_device(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
kind: crate::fs::DeviceKind,
major: u32,
minor: u32,
meta: crate::fs::FileMeta,
) -> Result<u32> {
self.writer_mut()?
.add_device(dev, path, kind, major, minor, meta)
}
pub fn remove(&mut self, dev: &mut dyn BlockDevice, path: &std::path::Path) -> Result<()> {
self.writer_mut()?.remove(dev, path)
}
pub fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
let Some(w) = self.writer.as_mut() else {
return Ok(());
};
w.flush(dev)?;
if let Ok(cp) = Checkpoint::load(dev, &self.sb) {
self.cp = cp;
}
Ok(())
}
fn writer_mut(&mut self) -> Result<&mut Writer> {
self.writer.as_mut().ok_or_else(|| {
crate::Error::Unsupported(
"f2fs: handle opened read-only; use F2fs::format for write access".into(),
)
})
}
pub fn total_bytes(&self) -> u64 {
self.sb.block_count << self.sb.log_blocksize
}
pub fn block_size(&self) -> u32 {
1u32 << self.sb.log_blocksize
}
pub fn volume_name(&self) -> &str {
&self.sb.volume_name
}
pub fn superblock(&self) -> &Superblock {
&self.sb
}
pub fn checkpoint(&self) -> &Checkpoint {
&self.cp
}
pub fn resolve_path(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<u32> {
let mut ino = self.sb.root_ino;
if path == "/" || path.is_empty() {
return Ok(ino);
}
for comp in path.trim_matches('/').split('/') {
if comp.is_empty() {
continue;
}
let (inode_block, inode) = self.read_inode(dev, ino)?;
if inode.mode & S_IFMT != S_IFDIR {
return Err(crate::Error::InvalidArgument(format!(
"f2fs: '{path}': '{comp}' parent is not a directory"
)));
}
let entries = self.list_inode(dev, &inode, &inode_block)?;
let Some(found) = entries.iter().find(|e| e.name == comp.as_bytes()) else {
return Err(crate::Error::InvalidArgument(format!(
"f2fs: '{path}': '{comp}' not found"
)));
};
ino = found.ino;
}
Ok(ino)
}
pub fn read_inode(&self, dev: &mut dyn BlockDevice, nid: u32) -> Result<(Vec<u8>, F2fsInode)> {
let addr = nat::lookup_node(dev, &self.sb, &self.cp, nid)?;
let mut buf = vec![0u8; F2FS_BLKSIZE];
dev.read_at(addr.block as u64 * self.sb.block_size() as u64, &mut buf)?;
let inode = decode_inode_block(&buf)?;
Ok((buf, inode))
}
pub fn list_inode(
&self,
dev: &mut dyn BlockDevice,
inode: &F2fsInode,
inode_block: &[u8],
) -> Result<Vec<RawDentry>> {
if inode.is_inline_dentry() {
return decode_inline_dentries(inode, inode_block);
}
let total_blocks = inode.size.div_ceil(F2FS_BLKSIZE as u64);
let mut out = Vec::new();
let mut buf = vec![0u8; F2FS_BLKSIZE];
for i in 0..total_blocks {
let phys = file::logical_to_physical(dev, &self.sb, &self.cp, inode, i)?;
if phys == 0 || phys == constants::NEW_ADDR {
continue;
}
dev.read_at(phys as u64 * self.sb.block_size() as u64, &mut buf)?;
let entries = decode_dentry_block(&buf)?;
out.extend(entries);
}
Ok(out)
}
pub fn list_path(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<Vec<crate::fs::DirEntry>> {
let ino = self.resolve_path(dev, path)?;
let (inode_block, inode) = self.read_inode(dev, ino)?;
if inode.mode & S_IFMT != S_IFDIR {
return Err(crate::Error::InvalidArgument(format!(
"f2fs: '{path}' is not a directory"
)));
}
let raws = self.list_inode(dev, &inode, &inode_block)?;
Ok(raws
.into_iter()
.filter(|d| d.name.as_slice() != b"." && d.name.as_slice() != b"..")
.map(|d| d.into_dir_entry())
.collect())
}
pub fn open_file_reader<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &str,
) -> Result<Box<dyn Read + 'a>> {
let ino = self.resolve_path(dev, path)?;
let (inode_block, inode) = self.read_inode(dev, ino)?;
if inode.mode & S_IFMT != S_IFREG {
return Err(crate::Error::InvalidArgument(format!(
"f2fs: '{path}' is not a regular file"
)));
}
let _ = inode;
let reader = FileReader::new(dev, self.sb.clone(), self.cp.clone(), inode_block)?;
Ok(Box::new(reader))
}
}
impl crate::fs::Filesystem for F2fs {
type FormatOpts = FormatOpts;
fn format(dev: &mut dyn BlockDevice, opts: &Self::FormatOpts) -> Result<Self> {
Self::format(dev, opts)
}
fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
Self::open(dev)
}
fn create_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
src: crate::fs::FileSource,
meta: crate::fs::FileMeta,
) -> Result<()> {
self.create_file(dev, path, src, meta).map(|_| ())
}
fn create_dir(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
meta: crate::fs::FileMeta,
) -> Result<()> {
self.create_dir(dev, path, meta).map(|_| ())
}
fn create_symlink(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
target: &std::path::Path,
meta: crate::fs::FileMeta,
) -> Result<()> {
self.create_symlink(dev, path, target, meta).map(|_| ())
}
fn create_device(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
kind: crate::fs::DeviceKind,
major: u32,
minor: u32,
meta: crate::fs::FileMeta,
) -> Result<()> {
self.create_device(dev, path, kind, major, minor, meta)
.map(|_| ())
}
fn remove(&mut self, dev: &mut dyn BlockDevice, path: &std::path::Path) -> Result<()> {
self.remove(dev, path)
}
fn list(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
) -> Result<Vec<crate::fs::DirEntry>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("f2fs: non-UTF-8 path".into()))?;
self.list_path(dev, s)
}
fn read_file<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &std::path::Path,
) -> Result<Box<dyn Read + 'a>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("f2fs: non-UTF-8 path".into()))?;
self.open_file_reader(dev, s)
}
fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
Self::flush(self, dev)
}
}
#[cfg(test)]
mod tests {
use super::checkpoint::{
Checkpoint, NatJournalEntry, encode_cp_head, encode_nat_journal_block,
};
use super::constants::{
ADDRS_PER_BLOCK, ADDRS_PER_INODE, F2FS_BLKSIZE, F2FS_FT_DIR, F2FS_FT_REG_FILE,
F2FS_INLINE_DATA, F2FS_INLINE_DENTRY, NR_DENTRY_IN_BLOCK, S_IFDIR, S_IFREG,
};
use super::dir::{RawDentry, encode_dentry_block, encode_inline_dentries_payload};
use super::inode::{F2fsInode, encode_direct_node, encode_indirect_node, encode_inode_block};
use super::nat::encode_nat_entry;
use super::superblock::F2FS_MAGIC;
use super::*;
use crate::block::MemoryBackend;
use crate::fs::EntryKind;
struct ImageLayout {
blocks: u64,
sb_blocks: u32, cp_segs: u32, sit_segs: u32, nat_segs: u32, ssa_segs: u32, blocks_per_seg: u32, }
impl ImageLayout {
fn default_tiny() -> Self {
Self {
blocks: 256,
sb_blocks: 2,
cp_segs: 2, sit_segs: 1,
nat_segs: 2,
ssa_segs: 1,
blocks_per_seg: 4,
}
}
}
fn build_image(
layout: &ImageLayout,
root_entries: &[(String, F2fsInode, Option<Vec<u8>>)],
) -> MemoryBackend {
let bs = F2FS_BLKSIZE as u64;
let dev_size = layout.blocks * bs;
let mut dev = MemoryBackend::new(dev_size);
let cp_blkaddr = layout.sb_blocks; let sit_blkaddr = cp_blkaddr + layout.cp_segs * layout.blocks_per_seg;
let nat_blkaddr = sit_blkaddr + layout.sit_segs * layout.blocks_per_seg;
let ssa_blkaddr = nat_blkaddr + layout.nat_segs * layout.blocks_per_seg;
let main_blkaddr = ssa_blkaddr + layout.ssa_segs * layout.blocks_per_seg;
let main_segs = (layout.blocks as u32 - main_blkaddr) / layout.blocks_per_seg;
let mut sb_buf = vec![0u8; 0x400];
sb_buf[0..4].copy_from_slice(&F2FS_MAGIC.to_le_bytes());
sb_buf[4..6].copy_from_slice(&1u16.to_le_bytes());
sb_buf[6..8].copy_from_slice(&15u16.to_le_bytes());
sb_buf[8..12].copy_from_slice(&9u32.to_le_bytes()); sb_buf[0x10..0x14].copy_from_slice(&12u32.to_le_bytes()); let log_bps = layout.blocks_per_seg.trailing_zeros();
sb_buf[0x14..0x18].copy_from_slice(&log_bps.to_le_bytes());
sb_buf[0x18..0x1C].copy_from_slice(&1u32.to_le_bytes()); sb_buf[0x1C..0x20].copy_from_slice(&1u32.to_le_bytes()); sb_buf[0x24..0x2C].copy_from_slice(&layout.blocks.to_le_bytes());
sb_buf[0x2C..0x30].copy_from_slice(&main_segs.to_le_bytes()); sb_buf[0x30..0x34].copy_from_slice(
&(main_segs + layout.cp_segs + layout.sit_segs + layout.nat_segs + layout.ssa_segs)
.to_le_bytes(),
);
sb_buf[0x34..0x38].copy_from_slice(&layout.cp_segs.to_le_bytes());
sb_buf[0x38..0x3C].copy_from_slice(&layout.sit_segs.to_le_bytes());
sb_buf[0x3C..0x40].copy_from_slice(&layout.nat_segs.to_le_bytes());
sb_buf[0x40..0x44].copy_from_slice(&layout.ssa_segs.to_le_bytes());
sb_buf[0x44..0x48].copy_from_slice(&main_segs.to_le_bytes());
sb_buf[0x48..0x4C].copy_from_slice(&0u32.to_le_bytes()); sb_buf[0x4C..0x50].copy_from_slice(&cp_blkaddr.to_le_bytes());
sb_buf[0x50..0x54].copy_from_slice(&sit_blkaddr.to_le_bytes());
sb_buf[0x54..0x58].copy_from_slice(&nat_blkaddr.to_le_bytes());
sb_buf[0x58..0x5C].copy_from_slice(&ssa_blkaddr.to_le_bytes());
sb_buf[0x5C..0x60].copy_from_slice(&main_blkaddr.to_le_bytes());
sb_buf[0x60..0x64].copy_from_slice(&3u32.to_le_bytes()); sb_buf[0x64..0x68].copy_from_slice(&1u32.to_le_bytes()); sb_buf[0x68..0x6C].copy_from_slice(&2u32.to_le_bytes()); let name = "test".encode_utf16().collect::<Vec<u16>>();
for (i, c) in name.iter().enumerate() {
sb_buf[0x7C + i * 2..0x7C + i * 2 + 2].copy_from_slice(&c.to_le_bytes());
}
dev.write_at(SB_OFFSET_PRIMARY, &sb_buf).unwrap();
dev.write_at(SB_OFFSET_BACKUP, &sb_buf).unwrap();
let mut next_main_blk = main_blkaddr;
let mut alloc = || {
let b = next_main_blk;
next_main_blk += 1;
assert!(next_main_blk < layout.blocks as u32);
b
};
let mut nat_entries: Vec<NatJournalEntry> = Vec::new();
let root_ino = 3u32;
let mut inode_blocks: Vec<(u32, F2fsInode, Option<Vec<u8>>, u32)> = Vec::new();
let mut child_entries_for_dir: Vec<RawDentry> = Vec::new();
for (child_nid, (name, ino, body)) in (100u32..).zip(root_entries.iter()) {
let phys_inode = alloc();
let ft = if ino.mode & S_IFMT == S_IFDIR {
F2FS_FT_DIR
} else {
F2FS_FT_REG_FILE
};
child_entries_for_dir.push(RawDentry {
hash: 0,
ino: child_nid,
file_type: ft,
name: name.as_bytes().to_vec(),
});
inode_blocks.push((child_nid, ino.clone(), body.clone(), phys_inode));
}
let root_phys_inode = alloc();
let mut root_inode = F2fsInode {
mode: S_IFDIR | 0o755,
size: 0,
uid: 0,
gid: 0,
links: 2,
atime: 0,
ctime: 0,
mtime: 0,
blocks: 1,
generation: 0,
flags: 0,
inline_flags: 0,
i_addr: [0; super::constants::ADDRS_PER_INODE],
i_nid: [0; super::constants::NIDS_PER_INODE],
};
let want_inline_root = child_entries_for_dir.iter().all(|e| e.name.len() <= 8)
&& child_entries_for_dir.len() <= 4;
if want_inline_root {
root_inode.inline_flags |= F2FS_INLINE_DENTRY;
root_inode.size = F2FS_BLKSIZE as u64; } else {
let root_dir_blk = alloc();
let buf = encode_dentry_block(&child_entries_for_dir);
dev.write_at(root_dir_blk as u64 * bs, &buf).unwrap();
root_inode.size = F2FS_BLKSIZE as u64;
root_inode.i_addr[0] = root_dir_blk;
}
for (nid, mut ino, body, phys_inode) in inode_blocks {
if let Some(bytes) = &body {
if !ino.is_inline_data() && !bytes.is_empty() {
let n_blocks = bytes.len().div_ceil(F2FS_BLKSIZE);
for i in 0..n_blocks {
let phys = alloc();
let start = i * F2FS_BLKSIZE;
let end = (start + F2FS_BLKSIZE).min(bytes.len());
let mut blk = vec![0u8; F2FS_BLKSIZE];
blk[..end - start].copy_from_slice(&bytes[start..end]);
dev.write_at(phys as u64 * bs, &blk).unwrap();
if i < super::constants::ADDRS_PER_INODE {
ino.i_addr[i] = phys;
}
}
}
}
let blk = encode_inode_block(&ino);
dev.write_at(phys_inode as u64 * bs, &blk).unwrap();
nat_entries.push(NatJournalEntry {
nid,
ino: nid,
block_addr: phys_inode,
version: 0,
});
}
if root_inode.inline_flags & F2FS_INLINE_DENTRY != 0 {
let mut blk = encode_inode_block(&root_inode);
let payload = encode_inline_dentries_payload(&child_entries_for_dir);
let off = super::inode::I_ADDR_OFFSET + 4;
let n = payload.len().min(blk.len() - off - 8); blk[off..off + n].copy_from_slice(&payload[..n]);
let crc = crate::fs::f2fs::constants::f2fs_crc32(
&blk[..super::constants::F2FS_BLK_CSUM_OFFSET],
);
blk[super::constants::F2FS_BLK_CSUM_OFFSET..super::constants::F2FS_BLK_CSUM_OFFSET + 4]
.copy_from_slice(&crc.to_le_bytes());
dev.write_at(root_phys_inode as u64 * bs, &blk).unwrap();
} else {
let blk = encode_inode_block(&root_inode);
dev.write_at(root_phys_inode as u64 * bs, &blk).unwrap();
}
nat_entries.push(NatJournalEntry {
nid: root_ino,
ino: root_ino,
block_addr: root_phys_inode,
version: 0,
});
let cp = Checkpoint {
version: 1,
user_block_count: layout.blocks,
valid_block_count: (next_main_blk - main_blkaddr) as u64,
rsvd_segment_count: 0,
overprov_segment_count: 0,
flags: 0,
cp_pack_start_sum: 1, cp_pack_total_block_count: 2,
cp_payload: 0,
head_blkaddr: cp_blkaddr,
nat_ver_bitmap_bytesize: 64,
sit_ver_bitmap_bytesize: 64,
cur_nat_pack: 0,
cur_sit_pack: 0,
nat_journal: Vec::new(),
cur_node_segno: [0, 1, 2],
cur_node_blkoff: [0, 0, 0],
cur_data_segno: [3, 4, 5],
cur_data_blkoff: [0, 0, 0],
free_segment_count: 0,
valid_node_count: 0,
valid_inode_count: 0,
next_free_nid: 0,
};
let cp_head = encode_cp_head(&cp);
dev.write_at(cp_blkaddr as u64 * bs, &cp_head).unwrap();
let cp_sum = encode_nat_journal_block(&nat_entries);
dev.write_at((cp_blkaddr as u64 + 1) * bs, &cp_sum).unwrap();
dev
}
#[test]
fn open_and_list_root_with_block_dentries() {
let layout = ImageLayout::default_tiny();
let mk_regular = |size: u64, payload: &[u8]| -> (F2fsInode, Option<Vec<u8>>) {
let mut ino = F2fsInode {
mode: S_IFREG | 0o644,
size,
uid: 0,
gid: 0,
links: 1,
atime: 0,
ctime: 0,
mtime: 0,
blocks: 1,
generation: 0,
flags: 0,
inline_flags: 0,
i_addr: [0; super::constants::ADDRS_PER_INODE],
i_nid: [0; super::constants::NIDS_PER_INODE],
};
if size as usize <= 3000 && !payload.is_empty() {
ino.inline_flags |= F2FS_INLINE_DATA;
}
(ino, Some(payload.to_vec()))
};
let (a_ino, a_body) = mk_regular(11, b"hello world");
let (b_ino, b_body) = mk_regular(0, b"");
let entries = vec![
("longish_filename_a.txt".to_string(), a_ino, a_body),
("b".to_string(), b_ino, b_body),
];
let mut dev = build_image(&layout, &entries);
let mut f = F2fs::open(&mut dev).unwrap();
assert_eq!(f.volume_name(), "test");
let list = f.list_path(&mut dev, "/").unwrap();
let names: Vec<_> = list.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"longish_filename_a.txt"));
assert!(names.contains(&"b"));
let a = list
.iter()
.find(|e| e.name == "longish_filename_a.txt")
.unwrap();
assert_eq!(a.kind, EntryKind::Regular);
}
#[test]
fn inline_data_round_trips() {
let layout = ImageLayout::default_tiny();
let payload = b"hello inline F2FS!";
let mut ino = F2fsInode {
mode: S_IFREG | 0o644,
size: payload.len() as u64,
uid: 0,
gid: 0,
links: 1,
atime: 0,
ctime: 0,
mtime: 0,
blocks: 0,
generation: 0,
flags: 0,
inline_flags: F2FS_INLINE_DATA,
i_addr: [0; super::constants::ADDRS_PER_INODE],
i_nid: [0; super::constants::NIDS_PER_INODE],
};
let bytes_as_words = payload;
for (i, b) in bytes_as_words.iter().enumerate() {
let slot = (i / 4) + 1;
let off = i % 4;
let mut bs4 = ino.i_addr[slot].to_le_bytes();
bs4[off] = *b;
ino.i_addr[slot] = u32::from_le_bytes(bs4);
}
let entries = vec![("hi.txt".to_string(), ino, Some(payload.to_vec()))];
let mut dev = build_image(&layout, &entries);
let mut f = F2fs::open(&mut dev).unwrap();
let mut r = f.open_file_reader(&mut dev, "/hi.txt").unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out, payload);
}
#[test]
fn streaming_read_walks_direct_pointers() {
let layout = ImageLayout::default_tiny();
let block_size = F2FS_BLKSIZE;
let payload: Vec<u8> = (0..(block_size * 3))
.map(|i| (i as u8).wrapping_mul(31))
.collect();
let ino = F2fsInode {
mode: S_IFREG | 0o644,
size: payload.len() as u64,
uid: 0,
gid: 0,
links: 1,
atime: 0,
ctime: 0,
mtime: 0,
blocks: 3,
generation: 0,
flags: 0,
inline_flags: 0,
i_addr: [0; super::constants::ADDRS_PER_INODE],
i_nid: [0; super::constants::NIDS_PER_INODE],
};
let entries = vec![("big.bin".to_string(), ino, Some(payload.clone()))];
let mut dev = build_image(&layout, &entries);
let mut f = F2fs::open(&mut dev).unwrap();
let mut r = f.open_file_reader(&mut dev, "/big.bin").unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out.len(), payload.len());
assert_eq!(out, payload);
}
#[test]
fn rejects_invalid_checkpoint_crc() {
let layout = ImageLayout::default_tiny();
let entries: Vec<(String, F2fsInode, Option<Vec<u8>>)> = vec![];
let mut dev = build_image(&layout, &entries);
let cp_blk = 2u64; let mut byte = [0u8; 1];
dev.read_at(cp_blk * F2FS_BLKSIZE as u64, &mut byte)
.unwrap();
byte[0] ^= 0xFF;
dev.write_at(cp_blk * F2FS_BLKSIZE as u64, &byte).unwrap();
dev.read_at(
(cp_blk + layout.blocks_per_seg as u64) * F2FS_BLKSIZE as u64,
&mut byte,
)
.unwrap();
byte[0] ^= 0xFF;
dev.write_at(
(cp_blk + layout.blocks_per_seg as u64) * F2FS_BLKSIZE as u64,
&byte,
)
.unwrap();
let err = F2fs::open(&mut dev).err().expect("should fail");
assert!(matches!(err, crate::Error::InvalidImage(_)));
}
#[test]
fn picks_higher_version_cp_when_both_valid() {
let layout = ImageLayout::default_tiny();
let entries: Vec<(String, F2fsInode, Option<Vec<u8>>)> = vec![];
let mut dev = build_image(&layout, &entries);
let cp_blk = 2u32;
let cp2 = Checkpoint {
version: 99,
user_block_count: 0,
valid_block_count: 0,
rsvd_segment_count: 0,
overprov_segment_count: 0,
flags: 0,
cp_pack_start_sum: 1,
cp_pack_total_block_count: 2,
cp_payload: 0,
head_blkaddr: cp_blk + layout.blocks_per_seg,
nat_ver_bitmap_bytesize: 0,
sit_ver_bitmap_bytesize: 0,
cur_nat_pack: 1,
cur_sit_pack: 1,
nat_journal: Vec::new(),
cur_node_segno: [0, 1, 2],
cur_node_blkoff: [0, 0, 0],
cur_data_segno: [3, 4, 5],
cur_data_blkoff: [0, 0, 0],
free_segment_count: 0,
valid_node_count: 0,
valid_inode_count: 0,
next_free_nid: 0,
};
let buf = encode_cp_head(&cp2);
let addr = (cp_blk + layout.blocks_per_seg) as u64 * F2FS_BLKSIZE as u64;
dev.write_at(addr, &buf).unwrap();
let sum = encode_nat_journal_block(&[]);
dev.write_at(addr + F2FS_BLKSIZE as u64, &sum).unwrap();
let f = F2fs::open(&mut dev).unwrap();
assert_eq!(f.checkpoint().version, 99);
assert_eq!(f.checkpoint().cur_nat_pack, 1);
}
#[test]
fn probe_detects_primary_copy() {
let mut dev = MemoryBackend::new(64 * 1024);
dev.write_at(SB_OFFSET_PRIMARY, &F2FS_MAGIC.to_le_bytes())
.unwrap();
assert!(probe(&mut dev).unwrap());
}
#[test]
fn probe_detects_backup_copy() {
let mut dev = MemoryBackend::new(64 * 1024);
dev.write_at(SB_OFFSET_BACKUP, &F2FS_MAGIC.to_le_bytes())
.unwrap();
assert!(probe(&mut dev).unwrap());
}
#[test]
fn open_reports_geometry() {
let layout = ImageLayout::default_tiny();
let entries: Vec<(String, F2fsInode, Option<Vec<u8>>)> = vec![];
let mut dev = build_image(&layout, &entries);
let f = F2fs::open(&mut dev).unwrap();
assert_eq!(f.block_size(), 4096);
assert_eq!(f.volume_name(), "test");
assert_eq!(f.total_bytes(), layout.blocks * 4096);
}
#[test]
fn nr_dentry_in_block_is_214() {
assert_eq!(NR_DENTRY_IN_BLOCK, 214);
}
#[test]
fn _exercise_encode_nat_entry() {
let mut page = vec![0u8; F2FS_BLKSIZE];
encode_nat_entry(&mut page, 0, 1, 3, 100);
assert_eq!(page[0], 1);
}
#[test]
fn inline_dentry_path_lists_short_names() {
let layout = ImageLayout::default_tiny();
let ino = F2fsInode {
mode: S_IFREG | 0o644,
size: 0,
uid: 0,
gid: 0,
links: 1,
atime: 0,
ctime: 0,
mtime: 0,
blocks: 0,
generation: 0,
flags: 0,
inline_flags: 0,
i_addr: [0; ADDRS_PER_INODE],
i_nid: [0; super::constants::NIDS_PER_INODE],
};
let entries = vec![
("a".to_string(), ino.clone(), None),
("bb".to_string(), ino.clone(), None),
];
let mut dev = build_image(&layout, &entries);
let mut f = F2fs::open(&mut dev).unwrap();
let list = f.list_path(&mut dev, "/").unwrap();
let names: Vec<_> = list.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"a"));
assert!(names.contains(&"bb"));
for e in &list {
assert_eq!(e.kind, EntryKind::Regular);
}
}
#[test]
fn open_file_reader_rejects_directory() {
let layout = ImageLayout::default_tiny();
let entries: Vec<(String, F2fsInode, Option<Vec<u8>>)> = vec![];
let mut dev = build_image(&layout, &entries);
let mut f = F2fs::open(&mut dev).unwrap();
let err = f.open_file_reader(&mut dev, "/").err().unwrap();
assert!(matches!(err, crate::Error::InvalidArgument(_)));
}
#[test]
fn list_path_missing_returns_error() {
let layout = ImageLayout::default_tiny();
let entries: Vec<(String, F2fsInode, Option<Vec<u8>>)> = vec![];
let mut dev = build_image(&layout, &entries);
let mut f = F2fs::open(&mut dev).unwrap();
let err = f.list_path(&mut dev, "/nope").err().unwrap();
assert!(matches!(err, crate::Error::InvalidArgument(_)));
}
#[test]
fn streaming_read_walks_direct_node_indirection() {
let bs = F2FS_BLKSIZE as u64;
let total_blocks: u64 = 2048;
let mut dev = MemoryBackend::new(total_blocks * bs);
let cp_blkaddr: u32 = 2;
let blocks_per_seg: u32 = 4;
let sit_blkaddr = cp_blkaddr + 2 * blocks_per_seg;
let nat_blkaddr = sit_blkaddr + blocks_per_seg;
let ssa_blkaddr = nat_blkaddr + 2 * blocks_per_seg;
let main_blkaddr = ssa_blkaddr + blocks_per_seg;
let mut sb_buf = vec![0u8; 0x400];
sb_buf[0..4].copy_from_slice(&F2FS_MAGIC.to_le_bytes());
sb_buf[4..6].copy_from_slice(&1u16.to_le_bytes());
sb_buf[8..12].copy_from_slice(&9u32.to_le_bytes());
sb_buf[0x10..0x14].copy_from_slice(&12u32.to_le_bytes());
let log_bps = blocks_per_seg.trailing_zeros();
sb_buf[0x14..0x18].copy_from_slice(&log_bps.to_le_bytes());
sb_buf[0x18..0x1C].copy_from_slice(&1u32.to_le_bytes());
sb_buf[0x1C..0x20].copy_from_slice(&1u32.to_le_bytes());
sb_buf[0x24..0x2C].copy_from_slice(&total_blocks.to_le_bytes());
sb_buf[0x30..0x34].copy_from_slice(&((total_blocks as u32) / blocks_per_seg).to_le_bytes());
sb_buf[0x34..0x38].copy_from_slice(&2u32.to_le_bytes());
sb_buf[0x38..0x3C].copy_from_slice(&1u32.to_le_bytes());
sb_buf[0x3C..0x40].copy_from_slice(&2u32.to_le_bytes());
sb_buf[0x40..0x44].copy_from_slice(&1u32.to_le_bytes());
sb_buf[0x44..0x48].copy_from_slice(
&((total_blocks as u32 - main_blkaddr) / blocks_per_seg).to_le_bytes(),
);
sb_buf[0x4C..0x50].copy_from_slice(&cp_blkaddr.to_le_bytes());
sb_buf[0x50..0x54].copy_from_slice(&sit_blkaddr.to_le_bytes());
sb_buf[0x54..0x58].copy_from_slice(&nat_blkaddr.to_le_bytes());
sb_buf[0x58..0x5C].copy_from_slice(&ssa_blkaddr.to_le_bytes());
sb_buf[0x5C..0x60].copy_from_slice(&main_blkaddr.to_le_bytes());
sb_buf[0x60..0x64].copy_from_slice(&3u32.to_le_bytes());
sb_buf[0x64..0x68].copy_from_slice(&1u32.to_le_bytes());
sb_buf[0x68..0x6C].copy_from_slice(&2u32.to_le_bytes());
dev.write_at(SB_OFFSET_PRIMARY, &sb_buf).unwrap();
dev.write_at(SB_OFFSET_BACKUP, &sb_buf).unwrap();
let mut next = main_blkaddr;
let mut alloc = || {
let b = next;
next += 1;
b
};
let n_blocks = ADDRS_PER_INODE + 3;
let payload: Vec<u8> = (0..(n_blocks * F2FS_BLKSIZE))
.map(|i| (i as u8).wrapping_mul(7))
.collect();
let mut data_blocks = Vec::with_capacity(n_blocks);
for _ in 0..n_blocks {
data_blocks.push(alloc());
}
for (i, &b) in data_blocks.iter().enumerate() {
let s = i * F2FS_BLKSIZE;
let e = ((i + 1) * F2FS_BLKSIZE).min(payload.len());
let mut blk = vec![0u8; F2FS_BLKSIZE];
blk[..e - s].copy_from_slice(&payload[s..e]);
dev.write_at(b as u64 * bs, &blk).unwrap();
}
let mut dnode_ptrs = vec![0u32; ADDRS_PER_BLOCK];
dnode_ptrs[0] = data_blocks[ADDRS_PER_INODE];
dnode_ptrs[1] = data_blocks[ADDRS_PER_INODE + 1];
dnode_ptrs[2] = data_blocks[ADDRS_PER_INODE + 2];
let dnode_blk = alloc();
dev.write_at(dnode_blk as u64 * bs, &encode_direct_node(&dnode_ptrs))
.unwrap();
let mut file_inode = F2fsInode {
mode: S_IFREG | 0o644,
size: (n_blocks * F2FS_BLKSIZE) as u64,
uid: 0,
gid: 0,
links: 1,
atime: 0,
ctime: 0,
mtime: 0,
blocks: n_blocks as u64,
generation: 0,
flags: 0,
inline_flags: 0,
i_addr: [0; ADDRS_PER_INODE],
i_nid: [0; super::constants::NIDS_PER_INODE],
};
file_inode.i_addr[..ADDRS_PER_INODE].copy_from_slice(&data_blocks[..ADDRS_PER_INODE]);
file_inode.i_nid[super::constants::NID_DIRECT_1] = 200;
let file_inode_blk = alloc();
dev.write_at(file_inode_blk as u64 * bs, &encode_inode_block(&file_inode))
.unwrap();
let root_inode = F2fsInode {
mode: S_IFDIR | 0o755,
size: F2FS_BLKSIZE as u64,
uid: 0,
gid: 0,
links: 2,
atime: 0,
ctime: 0,
mtime: 0,
blocks: 0,
generation: 0,
flags: 0,
inline_flags: F2FS_INLINE_DENTRY,
i_addr: [0; ADDRS_PER_INODE],
i_nid: [0; super::constants::NIDS_PER_INODE],
};
let root_inode_blk = alloc();
let root_entries = vec![RawDentry {
hash: 0,
ino: 100,
file_type: F2FS_FT_REG_FILE,
name: b"big.bin".to_vec(),
}];
let mut blk = encode_inode_block(&root_inode);
let payload_buf = encode_inline_dentries_payload(&root_entries);
let off = super::inode::I_ADDR_OFFSET + 4;
let n = payload_buf.len().min(blk.len() - off - 8);
blk[off..off + n].copy_from_slice(&payload_buf[..n]);
let crc =
crate::fs::f2fs::constants::f2fs_crc32(&blk[..super::constants::F2FS_BLK_CSUM_OFFSET]);
blk[super::constants::F2FS_BLK_CSUM_OFFSET..super::constants::F2FS_BLK_CSUM_OFFSET + 4]
.copy_from_slice(&crc.to_le_bytes());
dev.write_at(root_inode_blk as u64 * bs, &blk).unwrap();
let nat_entries = vec![
NatJournalEntry {
nid: 3,
ino: 3,
block_addr: root_inode_blk,
version: 0,
},
NatJournalEntry {
nid: 100,
ino: 100,
block_addr: file_inode_blk,
version: 0,
},
NatJournalEntry {
nid: 200,
ino: 100,
block_addr: dnode_blk,
version: 0,
},
];
let cp = Checkpoint {
version: 1,
user_block_count: total_blocks,
valid_block_count: (next - main_blkaddr) as u64,
rsvd_segment_count: 0,
overprov_segment_count: 0,
flags: 0,
cp_pack_start_sum: 1,
cp_pack_total_block_count: 2,
cp_payload: 0,
head_blkaddr: cp_blkaddr,
nat_ver_bitmap_bytesize: 64,
sit_ver_bitmap_bytesize: 64,
cur_nat_pack: 0,
cur_sit_pack: 0,
nat_journal: Vec::new(),
cur_node_segno: [0, 1, 2],
cur_node_blkoff: [0, 0, 0],
cur_data_segno: [3, 4, 5],
cur_data_blkoff: [0, 0, 0],
free_segment_count: 0,
valid_node_count: 0,
valid_inode_count: 0,
next_free_nid: 0,
};
dev.write_at(cp_blkaddr as u64 * bs, &encode_cp_head(&cp))
.unwrap();
dev.write_at(
(cp_blkaddr as u64 + 1) * bs,
&encode_nat_journal_block(&nat_entries),
)
.unwrap();
let mut f = F2fs::open(&mut dev).unwrap();
let list = f.list_path(&mut dev, "/").unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].name, "big.bin");
let mut r = f.open_file_reader(&mut dev, "/big.bin").unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out.len(), payload.len());
assert_eq!(out, payload);
}
#[test]
fn indirect_node_roundtrip() {
let nids = vec![10u32, 11, 12, 13];
let blk = encode_indirect_node(&nids);
let got = super::inode::decode_indirect_node(&blk).unwrap();
assert_eq!(got[..4], nids[..]);
assert_eq!(got.len(), super::constants::NIDS_PER_BLOCK);
}
#[test]
fn format_creates_readable_empty_root() {
let mut dev = MemoryBackend::new(1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
volume_label: "fstool".into(),
..super::FormatOpts::default()
};
let _fs = F2fs::format(&mut dev, &opts).unwrap();
let mut fs = F2fs::open(&mut dev).unwrap();
let list = fs.list_path(&mut dev, "/").unwrap();
assert!(list.is_empty());
assert_eq!(fs.volume_name(), "fstool");
}
#[test]
fn format_then_create_file_inline() {
let mut dev = MemoryBackend::new(1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
let payload = b"hello fresh f2fs writer";
let src = crate::fs::FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload.to_vec())),
len: payload.len() as u64,
};
fs.create_file(
&mut dev,
std::path::Path::new("/hello.txt"),
src,
crate::fs::FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
let mut fs2 = F2fs::open(&mut dev).unwrap();
let list = fs2.list_path(&mut dev, "/").unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].name, "hello.txt");
let mut r = fs2.open_file_reader(&mut dev, "/hello.txt").unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out, payload);
}
#[test]
fn format_then_create_file_block() {
let mut dev = MemoryBackend::new(2 * 1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
let payload: Vec<u8> = (0..(F2FS_BLKSIZE * 3))
.map(|i| (i as u8).wrapping_mul(13))
.collect();
let src = crate::fs::FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload.clone())),
len: payload.len() as u64,
};
fs.create_file(
&mut dev,
std::path::Path::new("/big.bin"),
src,
crate::fs::FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
let mut fs2 = F2fs::open(&mut dev).unwrap();
let mut r = fs2.open_file_reader(&mut dev, "/big.bin").unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out.len(), payload.len());
assert_eq!(out, payload);
}
#[test]
fn format_then_create_dir_and_child() {
let mut dev = MemoryBackend::new(1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
fs.create_dir(
&mut dev,
std::path::Path::new("/etc"),
crate::fs::FileMeta::with_mode(0o755),
)
.unwrap();
let src = crate::fs::FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"x=1".to_vec())),
len: 3,
};
fs.create_file(
&mut dev,
std::path::Path::new("/etc/config"),
src,
crate::fs::FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
let mut fs2 = F2fs::open(&mut dev).unwrap();
let root_list = fs2.list_path(&mut dev, "/").unwrap();
assert!(root_list.iter().any(|e| e.name == "etc"));
let etc = root_list.iter().find(|e| e.name == "etc").unwrap();
assert_eq!(etc.kind, EntryKind::Dir);
let etc_list = fs2.list_path(&mut dev, "/etc").unwrap();
assert_eq!(etc_list.len(), 1);
assert_eq!(etc_list[0].name, "config");
let mut r = fs2.open_file_reader(&mut dev, "/etc/config").unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out, b"x=1");
}
#[test]
fn format_then_create_symlink() {
let mut dev = MemoryBackend::new(1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
fs.create_symlink(
&mut dev,
std::path::Path::new("/link"),
std::path::Path::new("./target"),
crate::fs::FileMeta::with_mode(0o777),
)
.unwrap();
fs.flush(&mut dev).unwrap();
let mut fs2 = F2fs::open(&mut dev).unwrap();
let list = fs2.list_path(&mut dev, "/").unwrap();
let link = list.iter().find(|e| e.name == "link").unwrap();
assert_eq!(link.kind, EntryKind::Symlink);
}
#[test]
fn format_then_remove_file() {
let mut dev = MemoryBackend::new(1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
let src = crate::fs::FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"tmp".to_vec())),
len: 3,
};
fs.create_file(
&mut dev,
std::path::Path::new("/scratch"),
src,
crate::fs::FileMeta::default(),
)
.unwrap();
fs.remove(&mut dev, std::path::Path::new("/scratch"))
.unwrap();
fs.flush(&mut dev).unwrap();
let mut fs2 = F2fs::open(&mut dev).unwrap();
let list = fs2.list_path(&mut dev, "/").unwrap();
assert!(list.is_empty());
}
#[test]
fn read_only_open_rejects_create() {
let mut dev = MemoryBackend::new(1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let _ = F2fs::format(&mut dev, &opts).unwrap();
let mut fs_ro = F2fs::open(&mut dev).unwrap();
let src = crate::fs::FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"!".to_vec())),
len: 1,
};
let err = fs_ro
.create_file(
&mut dev,
std::path::Path::new("/x"),
src,
crate::fs::FileMeta::default(),
)
.err()
.unwrap();
assert!(matches!(err, crate::Error::Unsupported(_)));
}
#[test]
fn dir_spill_to_block_layout() {
let mut dev = MemoryBackend::new(4 * 1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
let n_files = 100;
for i in 0..n_files {
let name = format!("ab{i:07}"); let src = crate::fs::FileSource::Reader {
reader: Box::new(std::io::Cursor::new(vec![b'.'; 1])),
len: 1,
};
fs.create_file(
&mut dev,
std::path::Path::new(&format!("/{name}")),
src,
crate::fs::FileMeta::default(),
)
.unwrap();
}
fs.flush(&mut dev).unwrap();
let mut fs2 = F2fs::open(&mut dev).unwrap();
let list = fs2.list_path(&mut dev, "/").unwrap();
assert_eq!(list.len(), n_files);
for entry in &list {
assert_eq!(entry.kind, EntryKind::Regular);
}
}
#[test]
fn writer_overflows_into_direct_node() {
use super::constants::ADDRS_PER_INODE;
let mut dev = MemoryBackend::new(16 * 1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
let n_blocks = ADDRS_PER_INODE + 5;
let payload: Vec<u8> = (0..(n_blocks * F2FS_BLKSIZE))
.map(|i| (i as u8).wrapping_mul(17))
.collect();
let src = crate::fs::FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload.clone())),
len: payload.len() as u64,
};
fs.create_file(
&mut dev,
std::path::Path::new("/huge.bin"),
src,
crate::fs::FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
let mut fs2 = F2fs::open(&mut dev).unwrap();
let mut r = fs2.open_file_reader(&mut dev, "/huge.bin").unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out.len(), payload.len());
assert_eq!(out, payload);
}
#[test]
fn hardlink_two_names_same_inode() {
let mut dev = MemoryBackend::new(1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
let payload = b"hard-linked payload";
let src = crate::fs::FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload.to_vec())),
len: payload.len() as u64,
};
let src_nid = fs
.create_file(
&mut dev,
std::path::Path::new("/a.txt"),
src,
crate::fs::FileMeta::default(),
)
.unwrap();
let dst_nid = fs
.create_hardlink(
&mut dev,
std::path::Path::new("/a.txt"),
std::path::Path::new("/b.txt"),
)
.unwrap();
assert_eq!(src_nid, dst_nid, "hardlinks share their nid");
fs.flush(&mut dev).unwrap();
let mut fs2 = F2fs::open(&mut dev).unwrap();
let list = fs2.list_path(&mut dev, "/").unwrap();
let names: Vec<_> = list.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"a.txt"));
assert!(names.contains(&"b.txt"));
let a = list.iter().find(|e| e.name == "a.txt").unwrap();
let b = list.iter().find(|e| e.name == "b.txt").unwrap();
assert_eq!(a.inode, b.inode);
for name in &["/a.txt", "/b.txt"] {
let mut r = fs2.open_file_reader(&mut dev, name).unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out, payload);
}
let (_, ino) = fs2.read_inode(&mut dev, a.inode).unwrap();
assert_eq!(ino.links, 2);
}
#[test]
fn hardlink_rejects_directory() {
let mut dev = MemoryBackend::new(1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
fs.create_dir(
&mut dev,
std::path::Path::new("/d"),
crate::fs::FileMeta::default(),
)
.unwrap();
let err = fs
.create_hardlink(
&mut dev,
std::path::Path::new("/d"),
std::path::Path::new("/d2"),
)
.err()
.unwrap();
assert!(matches!(err, crate::Error::InvalidArgument(_)));
}
#[test]
fn dir_spills_across_multiple_dentry_blocks() {
let mut dev = MemoryBackend::new(8 * 1024 * 1024);
let opts = super::FormatOpts {
log_blocks_per_seg: 2,
..super::FormatOpts::default()
};
let mut fs = F2fs::format(&mut dev, &opts).unwrap();
let n = 300usize;
for i in 0..n {
let name = format!("file_with_name_{i:03}"); let src = crate::fs::FileSource::Reader {
reader: Box::new(std::io::Cursor::new(vec![b'.'; 1])),
len: 1,
};
fs.create_file(
&mut dev,
std::path::Path::new(&format!("/{name}")),
src,
crate::fs::FileMeta::default(),
)
.unwrap();
}
fs.flush(&mut dev).unwrap();
let mut fs2 = F2fs::open(&mut dev).unwrap();
let list = fs2.list_path(&mut dev, "/").unwrap();
assert_eq!(list.len(), n);
let (_, root) = fs2.read_inode(&mut dev, 3).unwrap();
assert!(
root.size > F2FS_BLKSIZE as u64,
"expected multi-block dentry layout, got size={}",
root.size
);
let names: std::collections::HashSet<_> = list.iter().map(|e| e.name.clone()).collect();
for i in 0..n {
assert!(names.contains(&format!("file_with_name_{i:03}")));
}
}
}