libfreemkv 0.7.1

Open source raw disc access library for optical drives
Documentation
//! Integration tests for the IOStream pipeline.

use std::io::{Cursor, Read, Write, Seek, SeekFrom};
use libfreemkv::*;
use libfreemkv::mux::meta::M2tsMeta;

fn sample_disc_title() -> DiscTitle {
    DiscTitle {
        playlist: "Test Movie".into(),
        playlist_id: 0,
        duration_secs: 7200.0,
        size_bytes: 0,
        clips: Vec::new(),
        streams: vec![
            Stream::Video(VideoStream {
                pid: 0x1011, codec: Codec::Hevc,
                resolution: "2160p".into(), frame_rate: "23.976".into(),
                hdr: HdrFormat::Hdr10, color_space: ColorSpace::Bt709,
                secondary: false, label: "Main".into(),
            }),
            Stream::Audio(AudioStream {
                pid: 0x1100, codec: Codec::TrueHd,
                channels: "7.1".into(), language: "eng".into(),
                sample_rate: "48kHz".into(), secondary: false,
                label: "English Atmos".into(),
            }),
            Stream::Audio(AudioStream {
                pid: 0x1101, codec: Codec::Ac3,
                channels: "5.1".into(), language: "fra".into(),
                sample_rate: "48kHz".into(), secondary: false,
                label: "French".into(),
            }),
            Stream::Subtitle(SubtitleStream {
                pid: 0x1200, codec: Codec::Pgs,
                language: "eng".into(), forced: false,
            }),
        ],
        extents: Vec::new(),
    }
}

// ── URL parsing ────────────────────────────────────────────────

#[test]
fn parse_url_disc() {
    let u = parse_url("disc://");
    assert_eq!(u.scheme, "disc");
    assert_eq!(u.path, "");
}

#[test]
fn parse_url_disc_device() {
    let u = parse_url("disc:///dev/sg4");
    assert_eq!(u.scheme, "disc");
    assert_eq!(u.path, "/dev/sg4");
}

#[test]
fn parse_url_mkv() {
    let u = parse_url("mkv://Dune.mkv");
    assert_eq!(u.scheme, "mkv");
    assert_eq!(u.path, "Dune.mkv");
}

#[test]
fn parse_url_network() {
    let u = parse_url("network://10.1.7.11:9000");
    assert_eq!(u.scheme, "network");
    assert_eq!(u.path, "10.1.7.11:9000");
}

#[test]
fn parse_url_bare_path_rejected() {
    let u = parse_url("Dune.mkv");
    assert_eq!(u.scheme, "unknown");
}

#[test]
fn parse_url_null() {
    let u = parse_url("null://");
    assert_eq!(u.scheme, "null");
    assert_eq!(u.path, "");
}

#[test]
fn parse_url_m2ts_with_path() {
    let u = parse_url("m2ts:///tmp/Dune.m2ts");
    assert_eq!(u.scheme, "m2ts");
    assert_eq!(u.path, "/tmp/Dune.m2ts");
}

#[test]
fn parse_url_m2ts_relative() {
    let u = parse_url("m2ts://Dune.m2ts");
    assert_eq!(u.scheme, "m2ts");
    assert_eq!(u.path, "Dune.m2ts");
}

#[test]
fn open_input_bare_path_errors() {
    let result = libfreemkv::open_input("Dune.mkv", &libfreemkv::InputOptions::default());
    assert!(result.is_err());
    let msg = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") };
    assert!(msg.contains("not a valid stream URL"), "got: {}", msg);
}

#[test]
fn open_output_bare_path_errors() {
    let dt = sample_disc_title();
    let result = libfreemkv::open_output("Dune.mkv", &dt);
    assert!(result.is_err());
    let msg = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") };
    assert!(msg.contains("not a valid stream URL"), "got: {}", msg);
}

