farena 0.3.0

A file-backed arena allocator using pread for memory byte storage
Documentation
use std::collections::BTreeMap;
use std::fs::File;
use std::io::{Error, Result};

use crate::{FileArena, Location};

/// Builder that assembles a [`FileArena`] from multiple writer files,
/// automatically placing each file at the correct position.
///
/// Unlike [`FileArena::new`], the builder does not require files to be
/// passed in index order. It reads the file index from each [`Location`]
/// and places files correctly.
///
/// # Index Requirements
///
/// File indices must form a contiguous range from 0 to N. Gaps are not
/// allowed since the arena uses the index as a direct vector position.
///
/// # Example
///
/// ```rust
/// # use farena::{FileArenaWriter, FileArenaBuilder};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut w0 = FileArenaWriter::new(0)?;
/// let loc0 = w0.push("a")?;
/// let f0 = w0.finish()?;
///
/// let mut w1 = FileArenaWriter::new(1)?;
/// let loc1 = w1.push("b")?;
/// let f1 = w1.finish()?;
///
/// let mut builder = FileArenaBuilder::new();
/// builder.add(f1, loc1);
/// builder.add(f0, loc0);
/// let arena = builder.build()?;
///
/// assert_eq!(arena.get(loc0)?, b"a");
/// assert_eq!(arena.get(loc1)?, b"b");
/// # Ok(())
/// # }
/// ```
pub struct FileArenaBuilder {
    files: BTreeMap<u16, File>,
}

impl FileArenaBuilder {
    /// Creates a new empty builder.
    #[must_use]
    pub fn new() -> Self {
        Self {
            files: BTreeMap::new(),
        }
    }

    /// Adds a finished writer file to the arena.
    ///
    /// The `location` parameter determines which file index this file
    /// corresponds to. Only the file index is used; offset and length
    /// are ignored.
    ///
    /// If a file with the same index already exists, it is replaced.
    pub fn add(&mut self, file: File, location: Location) {
        self.files.insert(location.file_index(), file);
    }

    /// Builds the [`FileArena`] from the collected files.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - No files were added
    /// - File indices do not form a contiguous range from 0 to N
    ///
    /// # Panics
    ///
    /// Panics if more than `u16::MAX` files are added (extremely unlikely).
    pub fn build(self) -> Result<FileArena> {
        if self.files.is_empty() {
            return Err(Error::other("FileArenaBuilder::build: no files were added"));
        }

        let n = self.files.len();
        let mut files = Vec::with_capacity(n);
        for (i, (index, file)) in self.files.into_iter().enumerate() {
            let expected = u16::try_from(i).expect("file index exceeds u16 range");
            if index != expected {
                return Err(Error::other(format!(
                    "FileArenaBuilder::build: gap in file indices at \
                     position {expected}, found index {index}\
                     indices must be contiguous from 0",
                )));
            }
            files.push(file);
        }

        FileArena::new(files)
    }
}

impl Default for FileArenaBuilder {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn builder_single_file() {
        let mut writer = FileArenaWriter::new(0).unwrap();
        let loc = writer.push("hello").unwrap();
        let file = writer.finish().unwrap();
        let mut builder = FileArenaBuilder::new();
        builder.add(file, loc);
        let arena = builder.build().unwrap();
        assert_eq!(arena.get(loc).unwrap(), b"hello");
    }

    #[test]
    fn builder_multiple_writers_reverse_order() {
        let mut w0 = FileArenaWriter::new(0).unwrap();
        let loc0 = w0.push("first").unwrap();
        let f0 = w0.finish().unwrap();
        let mut w1 = FileArenaWriter::new(1).unwrap();
        let loc1 = w1.push("second").unwrap();
        let f1 = w1.finish().unwrap();
        let mut builder = FileArenaBuilder::new();
        builder.add(f1, loc1);
        builder.add(f0, loc0);
        let arena = builder.build().unwrap();
        assert_eq!(arena.get(loc0).unwrap(), b"first");
        assert_eq!(arena.get(loc1).unwrap(), b"second");
    }

    #[test]
    fn builder_rejects_gap_in_indices() {
        let mut w0 = FileArenaWriter::new(0).unwrap();
        let loc0 = w0.push("a").unwrap();
        let f0 = w0.finish().unwrap();
        let mut w2 = FileArenaWriter::new(2).unwrap();
        let loc2 = w2.push("c").unwrap();
        let f2 = w2.finish().unwrap();
        let mut builder = FileArenaBuilder::new();
        builder.add(f0, loc0);
        builder.add(f2, loc2);
        let err_msg = match builder.build() {
            Ok(_) => panic!("expected error"),
            Err(e) => e.to_string(),
        };
        assert!(err_msg.contains("gap"));
    }

    #[test]
    fn builder_rejects_empty() {
        let builder = FileArenaBuilder::new();
        let err_msg = match builder.build() {
            Ok(_) => panic!("expected error"),
            Err(e) => e.to_string(),
        };
        assert!(err_msg.contains("no files"));
    }

    #[test]
    fn builder_default_impl() {
        let builder = FileArenaBuilder::default();
        let err_msg = match builder.build() {
            Ok(_) => panic!("expected error"),
            Err(e) => e.to_string(),
        };
        assert!(err_msg.contains("no files"));
    }
}