use std::collections::BTreeMap;
use std::io::{Read, Seek, SeekFrom, Write};
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::{FileHandle, FileMeta, OpenFlags};
use super::F2fs;
use super::constants::{F2FS_BLKSIZE, F2FS_DATA_EXIST, F2FS_INLINE_DATA, NEW_ADDR, NULL_ADDR};
use super::write::MAX_INLINE_DATA;
type DirtyMap = BTreeMap<u64, Vec<u8>>;
pub struct F2fsFileHandle<'a> {
fs: &'a mut F2fs,
dev: &'a mut dyn BlockDevice,
nid: u32,
size: u64,
pos: u64,
dirty: DirtyMap,
inline: bool,
inline_buf: Vec<u8>,
synced: bool,
}
impl<'a> F2fsFileHandle<'a> {
pub(crate) fn open(
fs: &'a mut F2fs,
dev: &'a mut dyn BlockDevice,
path: &std::path::Path,
flags: OpenFlags,
meta: Option<FileMeta>,
) -> Result<Self> {
if fs.writer.is_none() {
return Err(crate::Error::Unsupported(
"f2fs: open_file_rw requires a writable handle (use F2fs::format)".into(),
));
}
let existing_nid = {
let w = fs.writer.as_ref().expect("checked above");
resolve_in_writer(w, path)?
};
let nid = match existing_nid {
Some(n) => n,
None => {
if !flags.create {
return Err(crate::Error::InvalidArgument(format!(
"f2fs: open_file_rw: not found: {path:?}"
)));
}
let m = meta.ok_or_else(|| {
crate::Error::InvalidArgument(
"f2fs: open_file_rw create=true requires meta".into(),
)
})?;
fs.writer_mut()?
.add_file(dev, path, crate::fs::FileSource::Zero(0), m)?
}
};
let (inline, inline_buf, mut size) = {
let w = fs.writer.as_ref().expect("writer live");
let ino = w
.inodes
.get(&nid)
.ok_or_else(|| crate::Error::InvalidImage(format!("f2fs: ghost nid {nid}")))?;
let inline = ino.inline_flags & F2FS_INLINE_DATA != 0;
(inline, ino.inline_payload.clone(), ino.size)
};
let mut me = Self {
fs,
dev,
nid,
size,
pos: 0,
dirty: BTreeMap::new(),
inline,
inline_buf,
synced: false,
};
if flags.truncate {
me.set_len_internal(0)?;
size = 0;
}
if flags.append {
me.pos = size;
}
Ok(me)
}
fn read_at(&mut self, pos: u64, out: &mut [u8]) -> std::io::Result<usize> {
if pos >= self.size {
return Ok(0);
}
let bs = F2FS_BLKSIZE as u64;
let remaining_in_file = (self.size - pos) as usize;
let n_want = out.len().min(remaining_in_file);
if n_want == 0 {
return Ok(0);
}
if self.inline {
let start = pos as usize;
let end = (start + n_want).min(self.inline_buf.len());
if start >= end {
out[..n_want].fill(0);
return Ok(n_want);
}
let n_copy = end - start;
out[..n_copy].copy_from_slice(&self.inline_buf[start..end]);
if n_copy < n_want {
out[n_copy..n_want].fill(0);
}
return Ok(n_want);
}
let block_idx = pos / bs;
let in_block_off = (pos % bs) as usize;
let n = n_want.min(F2FS_BLKSIZE - in_block_off);
let block_bytes = self.fetch_block(block_idx).map_err(std::io::Error::other)?;
out[..n].copy_from_slice(&block_bytes[in_block_off..in_block_off + n]);
Ok(n)
}
fn fetch_block(&mut self, block_idx: u64) -> Result<Vec<u8>> {
if let Some(b) = self.dirty.get(&block_idx) {
return Ok(b.clone());
}
self.read_block_on_disk(block_idx)
}
fn read_block_on_disk(&mut self, block_idx: u64) -> Result<Vec<u8>> {
let w = self.fs.writer.as_ref().expect("writer live");
let phys = w.current_data_block(self.nid, block_idx)?;
let bs = F2FS_BLKSIZE as u64;
if phys == NULL_ADDR || phys == NEW_ADDR {
return Ok(vec![0u8; F2FS_BLKSIZE]);
}
let mut buf = vec![0u8; F2FS_BLKSIZE];
self.dev.read_at(phys as u64 * bs, &mut buf)?;
Ok(buf)
}
fn write_at(&mut self, pos: u64, data: &[u8]) -> std::io::Result<usize> {
if data.is_empty() {
return Ok(0);
}
if self.inline && (pos + data.len() as u64) as usize > MAX_INLINE_DATA {
self.de_inline().map_err(std::io::Error::other)?;
}
if self.inline {
let end = (pos + data.len() as u64) as usize;
if self.inline_buf.len() < end {
self.inline_buf.resize(end, 0);
}
self.inline_buf[pos as usize..end].copy_from_slice(data);
let new_size = self.size.max(end as u64);
self.size = new_size;
return Ok(data.len());
}
let bs = F2FS_BLKSIZE as u64;
let mut written = 0usize;
let total = data.len();
let mut cur_pos = pos;
while written < total {
let block_idx = cur_pos / bs;
let in_block_off = (cur_pos % bs) as usize;
let chunk = (F2FS_BLKSIZE - in_block_off).min(total - written);
if !self.dirty.contains_key(&block_idx) {
let on_disk = self
.read_block_on_disk(block_idx)
.map_err(std::io::Error::other)?;
self.dirty.insert(block_idx, on_disk);
}
let block = self.dirty.get_mut(&block_idx).expect("just inserted");
block[in_block_off..in_block_off + chunk]
.copy_from_slice(&data[written..written + chunk]);
written += chunk;
cur_pos += chunk as u64;
}
let new_size = self.size.max(pos + data.len() as u64);
self.size = new_size;
Ok(data.len())
}
fn de_inline(&mut self) -> Result<()> {
if !self.inline {
return Ok(());
}
let mut block0 = vec![0u8; F2FS_BLKSIZE];
let n = self.inline_buf.len().min(F2FS_BLKSIZE);
block0[..n].copy_from_slice(&self.inline_buf[..n]);
self.dirty.insert(0, block0);
self.inline = false;
self.inline_buf.clear();
let w = self.fs.writer.as_mut().expect("writer live");
let ino = w
.inodes
.get_mut(&self.nid)
.ok_or_else(|| crate::Error::InvalidImage("f2fs: ghost nid".into()))?;
ino.inline_flags &= !F2FS_INLINE_DATA;
ino.inline_flags &= !F2FS_DATA_EXIST;
ino.inline_payload.clear();
Ok(())
}
fn set_len_internal(&mut self, new_len: u64) -> Result<()> {
if new_len == self.size {
return Ok(());
}
if new_len < self.size {
let bs = F2FS_BLKSIZE as u64;
self.dirty.retain(|&idx, _| idx * bs < new_len);
let last_idx = new_len / bs;
let last_off = (new_len % bs) as usize;
if last_off != 0 {
if let Some(b) = self.dirty.get_mut(&last_idx) {
for byte in b.iter_mut().skip(last_off) {
*byte = 0;
}
}
}
if self.inline {
let n = (new_len as usize).min(self.inline_buf.len());
self.inline_buf.truncate(n);
}
self.size = new_len;
if self.pos > self.size {
self.pos = self.size;
}
return Ok(());
}
if self.inline {
if (new_len as usize) <= MAX_INLINE_DATA {
self.inline_buf.resize(new_len as usize, 0);
self.size = new_len;
return Ok(());
}
self.de_inline()?;
}
self.size = new_len;
Ok(())
}
fn sync_internal(&mut self) -> Result<()> {
if self.synced {
return Ok(());
}
let bs = F2FS_BLKSIZE as u64;
{
let w = self.fs.writer.as_mut().expect("writer live");
let ino = w
.inodes
.get_mut(&self.nid)
.ok_or_else(|| crate::Error::InvalidImage("f2fs: ghost nid".into()))?;
if self.inline {
ino.inline_flags |= F2FS_INLINE_DATA | F2FS_DATA_EXIST;
ino.inline_payload = self.inline_buf.clone();
ino.blocks = 0;
ino.i_addr = [0; super::constants::ADDRS_PER_INODE];
ino.i_nid = [0; super::constants::NIDS_PER_INODE];
}
ino.size = self.size;
}
if !self.inline {
let dirty_indices: Vec<u64> = self.dirty.keys().copied().collect();
for idx in dirty_indices {
if idx * bs >= self.size {
continue;
}
let mut block = self.dirty.remove(&idx).expect("just listed");
let file_end_in_block = if (idx + 1) * bs <= self.size {
F2FS_BLKSIZE
} else {
(self.size - idx * bs) as usize
};
for byte in block.iter_mut().skip(file_end_in_block) {
*byte = 0;
}
let phys = {
let w = self.fs.writer.as_mut().expect("writer live");
w.alloc_data_block()?
};
self.dev.write_at(phys as u64 * bs, &block)?;
let w = self.fs.writer.as_mut().expect("writer live");
w.place_data_block(self.nid, idx, phys)?;
}
let w = self.fs.writer.as_mut().expect("writer live");
let ino = w
.inodes
.get_mut(&self.nid)
.ok_or_else(|| crate::Error::InvalidImage("f2fs: ghost nid".into()))?;
let mut count = 0u64;
for a in ino.i_addr.iter() {
if *a != 0 {
count += 1;
}
}
ino.blocks = count;
} else {
self.dirty.clear();
}
self.fs.flush(self.dev)?;
self.synced = true;
Ok(())
}
}
impl<'a> Read for F2fsFileHandle<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n = self.read_at(self.pos, buf)?;
self.pos += n as u64;
Ok(n)
}
}
impl<'a> Write for F2fsFileHandle<'a> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.synced = false;
let n = self.write_at(self.pos, buf)?;
self.pos += n as u64;
Ok(n)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> Seek for F2fsFileHandle<'a> {
fn seek(&mut self, from: SeekFrom) -> std::io::Result<u64> {
let new_pos: i64 = match from {
SeekFrom::Start(n) => n as i64,
SeekFrom::End(off) => self.size as i64 + off,
SeekFrom::Current(off) => self.pos as i64 + off,
};
if new_pos < 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"f2fs: seek before start",
));
}
self.pos = new_pos as u64;
Ok(self.pos)
}
}
impl<'a> FileHandle for F2fsFileHandle<'a> {
fn len(&self) -> u64 {
self.size
}
fn set_len(&mut self, new_len: u64) -> Result<()> {
self.synced = false;
self.set_len_internal(new_len)
}
fn sync(&mut self) -> Result<()> {
self.sync_internal()
}
}
impl<'a> Drop for F2fsFileHandle<'a> {
fn drop(&mut self) {
let _ = self.sync_internal();
}
}
fn resolve_in_writer(w: &super::write::Writer, path: &std::path::Path) -> Result<Option<u32>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("f2fs: non-UTF-8 path".into()))?;
if s == "/" || s.is_empty() {
return Ok(Some(3));
}
let parts: Vec<&str> = s
.trim_matches('/')
.split('/')
.filter(|p| !p.is_empty())
.collect();
if parts.is_empty() {
return Err(crate::Error::InvalidArgument(format!(
"f2fs: empty path {s:?}"
)));
}
let mut cur = 3u32;
for comp in &parts[..parts.len() - 1] {
let kids = w.children.get(&cur).ok_or_else(|| {
crate::Error::InvalidArgument(format!("f2fs: parent of {s:?} not a known dir"))
})?;
let found = kids
.iter()
.find(|d| d.name == comp.as_bytes())
.ok_or_else(|| {
crate::Error::InvalidArgument(format!("f2fs: no such dir {comp:?} in {s:?}"))
})?;
cur = found.ino;
}
let leaf = parts[parts.len() - 1].as_bytes();
let existing = w
.children
.get(&cur)
.and_then(|kids| kids.iter().find(|d| d.name == leaf).map(|d| d.ino));
Ok(existing)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
use crate::fs::{FileMeta, FileSource, Filesystem, OpenFlags};
use std::io::SeekFrom;
use std::path::Path;
fn fresh_fs() -> (MemoryBackend, super::F2fs) {
let mut dev = MemoryBackend::new(4 * 1024 * 1024);
let opts = super::super::FormatOpts {
log_blocks_per_seg: 2,
..super::super::FormatOpts::default()
};
let fs = super::F2fs::format(&mut dev, &opts).unwrap();
(dev, fs)
}
fn read_all_from_reopened(dev: &mut MemoryBackend, name: &str) -> Vec<u8> {
let mut fs2 = super::F2fs::open(dev).unwrap();
let mut r = fs2.open_file_reader(dev, &format!("/{name}")).unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
out
}
#[test]
fn open_file_rw_partial_write_round_trip() {
let (mut dev, mut fs) = fresh_fs();
let initial = b"hello world, this is a test payload!".to_vec();
fs.create_file(
&mut dev,
Path::new("/note.txt"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(initial.clone())),
len: initial.len() as u64,
},
FileMeta::default(),
)
.unwrap();
{
let mut h = fs
.open_file_rw(&mut dev, Path::new("/note.txt"), OpenFlags::default(), None)
.unwrap();
assert_eq!(h.len(), initial.len() as u64);
h.seek(SeekFrom::Start(7)).unwrap();
h.write_all(b"WORLD").unwrap();
}
fs.flush(&mut dev).unwrap();
let got = read_all_from_reopened(&mut dev, "note.txt");
let mut expected = initial.clone();
expected[7..12].copy_from_slice(b"WORLD");
assert_eq!(got, expected);
}
#[test]
fn open_file_rw_extends_file() {
let (mut dev, mut fs) = fresh_fs();
let initial = b"short".to_vec();
fs.create_file(
&mut dev,
Path::new("/grow.bin"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(initial.clone())),
len: initial.len() as u64,
},
FileMeta::default(),
)
.unwrap();
{
let mut h = fs
.open_file_rw(&mut dev, Path::new("/grow.bin"), OpenFlags::default(), None)
.unwrap();
h.seek(SeekFrom::End(0)).unwrap();
h.write_all(b"-extended-tail").unwrap();
h.sync().unwrap();
assert_eq!(h.len(), (initial.len() + "-extended-tail".len()) as u64);
}
fs.flush(&mut dev).unwrap();
let got = read_all_from_reopened(&mut dev, "grow.bin");
assert_eq!(got, b"short-extended-tail");
}
#[test]
fn open_file_rw_set_len_grow_and_shrink() {
let (mut dev, mut fs) = fresh_fs();
let initial = b"abcdefghij".to_vec(); fs.create_file(
&mut dev,
Path::new("/sz.bin"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(initial.clone())),
len: initial.len() as u64,
},
FileMeta::default(),
)
.unwrap();
{
let mut h = fs
.open_file_rw(&mut dev, Path::new("/sz.bin"), OpenFlags::default(), None)
.unwrap();
h.set_len(20).unwrap();
assert_eq!(h.len(), 20);
h.sync().unwrap();
}
fs.flush(&mut dev).unwrap();
let got = read_all_from_reopened(&mut dev, "sz.bin");
let mut expected = initial.clone();
expected.resize(20, 0);
assert_eq!(got, expected);
{
let mut h = fs
.open_file_rw(&mut dev, Path::new("/sz.bin"), OpenFlags::default(), None)
.unwrap();
h.set_len(4).unwrap();
assert_eq!(h.len(), 4);
h.sync().unwrap();
}
fs.flush(&mut dev).unwrap();
let got = read_all_from_reopened(&mut dev, "sz.bin");
assert_eq!(got, b"abcd");
}
#[test]
fn open_file_rw_append() {
let (mut dev, mut fs) = fresh_fs();
fs.create_file(
&mut dev,
Path::new("/log.txt"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"line1\n".to_vec())),
len: 6,
},
FileMeta::default(),
)
.unwrap();
{
let mut h = fs
.open_file_rw(
&mut dev,
Path::new("/log.txt"),
OpenFlags {
append: true,
..OpenFlags::default()
},
None,
)
.unwrap();
h.write_all(b"line2\n").unwrap();
h.sync().unwrap();
}
fs.flush(&mut dev).unwrap();
let got = read_all_from_reopened(&mut dev, "log.txt");
assert_eq!(got, b"line1\nline2\n");
}
#[test]
fn open_file_rw_create_new() {
let (mut dev, mut fs) = fresh_fs();
{
let mut h = fs
.open_file_rw(
&mut dev,
Path::new("/fresh.bin"),
OpenFlags {
create: true,
..OpenFlags::default()
},
Some(FileMeta::default()),
)
.unwrap();
assert_eq!(h.len(), 0);
h.write_all(b"freshly-minted").unwrap();
h.sync().unwrap();
}
fs.flush(&mut dev).unwrap();
let got = read_all_from_reopened(&mut dev, "fresh.bin");
assert_eq!(got, b"freshly-minted");
}
#[test]
fn open_file_rw_writes_are_persisted_via_checkpoint() {
let (mut dev, mut fs) = fresh_fs();
fs.create_file(
&mut dev,
Path::new("/cp.bin"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"AAAA".to_vec())),
len: 4,
},
FileMeta::default(),
)
.unwrap();
{
let mut h = fs
.open_file_rw(&mut dev, Path::new("/cp.bin"), OpenFlags::default(), None)
.unwrap();
h.seek(SeekFrom::Start(1)).unwrap();
h.write_all(b"ZZ").unwrap();
h.sync().unwrap();
}
let mut fs2 = super::F2fs::open(&mut dev).unwrap();
let mut r = fs2.open_file_reader(&mut dev, "/cp.bin").unwrap();
let mut out = Vec::new();
r.read_to_end(&mut out).unwrap();
assert_eq!(out, b"AZZA");
}
#[test]
fn open_file_rw_de_inlines_when_growing_past_threshold() {
let (mut dev, mut fs) = fresh_fs();
fs.create_file(
&mut dev,
Path::new("/grow.bin"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"tiny".to_vec())),
len: 4,
},
FileMeta::default(),
)
.unwrap();
let big_size = MAX_INLINE_DATA + F2FS_BLKSIZE; let mut tail = vec![0u8; big_size - 4];
for (i, b) in tail.iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(7);
}
{
let mut h = fs
.open_file_rw(&mut dev, Path::new("/grow.bin"), OpenFlags::default(), None)
.unwrap();
h.seek(SeekFrom::Start(4)).unwrap();
h.write_all(&tail).unwrap();
h.sync().unwrap();
assert_eq!(h.len(), big_size as u64);
}
fs.flush(&mut dev).unwrap();
let got = read_all_from_reopened(&mut dev, "grow.bin");
let mut expected = b"tiny".to_vec();
expected.extend_from_slice(&tail);
assert_eq!(got.len(), expected.len());
assert_eq!(got, expected);
}
}