use std::io::Write;
use std::process::{Command, Stdio};
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(§ion);
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
}
fn text(value: &str) -> Vec<u8> {
[&[0x0E][..], value.as_bytes()].concat()
}
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, 0xE1, 0x00, 0xF0, 0x00, 0x02, 0xE1, 0x00, 0xF0, 0x03, 0x52, 0x01, 0x00, 0x0F, 0xE1, 0x10, 0xF0, 0x03, 0x52, 0x01, 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 = [
&[0x50, 0x06, 0xF1, 0xB3, 0x00][..],
b"jpn",
&[0xC4, 0x09, 0xF3, 0x03, 0x10, 0x0F, 0xFF, 0x5E],
b"jpn",
]
.concat();
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" })
);
}