libfreemkv 0.31.1

Open source raw disc access library for optical drives
Documentation
//! Output-sink trait split for the buffering architecture.
//!
//! Two traits, one for each capability axis of an output destination:
//!
//! - [`SequentialSink`] — anything you can `Write` to in order. Sockets,
//!   pipes, append-only stores, plain files. Containers that don't need
//!   seek (M2TS, fMP4, HEVC elementary) target this.
//! - [`RandomAccessSink`] — everything `SequentialSink` plus a working
//!   `Seek`. Local files, NFS files, anything with random-write
//!   semantics. Containers that need backpatch (MKV cluster sizes, Cues
//!   index, MP4 moov-at-end) target this.
//!
//! `RandomAccessSink: SequentialSink` — every random-access sink is
//! also a valid sequential sink. The muxer is generic over which it
//! requires (`MkvMux<S: RandomAccessSink>`, `M2tsMux<S: SequentialSink>`)
//! so an attempt to mux MKV to a network socket is a compile error.
//!
//! Buffering policy belongs to the concrete sink, not to a wrapper at
//! the call site. `LocalFileSink` wraps a `BufWriter<File>` with a
//! 4 MiB buffer for the common local-disk case; `WritebackFile`
//! (separate module) wraps a `File` with the adaptive-chunk
//! `sync_file_range` machinery for the Linux+NFS case.

use std::io::{Seek, Write};

mod local_file;
mod preallocate;
mod socket;

pub use local_file::LocalFileSink;
pub use socket::{SocketSink, UdpSocketSink};

/// Sequential-only write destination. Sockets, pipes, append-only
/// stores. No seek. Implementations own their write buffering — the
/// trait does not impose or hide any buffering of its own.
///
/// `finish` drains any internal buffering and signals end-of-stream to
/// the underlying transport (close-write on a socket, flush + fsync on
/// a buffered file, etc.). The default impl flushes via [`Write::flush`]
/// — correct for an unbuffered destination — but every concrete sink in
/// this module overrides it to drain its own buffer and run its
/// transport-specific finalisation (socket `shutdown(Write)`, file
/// `fsync`). There is deliberately NO blanket `impl SequentialSink for
/// T`: a blanket impl would force the no-op-style default on every
/// concrete sink (a blanket impl cannot be overridden per-type without a
/// coherence conflict), so a `Box<dyn SequentialSink>` / `&mut dyn
/// SequentialSink` `finish()` call would silently skip the flush and
/// transport shutdown. With explicit per-type impls the vtable dispatches
/// `finish` to the real implementation, so flush + durable-finish
/// actually happen through a trait object.
pub trait SequentialSink: Write + Send {
    fn finish(&mut self) -> std::io::Result<()> {
        self.flush()
    }
}

/// Random-access write destination. Local files, NFS files, anything
/// with a working `Seek`. Inherits the `SequentialSink` contract — a
/// random-access sink is always usable as a sequential sink.
pub trait RandomAccessSink: SequentialSink + Seek {}

/// Pick the right `RandomAccessSink` impl for `dest` based on its
/// filesystem type.
///
/// - Linux + NFS path → `WritebackFile` with its adaptive-chunk
///   sync_file_range machinery and (when supported) `fallocate` size
///   hint.
/// - everything else → [`LocalFileSink`] over `BufWriter<File>`. On
///   non-Linux there is no `WritebackFile` machinery to opt into, and
///   on local Linux the kernel's default writeback policy is already
///   fine.
///
/// `size_hint`, when present, is forwarded to the per-OS preallocate
/// path (`fallocate(KEEP_SIZE)` on Linux, `F_PREALLOCATE` on macOS when
/// implemented, no-op elsewhere).
///
/// Returns a boxed trait object so the call site (mux construction)
/// stays agnostic of which concrete sink got picked.
#[allow(dead_code)] // wiring to mux::resolve is a follow-up commit
pub fn open_for_mkv(
    dest: &std::path::Path,
    size_hint: Option<u64>,
) -> std::io::Result<Box<dyn RandomAccessSink>> {
    #[cfg(target_os = "linux")]
    {
        use crate::platform::fs_type::{FsType, detect};
        if detect(dest) == FsType::Nfs {
            let wf = match size_hint {
                Some(n) => crate::io::WritebackFile::create_with_size_hint(dest, n)?,
                None => crate::io::WritebackFile::create(dest)?,
            };
            return Ok(Box::new(wf));
        }
    }
    // Only Linux differentiates the sink by filesystem type (NFS gets
    // the WritebackFile machinery); every other OS always uses
    // `LocalFileSink`. Reference `detect` as a value (no call, no
    // `statfs` syscall) so it isn't flagged dead on non-Linux while
    // still avoiding the wasted probe whose result we'd discard.
    #[cfg(not(target_os = "linux"))]
    let _ = crate::platform::fs_type::detect;

    let sink = match size_hint {
        Some(n) => LocalFileSink::with_size_hint(dest, n)?,
        None => LocalFileSink::create(dest)?,
    };
    Ok(Box::new(sink))
}

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

    // Type-level assertion: the concrete sinks satisfy the trait
    // objects. These functions never run; they just have to type-check.
    fn _assert_is_sequential(_: &mut dyn SequentialSink) {}
    fn _assert_is_random_access(_: &mut dyn RandomAccessSink) {}

    #[test]
    fn concrete_sinks_satisfy_traits() {
        let dir = tempfile::tempdir().unwrap();

        // `LocalFileSink` is a random-access (and thus sequential) sink.
        let mut s = LocalFileSink::create(&dir.path().join("b.bin")).unwrap();
        _assert_is_sequential(&mut s);
        _assert_is_random_access(&mut s);

        // `WritebackFile` ditto, via its explicit per-type impls.
        let mut wf = crate::io::WritebackFile::create(&dir.path().join("c.bin")).unwrap();
        _assert_is_sequential(&mut wf);
        _assert_is_random_access(&mut wf);
    }

    #[test]
    fn open_for_mkv_returns_a_random_access_sink() {
        let dir = tempfile::tempdir().unwrap();
        let p = dir.path().join("c.bin");
        let mut sink = open_for_mkv(&p, Some(64 * 1024)).unwrap();
        use std::io::{Seek, SeekFrom, Write};
        sink.write_all(b"hello").unwrap();
        sink.seek(SeekFrom::Start(0)).unwrap();
        sink.finish().unwrap();
        drop(sink);
        let bytes = std::fs::read(&p).unwrap();
        assert_eq!(&bytes[..5], b"hello");
    }

    /// finish() through a `dyn SequentialSink` trait object must
    /// dispatch to the concrete sink's override (flush + fsync), not a
    /// no-op default. This is the regression test for the silent-no-op
    /// finish() bug.
    #[test]
    fn finish_through_trait_object_flushes_local_file() {
        let dir = tempfile::tempdir().unwrap();
        let p = dir.path().join("trait-finish.bin");
        let sink = LocalFileSink::create(&p).unwrap();
        // Box as the trait object the production path uses.
        let mut boxed: Box<dyn SequentialSink> = Box::new(sink);
        boxed.write_all(b"buffered-tail").unwrap();
        // finish() through the vtable must drain the 4 MiB BufWriter and
        // fsync; the bytes must be visible to a separate reader BEFORE
        // we drop the sink (drop-flush must not be what saves us).
        boxed.finish().unwrap();
        let bytes = std::fs::read(&p).unwrap();
        assert_eq!(&bytes[..], b"buffered-tail");
    }
}