arib-cli 0.3.0

Reads the signalling of ARIB broadcasts, as an example of the arib crate
//! Runs the commands on a stream made up here, as there is no broadcast to record in CI.

use std::io::Write;
use std::process::{Command, Stdio};

/// The CRC-32 of MPEG-2 the sections end in.
fn crc32(bytes: &[u8]) -> u32 {
    let mut crc = 0xFFFF_FFFF_u32;
    for &byte in bytes {
        crc ^= u32::from(byte) << 24;
        for _ in 0..8 {
            crc = if crc & 0x8000_0000 != 0 {
                (crc << 1) ^ 0x04C1_1DB7
            } else {
                crc << 1
            };
        }
    }
    crc
}

fn section(table_id: u8, body: &[u8]) -> Vec<u8> {
    let length = body.len() + 4;
    let mut section = vec![table_id, 0xB0 | (length >> 8) as u8, length as u8];
    section.extend_from_slice(body);
    let crc = crc32(&section);
    section.extend_from_slice(&crc.to_be_bytes());
    section
}

fn ts_packet(pid: u16, section: &[u8]) -> Vec<u8> {
    let mut packet = vec![0x47, 0x40 | (pid >> 8) as u8, pid as u8, 0x10, 0x00];
    packet.extend_from_slice(section);
    packet.resize(188, 0xFF);
    packet
}

/// Text in the alphanumeric set of ARIB STD-B24, which G1 holds by default.
fn text(value: &str) -> Vec<u8> {
    [&[0x0E][..], value.as_bytes()].concat()
}

/// A stream of network 4 named "BS!", carrying service 0x0400 "ARIB TV" of "ARIB" of an MPEG-2
/// video and an AAC audio, with the event 0x1234 "News" on air from 1993-10-13 12:45 for 30
/// minutes, in 1080i and stereo.
fn stream() -> Vec<u8> {
    let pat = section(
        0x00,
        &[0x40, 0x10, 0xC1, 0x00, 0x00, 0x04, 0x00, 0xF0, 0x00],
    );
    let pmt = section(
        0x02,
        &[
            0x04, 0x00, 0xC1, 0x00, 0x00, // program 0x0400
            0xE1, 0x00, 0xF0, 0x00, // PCR PID, no descriptors
            0x02, 0xE1, 0x00, 0xF0, 0x03, 0x52, 0x01, 0x00, // MPEG-2 video tagged 0x00
            0x0F, 0xE1, 0x10, 0xF0, 0x03, 0x52, 0x01, 0x10, // AAC tagged 0x10
        ],
    );

    let network_name = text("BS!");
    let nit = section(
        0x40,
        &[
            &[
                0x00,
                0x04,
                0xC1,
                0x00,
                0x00,
                0xF0,
                2 + network_name.len() as u8,
                0x40,
            ][..],
            &[network_name.len() as u8],
            &network_name,
            &[0xF0, 0x00],
        ]
        .concat(),
    );

    let (provider, name) = (text("ARIB"), text("ARIB TV"));
    let service_descriptor = [
        &[
            0x48,
            (3 + provider.len() + name.len()) as u8,
            0x01,
            provider.len() as u8,
        ][..],
        &provider,
        &[name.len() as u8],
        &name,
    ]
    .concat();
    let sdt = section(
        0x42,
        &[
            &[0x40, 0x10, 0xC1, 0x00, 0x00, 0x00, 0x04, 0xFF][..],
            &[0x04, 0x00, 0xFD, 0x80, service_descriptor.len() as u8],
            &service_descriptor,
        ]
        .concat(),
    );

    let (event_name, description) = (text("News"), text("Today"));
    let event_descriptor = [
        &[0x4D, (5 + event_name.len() + description.len()) as u8][..],
        b"jpn",
        &[event_name.len() as u8],
        &event_name,
        &[description.len() as u8],
        &description,
    ]
    .concat();
    let components = [
        // 1080i, 16:9, tagged 0x00.
        &[0x50, 0x06, 0xF1, 0xB3, 0x00][..],
        b"jpn",
        // Stereo in 48 kHz, main, tagged 0x10.
        &[0xC4, 0x09, 0xF3, 0x03, 0x10, 0x0F, 0xFF, 0x5E],
        b"jpn",
    ]
    .concat();
    // Of weather, with "Cast: Alice and Bob" in the detailed description, split between two
    // descriptors in the middle of the text, which only goes on in the alphanumeric set when the
    // two halves are read as one.
    let content = [0x54, 0x02, 0x01, 0xFF];
    let extended = |number: u8, description: &[u8], item: &[u8]| {
        let items = [
            &[description.len() as u8][..],
            description,
            &[item.len() as u8],
            item,
        ]
        .concat();
        [
            &[0x4E, (6 + items.len()) as u8, number << 4 | 1][..],
            b"jpn",
            &[items.len() as u8],
            &items,
            &[0x00],
        ]
        .concat()
    };
    let details = [
        extended(0, &text("Cast"), &text("Alice an")),
        extended(1, b"", b"d Bob"),
    ]
    .concat();
    let descriptors = [event_descriptor, components, content.to_vec(), details].concat();
    let eit = section(
        0x4E,
        &[
            &[
                0x04, 0x00, 0xC1, 0x00, 0x01, 0x40, 0x10, 0x00, 0x04, 0x01, 0x4E,
            ][..],
            &[0x12, 0x34, 0xC0, 0x79, 0x12, 0x45, 0x00, 0x00, 0x30, 0x00],
            &[0x80, descriptors.len() as u8],
            &descriptors,
        ]
        .concat(),
    );

    [
        ts_packet(0x0000, &pat),
        ts_packet(0x1000, &pmt),
        ts_packet(0x0010, &nit),
        ts_packet(0x0011, &sdt),
        ts_packet(0x0012, &eit),
    ]
    .concat()
}

