farena 0.2.1

A file-backed arena allocator using pread for RSS-conscious byte storage
Documentation
use std::fs::File;
use std::io::{BufWriter, Write};

use crate::Location;

/// Writer for appending data to an anonymous temp file.
#[must_use = "call `finish()` to flush the backing file"]
pub struct FileArenaWriter {
    file_index: u16,
    file: File,
    writer: BufWriter<File>,
    cursor: usize,
}

impl FileArenaWriter {
    /// Creates a new writer backed by an anonymous temp file.
    ///
    /// `file_index` is the index this writer's file will occupy in the
    /// `FileArena`. It is baked into every `Location` returned by `push()`.
    ///
    /// Uses a default buffer size of 8KB. For custom buffer sizes, use
    /// [`with_capacity`](Self::with_capacity).
    pub fn new(file_index: u16) -> std::io::Result<Self> {
        Self::with_capacity(file_index, 8 * 1024)
    }

    /// Creates a new writer with a custom buffer size.
    ///
    /// `file_index` is the index this writer's file will occupy in the
    /// `FileArena`. `capacity` is the buffer size in bytes.
    ///
    /// Larger buffers reduce syscalls for sequential writes but use more memory.
    /// Smaller buffers flush more frequently, which may be better for crash
    /// recovery or when memory is constrained.
    pub fn with_capacity(file_index: u16, capacity: usize) -> std::io::Result<Self> {
        let file = tempfile::tempfile()?;
        let writer = BufWriter::with_capacity(capacity, file.try_clone()?);
        Ok(Self {
            file_index,
            file,
            writer,
            cursor: 0,
        })
    }

    /// Appends data, returning a [`Location`] pointing to the written bytes.
    pub fn push(&mut self, data: impl AsRef<[u8]>) -> std::io::Result<Location> {
        let data = data.as_ref();

        if data.is_empty() {
            return Ok(Location::new(self.file_index, self.cursor, 0));
        }

        let offset = self.cursor;
        self.writer.write_all(data)?;
        self.cursor += data.len();

        Ok(Location::new(self.file_index, offset, data.len()))
    }

    /// Returns the current cursor position (total bytes written).
    pub fn len(&self) -> usize {
        self.cursor
    }

    /// Returns true if no data has been written.
    pub fn is_empty(&self) -> bool {
        self.cursor == 0
    }

    /// Flushes the writer and returns the underlying file.
    ///
    /// Always returns a file — empty writers return a 0-byte file.
    pub fn finish(mut self) -> std::io::Result<File> {
        self.writer.flush()?;
        Ok(self.file)
    }

    /// Flushes the writer and wraps its file in a single-file [`FileArena`].
    ///
    /// Convenience for the common case where one writer produces one arena.
    /// For multi-writer workflows, use [`finish`](Self::finish) instead.
    pub fn into_arena(self) -> std::io::Result<crate::FileArena> {
        let file = self.finish()?;
        crate::FileArena::new(vec![file])
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn push_returns_location_with_correct_file_index() {
        let mut writer = FileArenaWriter::new(3).unwrap();
        let loc = writer.push("hello").unwrap();
        assert_eq!(loc.file_index, 3);
        assert_eq!(loc.offset, 0);
        assert_eq!(loc.len, 5);
    }

    #[test]
    fn push_advances_offset() {
        let mut writer = FileArenaWriter::new(0).unwrap();
        let loc1 = writer.push("abc").unwrap();
        let loc2 = writer.push("de").unwrap();
        assert_eq!(loc1.offset, 0);
        assert_eq!(loc1.len, 3);
        assert_eq!(loc2.offset, 3);
        assert_eq!(loc2.len, 2);
    }

    #[test]
    fn push_empty_returns_zero_location() {
        let mut writer = FileArenaWriter::new(1).unwrap();
        let loc = writer.push("").unwrap();
        assert_eq!(loc.len, 0);
        assert_eq!(loc.offset, 0);
        assert!(writer.is_empty());
    }

    #[test]
    fn push_empty_after_data_uses_current_cursor() {
        let mut writer = FileArenaWriter::new(0).unwrap();
        let loc1 = writer.push("hello").unwrap();
        assert_eq!(loc1.offset, 0);
        assert_eq!(loc1.len, 5);
        assert_eq!(writer.len(), 5);

        // Empty push after data should use current cursor (5)
        let loc2 = writer.push("").unwrap();
        assert_eq!(loc2.len, 0);
        assert_eq!(loc2.offset, 5);

        let loc3 = writer.push("world").unwrap();
        assert_eq!(loc3.offset, 5);
        assert_eq!(loc3.len, 5);
    }

    #[test]
    fn finish_empty_writer_returns_file() {
        // No Option — always returns a File even when nothing was written
        let writer = FileArenaWriter::new(0).unwrap();
        let file = writer.finish().unwrap();
        // Can confirm it's a 0-byte file
        assert_eq!(file.metadata().unwrap().len(), 0);
    }

    #[test]
    fn finish_returns_file_after_push() {
        let mut writer = FileArenaWriter::new(0).unwrap();
        writer.push("data").unwrap();
        let file = writer.finish().unwrap();
        assert_eq!(file.metadata().unwrap().len(), 4);
    }
}