libfreemkv 0.18.2

Open source raw disc access library for optical drives
Documentation
//! File-backed sector I/O — read and write 2048-byte sectors against
//! an ISO image on disk.
//!
//! [`FileSectorSource`] is the read side (open-only). [`FileSectorSink`]
//! is the write side (create or open-rw); writes go through
//! [`crate::io::WritebackFile`] so big sequential ISO writes share
//! the same bounded-cache writeback pipeline used by sweep / patch /
//! mux.

use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;

use crate::error::{Error, Result};

use super::{SectorReader, SectorSink};

/// SectorSource backed by a file (ISO image).
///
/// Seeks to `lba * 2048`, reads `count * 2048` bytes per call. The
/// file is held directly: every `read_sectors` call performs an
/// absolute seek, so a wrapping `BufReader` would have its buffer
/// invalidated on every call (its internal cursor moves with the
/// `Seek` impl) — pure overhead. Callers that benefit from buffered
/// reads should compose their own `BufReader` at the `read_sectors`
/// granularity they care about.
pub struct FileSectorSource {
    file: File,
    capacity: u32,
}

impl FileSectorSource {
    /// Open an existing ISO file for reading. Capacity is derived
    /// from `metadata().len() / 2048`. Returns
    /// [`Error::IsoTooLarge`] if the file would exceed the 32-bit
    /// LBA address space (~8 TB).
    pub fn open(path: &Path) -> std::io::Result<Self> {
        let file = File::open(path)?;
        let len = file.metadata()?.len();
        let sectors = len / 2048;
        if sectors > u32::MAX as u64 {
            return Err(Error::IsoTooLarge {
                path: path.to_string_lossy().into_owned(),
            }
            .into());
        }
        let capacity = sectors as u32;
        Ok(Self { file, capacity })
    }
}

// Implement the legacy `SectorReader` trait. The blanket impl in
// `super` produces the `SectorSource` impl automatically — no need
// to write both, and writing both would conflict. This keeps the
// 0.17 method-resolution path intact (callers with `SectorReader`
// in scope can still write `fsr.read_sectors(..)` against a
// `FileSectorSource`).
impl SectorReader for FileSectorSource {
    fn capacity(&self) -> u32 {
        self.capacity
    }

    fn read_sectors(
        &mut self,
        lba: u32,
        count: u16,
        buf: &mut [u8],
        _recovery: bool,
    ) -> Result<usize> {
        let offset = lba as u64 * 2048;
        let bytes = count as usize * 2048;
        self.file
            .seek(SeekFrom::Start(offset))
            .map_err(|e| Error::IoError { source: e })?;
        self.file
            .read_exact(&mut buf[..bytes])
            .map_err(|e| Error::IoError { source: e })?;
        Ok(bytes)
    }
}

/// SectorSink backed by a file (ISO image).
///
/// Writes go through [`crate::io::WritebackFile`], which on Linux drives
/// continuous `sync_file_range` + `posix_fadvise(DONTNEED)` to keep
/// the kernel dirty page cache bounded during multi-GB sequential
/// writes. macOS / Windows fall through to a no-op pipeline.
///
/// `finish` runs `sync_all` before dropping the underlying file.
pub struct FileSectorSink {
    inner: crate::io::WritebackFile,
}

impl FileSectorSink {
    /// Create a new ISO file at `path`, truncating any existing
    /// file. The file is opened read-write so the same handle can
    /// later be reused for verification reads if needed (sweep
    /// doesn't, but it costs nothing here).
    pub fn create(path: &Path) -> std::io::Result<Self> {
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .truncate(true)
            .open(path)?;
        Ok(Self {
            inner: crate::io::WritebackFile::new(file)?,
        })
    }

    /// Open an existing ISO file for in-place updates (e.g. patch
    /// pass writing recovered sectors over zero-filled holes).
    /// Does not truncate.
    pub fn open(path: &Path) -> std::io::Result<Self> {
        let file = OpenOptions::new().read(true).write(true).open(path)?;
        Ok(Self {
            inner: crate::io::WritebackFile::new(file)?,
        })
    }
}