#[test]
fn open_input_m2ts_empty_path_errors() {
    let result = libfreemkv::open_input("m2ts://", &libfreemkv::InputOptions::default());
    assert!(result.is_err());
    let msg = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") };
    assert!(msg.contains("requires a file path"), "got: {}", msg);
}

#[test]
fn open_output_null_input_errors() {
    let result = libfreemkv::open_input("null://", &libfreemkv::InputOptions::default());
    assert!(result.is_err());
    let msg = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") };
    assert!(msg.contains("write-only"), "got: {}", msg);
}

#[test]
fn open_output_disc_errors() {
    let dt = sample_disc_title();
    let result = libfreemkv::open_output("disc://", &dt);
    assert!(result.is_err());
    let msg = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") };
    assert!(msg.contains("read-only"), "got: {}", msg);
}

#[test]
fn open_input_network_no_port_errors() {
    let result = libfreemkv::open_input("network://10.0.0.1", &libfreemkv::InputOptions::default());
    assert!(result.is_err());
    let msg = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") };
    assert!(msg.contains("missing port"), "got: {}", msg);
}

#[test]
fn parse_url_stdio() {
    let u = parse_url("stdio://");
    assert_eq!(u.scheme, "stdio");
    assert_eq!(u.path, "");
}

// ── M2TS metadata roundtrip ───────────────────────────────────

#[test]
fn m2ts_meta_roundtrip() {
    let dt = sample_disc_title();
    let meta = M2tsMeta::from_title(&dt);
    let restored = meta.to_title();

    assert_eq!(restored.playlist, dt.playlist);
    assert_eq!(restored.duration_secs, dt.duration_secs);
    assert_eq!(restored.streams.len(), dt.streams.len());

    // Check video
    if let Stream::Video(v) = &restored.streams[0] {
        assert_eq!(v.codec, Codec::Hevc);
        assert_eq!(v.resolution, "2160p");
        assert_eq!(v.label, "Main");
    } else { panic!("expected video"); }

    // Check audio
    if let Stream::Audio(a) = &restored.streams[1] {
        assert_eq!(a.codec, Codec::TrueHd);
        assert_eq!(a.language, "eng");
        assert_eq!(a.label, "English Atmos");
    } else { panic!("expected audio"); }

    // Check subtitle
    if let Stream::Subtitle(s) = &restored.streams[3] {
        assert_eq!(s.language, "eng");
        assert!(!s.forced);
    } else { panic!("expected subtitle"); }
}

// ── M2TS header write + read ──────────────────────────────────

#[test]
fn m2ts_header_write_read() {
    let dt = sample_disc_title();
    let meta = M2tsMeta::from_title(&dt);

    // Write header to buffer
    let mut buf = Vec::new();
    libfreemkv::mux::meta::write_header(&mut buf, &meta).unwrap();

    // Verify magic
    assert_eq!(&buf[..4], b"FMKV");

    // Verify 192-byte alignment
    assert_eq!(buf.len() % 192, 0);

    // Read it back
    let mut cursor = Cursor::new(&buf);
    let read_back = libfreemkv::mux::meta::read_header(&mut cursor).unwrap().unwrap();
    assert_eq!(read_back.title, "Test Movie");
    assert_eq!(read_back.duration, 7200.0);
    assert_eq!(read_back.streams.len(), 4);

    // Cursor should be at end of header (192-aligned)
    assert_eq!(cursor.position() as usize, buf.len());
}

// ── M2tsStream write + read ───────────────────────────────────