fn arib(args: &[&str]) -> String {
    let mut child = Command::new(env!("CARGO_BIN_EXE_arib"))
        .args(args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();
    child.stdin.take().unwrap().write_all(&stream()).unwrap();

    let output = child.wait_with_output().unwrap();
    assert!(output.status.success(), "arib {args:?} failed");
    String::from_utf8(output.stdout).unwrap()
}

#[test]
fn shows_the_status_of_the_stream() {
    assert_eq!(
        arib(&["status"]),
        "Network: 0x0004 BS!\n\
         Stream: 0x4010\n\
         Services:\n  \
           0x0400: ARIB TV (type 0x01)\n    \
             Provider: ARIB\n    \
           Stream 0x0100[0x00]: Video: MPEG-2, 1080i, 16:9\n    \
           Stream 0x0110[0x10]: Audio: AAC (ADTS), stereo, 48 kHz, jpn, main\n    \
           1993-10-13 12:45 - 13:15 (30 min)  0x1234  News\n        \
             Today\n"
    );
}

#[test]
fn lists_the_services() {
    let output = arib(&["services", "--format", "ts"]);

    assert!(
        output.contains("Stream: 0x4010 of network 0x0004 (this stream)"),
        "{output}"
    );
    assert!(output.contains("0x0400: ARIB TV (type 0x01)"), "{output}");
}

#[test]
fn lists_the_events_of_a_service() {
    let output = arib(&["events", "--service", "0x0400"]);
    assert!(output.starts_with("Service: 0x0400\n"), "{output}");
    assert!(output.contains("0x1234  News"), "{output}");

    assert_eq!(arib(&["events", "--service", "1"]), "");
}

#[test]
fn shows_the_details_of_the_events_with_verbose() {
    let output = arib(&["events", "-v"]);

    let details = [
        "      Genre: News/reports - Weather report",
        "      Access: free",
        "      Cast:",
        "        Alice and Bob",
    ];
    assert!(
        output.ends_with(&format!("{}\n", details.join("\n"))),
        "{output}"
    );
    assert!(!arib(&["events"]).contains("Genre:"));
}

#[test]
fn prints_the_status_as_json() {
    let status: serde_json::Value = serde_json::from_str(&arib(&["status", "--json"])).unwrap();

    assert_eq!(status["network"]["name"], "BS!");
    assert_eq!(status["stream_id"], 0x4010);
    let service = &status["services"][0];
    assert_eq!(service["name"], "ARIB TV");
    assert_eq!(
        service["streams"][1]["details"],
        serde_json::json!(["stereo", "48 kHz", "jpn", "main"])
    );
    assert_eq!(service["event"]["start_time"], "1993-10-13T12:45:00+09:00");
    assert_eq!(service["event"]["duration"], 1800);
}

#[test]
fn prints_the_services_as_json() {
    let services: serde_json::Value = serde_json::from_str(&arib(&["services", "--json"])).unwrap();

    assert_eq!(services["streams"][0]["actual"], true);
    assert_eq!(services["streams"][0]["services"][0]["id"], 0x0400);
}

#[test]
fn prints_the_events_as_json_lines() {
    let output = arib(&["events", "--json"]);
    let events: Vec<serde_json::Value> = output
        .lines()
        .map(|line| serde_json::from_str(line).unwrap())
        .collect();

    assert_eq!(events.len(), 1, "{output}");
    assert_eq!(events[0]["service_id"], 0x0400);
    assert_eq!(events[0]["name"], "News");
    assert_eq!(events[0]["genres"][0], "News/reports - Weather report");
    assert_eq!(
        events[0]["details"][0],
        serde_json::json!({ "description": "Cast", "item": "Alice and Bob" })
    );
}