impl SectorSink for FileSectorSink {
    fn write_sectors(&mut self, lba: u32, buf: &[u8]) -> Result<()> {
        debug_assert!(
            buf.len() % 2048 == 0,
            "FileSectorSink::write_sectors: buf len {} not a multiple of 2048",
            buf.len()
        );
        let offset = lba as u64 * 2048;
        self.inner
            .seek(SeekFrom::Start(offset))
            .map_err(|e| Error::IoError { source: e })?;
        self.inner
            .write_all(buf)
            .map_err(|e| Error::IoError { source: e })?;
        Ok(())
    }

    fn finish(mut self: Box<Self>) -> Result<()> {
        self.inner
            .sync_all()
            .map_err(|e| Error::IoError { source: e })?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    // Bring the 0.18 trait into scope (not super::*: the super
    // module also re-exports the legacy `SectorReader`, and
    // having both `SectorReader::read_sectors` and
    // `SectorSource::read_sectors` visible would force every
    // call site to disambiguate). External consumers see the
    // same surface this test exercises.
    use super::{FileSectorSink, FileSectorSource};
    use crate::sector::{SectorSink, SectorSource};
    use tempfile::tempdir;

    #[test]
    fn round_trip_single_sector() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("rt.iso");

        let mut sink = FileSectorSink::create(&path).unwrap();
        // Pre-extend the file to 4 sectors of zeros so we can write
        // sector 2 in place. Easiest way: write zeros first.
        let zeros = [0u8; 4 * 2048];
        sink.write_sectors(0, &zeros).unwrap();

        let mut payload = [0u8; 2048];
        for (i, b) in payload.iter_mut().enumerate() {
            *b = (i as u8).wrapping_mul(17);
        }
        sink.write_sectors(2, &payload).unwrap();
        Box::new(sink).finish().unwrap();

        let mut src = FileSectorSource::open(&path).unwrap();
        assert_eq!(src.capacity_sectors(), 4);

        let mut got = [0u8; 2048];
        let n = src.read_sectors(2, 1, &mut got, false).unwrap();
        assert_eq!(n, 2048);
        assert_eq!(got, payload);

        // Sectors 0,1,3 still zero.
        let mut z = [0xffu8; 2048];
        src.read_sectors(0, 1, &mut z, false).unwrap();
        assert!(z.iter().all(|b| *b == 0));
    }

    #[test]
    fn round_trip_multi_sector() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("multi.iso");

        let mut sink = FileSectorSink::create(&path).unwrap();
        let mut payload = vec![0u8; 8 * 2048];
        for (i, b) in payload.iter_mut().enumerate() {
            *b = ((i * 31) ^ (i >> 7)) as u8;
        }
        sink.write_sectors(0, &payload).unwrap();
        Box::new(sink).finish().unwrap();

        let mut src = FileSectorSource::open(&path).unwrap();
        assert_eq!(src.capacity_sectors(), 8);

        let mut got = vec![0u8; 8 * 2048];
        let n = src.read_sectors(0, 8, &mut got, false).unwrap();
        assert_eq!(n, 8 * 2048);
        assert_eq!(got, payload);
    }

    #[test]
    fn open_existing_does_not_truncate() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("open.iso");

        // Create with 4 sectors of pattern A.
        let mut sink = FileSectorSink::create(&path).unwrap();
        let pat_a = [0xaau8; 4 * 2048];
        sink.write_sectors(0, &pat_a).unwrap();
        Box::new(sink).finish().unwrap();

        // Reopen and overwrite sector 1 only.
        let mut sink = FileSectorSink::open(&path).unwrap();
        let pat_b = [0xbbu8; 2048];
        sink.write_sectors(1, &pat_b).unwrap();
        Box::new(sink).finish().unwrap();

        let mut src = FileSectorSource::open(&path).unwrap();
        assert_eq!(src.capacity_sectors(), 4);
        let mut got = [0u8; 2048];

        src.read_sectors(0, 1, &mut got, false).unwrap();
        assert_eq!(got, [0xaau8; 2048]);

        src.read_sectors(1, 1, &mut got, false).unwrap();
        assert_eq!(got, [0xbbu8; 2048]);

        src.read_sectors(2, 1, &mut got, false).unwrap();
        assert_eq!(got, [0xaau8; 2048]);
    }
}