#[test]
fn m2ts_stream_write_read() {
    let dt = sample_disc_title();

    // Build fake BD-TS packets
    let mut ts_data = Vec::new();
    for i in 0..10u8 {
        let mut pkt = [0u8; 192];
        pkt[4] = 0x47;
        pkt[5] = 0x10;
        pkt[6] = 0x11;
        pkt[7] = 0x10;
        pkt[8] = i;
        ts_data.extend_from_slice(&pkt);
    }

    // Write through M2tsStream to a Cursor
    let output = Cursor::new(Vec::new());
    let mut stream = M2tsStream::new(output).meta(&dt);
    stream.write_all(&ts_data).unwrap();
    stream.finish().unwrap();

    // M2tsStream consumed the cursor — we need the inner data.
    // For this test, write to a shared buffer instead.
    // Use a second pass: write header + data manually to verify read side.
    let mut encoded = Vec::new();
    let meta = M2tsMeta::from_title(&dt);
    libfreemkv::mux::meta::write_header(&mut encoded, &meta).unwrap();
    encoded.extend_from_slice(&ts_data);

    // Read back
    let cursor = Cursor::new(encoded);
    let mut stream = M2tsStream::open(cursor).unwrap();
    let info = stream.info();
    assert_eq!(info.streams.len(), 4);
    assert_eq!(info.duration_secs, 7200.0);

    // Read BD-TS data
    let mut read_buf = vec![0u8; 192 * 10];
    let mut total = 0;
    loop {
        match stream.read(&mut read_buf[total..]) {
            Ok(0) => break,
            Ok(n) => total += n,
            Err(_) => break,
        }
    }

    assert_eq!(total, 192 * 10);
    for i in 0..10u8 {
        assert_eq!(read_buf[i as usize * 192 + 4], 0x47);
        assert_eq!(read_buf[i as usize * 192 + 8], i);
    }
}

// ── M2tsStream passthrough identity ───────────────────────────

#[test]
fn m2ts_passthrough_preserves_data() {
    let dt = sample_disc_title();

    // Create original BD-TS data
    let mut original = Vec::new();
    for i in 0..100u8 {
        let mut pkt = [0u8; 192];
        pkt[4] = 0x47;
        pkt[5] = (i % 3) << 4;
        pkt[6] = i;
        for j in 8..192 { pkt[j] = i.wrapping_add(j as u8); }
        original.extend_from_slice(&pkt);
    }

    // Build encoded: header + original data
    let mut encoded = Vec::new();
    let meta = M2tsMeta::from_title(&dt);
    libfreemkv::mux::meta::write_header(&mut encoded, &meta).unwrap();
    encoded.extend_from_slice(&original);

    // Read back through M2tsStream
    let cursor = Cursor::new(encoded);
    let mut stream = M2tsStream::open(cursor).unwrap();
    let mut decoded = vec![0u8; original.len()];
    let mut total = 0;
    loop {
        match stream.read(&mut decoded[total..]) {
            Ok(0) => break,
            Ok(n) => total += n,
            Err(_) => break,
        }
    }

    // BD-TS data must be byte-identical
    assert_eq!(total, original.len());
    assert_eq!(decoded, original);
}

// ── IOStream trait ────────────────────────────────────────────

#[test]
fn m2ts_implements_iostream() {
    let dt = sample_disc_title();
    let output = Cursor::new(Vec::new());
    let stream = M2tsStream::new(output).meta(&dt);

    let mut boxed: Box<dyn IOStream> = Box::new(stream);
    let meta = boxed.info();
    assert_eq!(meta.streams.len(), 4);

    let pkt = [0u8; 192];
    boxed.write_all(&pkt).unwrap();
    boxed.finish().unwrap();
}

#[test]
fn m2ts_read_returns_error_on_write_stream() {
    let output = Cursor::new(Vec::new());
    let stream = M2tsStream::new(output);
    let mut boxed: Box<dyn IOStream> = Box::new(stream);
    let mut buf = [0u8; 10];
    assert!(boxed.read(&mut buf).is_err());
}

// ── DiscTitle::empty ──────────────────────────────────────────

#[test]
fn disc_title_empty() {
    let dt = DiscTitle::empty();
    assert_eq!(dt.streams.len(), 0);
    assert_eq!(dt.duration_secs, 0.0);
    assert!(dt.playlist.is_empty());
}