dvb-si 6.3.0

ETSI EN 300 468 DVB Service Information parser + builder. MPEG-2 PSI included.
Documentation
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::Path;

fn main() {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let out_dir = env::var("OUT_DIR").unwrap();

    println!("cargo:rerun-if-changed=registries/tsCAS.names");
    println!("cargo:rerun-if-changed=registries/tsPDS.names");

    let ca_entries = parse_names_file(
        &Path::new(&manifest_dir).join("registries/tsCAS.names"),
        "CASystemId",
    );
    let pds_entries = parse_names_file(
        &Path::new(&manifest_dir).join("registries/tsPDS.names"),
        "PrivateDataSpecifier",
    );

    let dest = Path::new(&out_dir).join("registry_names.rs");
    let mut f = fs::File::create(&dest).unwrap();

    write_ca_function(&mut f, &ca_entries).unwrap();
    writeln!(f).unwrap();
    write_pds_function(&mut f, &pds_entries).unwrap();
}

struct Entry {
    lo: u32,
    hi: u32,
    name: String,
    is_range: bool,
}

/// Parse a `.names` file, extracting entries from `section_name`.
/// Returns entries in file order.
fn parse_names_file(path: &Path, section_name: &str) -> Vec<Entry> {
    let content = fs::read_to_string(path).unwrap_or_else(|e| {
        panic!("failed to read {}: {}", path.display(), e);
    });

    let mut entries: Vec<Entry> = Vec::new();
    let mut in_section = false;

    for line in content.lines() {
        let line = line.trim();

        // Strip full-line comments.
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        // Section header.
        if line.starts_with('[') {
            in_section = line == format!("[{}]", section_name);
            continue;
        }

        if !in_section {
            continue;
        }

        // Metadata (Bits = N) — ignore.
        if line.starts_with("Bits") {
            continue;
        }

        // Split on first '='.
        let (key_part, value_part) = match line.find('=') {
            Some(pos) => {
                let k = line[..pos].trim();
                let v = &line[pos + 1..];
                (k, v)
            }
            None => continue,
        };

        // Strip inline comment.
        let value_part = match value_part.find('#') {
            Some(pos) => value_part[..pos].trim(),
            None => value_part.trim(),
        };

        if value_part.is_empty() {
            continue;
        }

        // Parse key: either "0xLO" or "0xLO-0xHI".
        let (lo, hi, is_range) = if let Some(dash_pos) = key_part.find('-') {
            let lo_str = key_part[..dash_pos].trim();
            let hi_str = key_part[dash_pos + 1..].trim();
            let lo = parse_hex(lo_str);
            let hi = parse_hex(hi_str);
            (lo, hi, true)
        } else {
            let lo = parse_hex(key_part);
            (lo, lo, false)
        };

        entries.push(Entry {
            lo,
            hi,
            name: value_part.to_string(),
            is_range,
        });
    }

    entries
}

fn parse_hex(s: &str) -> u32 {
    let s = s.strip_prefix("0x").unwrap_or(s);
    u32::from_str_radix(s, 16).unwrap_or_else(|_| panic!("invalid hex: {}", s))
}

fn escape_name(name: &str) -> String {
    name.replace('\\', "\\\\").replace('"', "\\\"")
}

fn write_ca_function(f: &mut fs::File, entries: &[Entry]) -> io::Result<()> {
    writeln!(f, "// Generated by build.rs. Do not edit.")?;
    writeln!(f, "// Source: registries/tsCAS.names [CASystemId]")?;
    writeln!(f, "#[allow(clippy::match_same_arms)]")?;
    writeln!(
        f,
        "pub(crate) fn ca_system_name_generated(id: u16) -> Option<&'static str> {{"
    )?;
    writeln!(f, "    match id {{")?;

    let mut seen_exact: BTreeSet<u32> = BTreeSet::new();
    let mut exact_pairs: Vec<(u32, &str)> = Vec::new();
    let mut range_pairs: Vec<(u32, u32, &str)> = Vec::new();
    let mut seen_range_starts: BTreeSet<u32> = BTreeSet::new();

    for entry in entries {
        if entry.is_range {
            // Skip identical range duplicates.
            if seen_range_starts.contains(&entry.lo) {
                continue;
            }
            seen_range_starts.insert(entry.lo);
            range_pairs.push((entry.lo, entry.hi, &entry.name));
        } else {
            if seen_exact.contains(&entry.lo) {
                continue;
            }
            seen_exact.insert(entry.lo);
            exact_pairs.push((entry.lo, &entry.name));
        }
    }

    // Sort exact by key, then range by start.
    exact_pairs.sort_by_key(|(k, _)| *k);
    range_pairs.sort_by_key(|(lo, hi, _)| (*lo, *hi));

    for (k, name) in &exact_pairs {
        writeln!(f, "        0x{:04X} => Some(\"{}\"),", k, escape_name(name))?;
    }

    if !range_pairs.is_empty() {
        for (lo, hi, name) in &range_pairs {
            writeln!(
                f,
                "        0x{:04X}..=0x{:04X} => Some(\"{}\"),",
                lo,
                hi,
                escape_name(name)
            )?;
        }
    }

    writeln!(f, "        _ => None,")?;
    writeln!(f, "    }}")?;
    writeln!(f, "}}")?;
    Ok(())
}

fn write_pds_function(f: &mut fs::File, entries: &[Entry]) -> io::Result<()> {
    writeln!(f, "// Generated by build.rs. Do not edit.")?;
    writeln!(
        f,
        "// Source: registries/tsPDS.names [PrivateDataSpecifier]"
    )?;
    writeln!(f, "#[allow(clippy::match_same_arms)]")?;
    writeln!(
        f,
        "pub(crate) fn private_data_specifier_name_generated(v: u32) -> Option<&'static str> {{"
    )?;
    writeln!(f, "    match v {{")?;

    let mut seen_exact: BTreeSet<u32> = BTreeSet::new();
    let mut exact_pairs: Vec<(u32, &str)> = Vec::new();
    let mut range_pairs: Vec<(u32, u32, &str)> = Vec::new();
    let mut seen_range_starts: BTreeSet<u32> = BTreeSet::new();

    for entry in entries {
        if entry.is_range {
            if seen_range_starts.contains(&entry.lo) {
                continue;
            }
            seen_range_starts.insert(entry.lo);
            range_pairs.push((entry.lo, entry.hi, &entry.name));
        } else {
            if seen_exact.contains(&entry.lo) {
                continue;
            }
            seen_exact.insert(entry.lo);
            exact_pairs.push((entry.lo, &entry.name));
        }
    }

    exact_pairs.sort_by_key(|(k, _)| *k);
    range_pairs.sort_by_key(|(lo, hi, _)| (*lo, *hi));

    for (k, name) in &exact_pairs {
        writeln!(f, "        0x{:08X} => Some(\"{}\"),", k, escape_name(name))?;
    }

    if !range_pairs.is_empty() {
        for (lo, hi, name) in &range_pairs {
            writeln!(
                f,
                "        0x{:08X}..=0x{:08X} => Some(\"{}\"),",
                lo,
                hi,
                escape_name(name)
            )?;
        }
    }

    writeln!(f, "        _ => None,")?;
    writeln!(f, "    }}")?;
    writeln!(f, "}}")?;
    Ok(())
}