//! What a file is, from its bytes alone (extensions/media-identify.md): the formats its bytes
//! fit, each with the bytes that say so and how far they were followed; what a registered
//! probe reads of it; and measures that hold when no format is recognised. It decodes nothing,
//! and what it does not establish it leaves out.
//!
//! All of it is pure integer code over bytes with no dependency beyond `serde_json` and the
//! probes, so it builds for wasm32 and gives the same result on every machine. Every walk reads
//! through a [`Source`] and is bounded, so a hostile or enormous file costs a bounded amount.

use crate::codecs::*;
use serde_json::{json, Map, Value};
use std::io;

pub mod audio;
pub mod corpus;
pub mod image;
pub mod iso;
pub mod matroska;
pub mod measure;
pub mod stream;
pub mod text;
pub mod ts;

/// The bytes every walk of a stream sees, and every probe (§3.2).
pub const HEAD: usize = 1 << 20;
/// The bytes the measures and the search for embedded signatures see (§3.2).
pub const SCAN: usize = 16 << 20;
/// The structures one walk reads before it stops with `limit` (§3.3).
pub const MAX_STEPS: u64 = 4096;
/// The tracks one candidate reports (§3.3).
pub const MAX_TRACKS: usize = 64;
/// The evidence one candidate reports (§3.3).
pub const MAX_EVIDENCE: usize = 8;
/// The embedded signatures reported (§7).
pub const MAX_EMBEDDED: usize = 32;
/// The longest string read from a file that a result holds (§3.4).
pub const MAX_STRING: usize = 64;

/// The bytes being identified, read at any offset.
pub trait Source {
    fn size(&self) -> u64;
    /// Reads into `buf` from `at`; fewer bytes than asked only at the end of the file.
    fn read_at(&self, at: u64, buf: &mut [u8]) -> io::Result<usize>;
}

impl Source for &[u8] {
    fn size(&self) -> u64 {
        self.len() as u64
    }

    fn read_at(&self, at: u64, buf: &mut [u8]) -> io::Result<usize> {
        let tail = usize::try_from(at).ok().and_then(|a| self.get(a..)).unwrap_or(&[]);
        let n = tail.len().min(buf.len());
        buf[..n].copy_from_slice(&tail[..n]);
        Ok(n)
    }
}

/// A file, read where a walk asks, so that a large one is never held in memory.
pub struct FileSource {
    file: std::cell::RefCell<std::fs::File>,
    size: u64,
}

impl FileSource {
    pub fn open(path: &std::path::Path) -> io::Result<Self> {
        let file = std::fs::File::open(path)?;
        let size = file.metadata()?.len();
        Ok(Self { file: std::cell::RefCell::new(file), size })
    }
}

impl Source for FileSource {
    fn size(&self) -> u64 {
        self.size
    }

    fn read_at(&self, at: u64, buf: &mut [u8]) -> io::Result<usize> {
        use io::{Read, Seek, SeekFrom};
        let mut f = self.file.borrow_mut();
        f.seek(SeekFrom::Start(at))?;
        let mut done = 0;
        while done < buf.len() {
            match f.read(&mut buf[done..])? {
                0 => break,
                n => done += n,
            }
        }
        Ok(done)
    }
}

/// Up to `n` bytes at `at`: fewer only at the end of the file.
pub(crate) fn get(src: &dyn Source, at: u64, n: usize) -> io::Result<Vec<u8>> {
    let mut v = vec![0u8; n];
    let got = src.read_at(at, &mut v)?;
    v.truncate(got);
    Ok(v)
}

pub(crate) fn be16(b: &[u8], i: usize) -> Option<u64> {
    Some(u16::from_be_bytes(b.get(i..i.checked_add(2)?)?.try_into().ok()?) as u64)
}
pub(crate) fn be24(b: &[u8], i: usize) -> Option<u64> {
    let s = b.get(i..i.checked_add(3)?)?;
    Some(((s[0] as u64) << 16) | ((s[1] as u64) << 8) | s[2] as u64)
}
pub(crate) fn be32(b: &[u8], i: usize) -> Option<u64> {
    Some(u32::from_be_bytes(b.get(i..i.checked_add(4)?)?.try_into().ok()?) as u64)
}
pub(crate) fn be64(b: &[u8], i: usize) -> Option<u64> {
    Some(u64::from_be_bytes(b.get(i..i.checked_add(8)?)?.try_into().ok()?))
}
pub(crate) fn le16(b: &[u8], i: usize) -> Option<u64> {
    Some(u16::from_le_bytes(b.get(i..i.checked_add(2)?)?.try_into().ok()?) as u64)
}
pub(crate) fn le32(b: &[u8], i: usize) -> Option<u64> {
    Some(u32::from_le_bytes(b.get(i..i.checked_add(4)?)?.try_into().ok()?) as u64)
}

/// A string read from a file as a result may hold it (§3.4): printable ASCII, every other byte
/// and the backslash written `\xNN`, at most [`MAX_STRING`] bytes of it.
pub(crate) fn clean(b: &[u8]) -> String {
    let mut s = String::new();
    for &c in b.iter().take(MAX_STRING) {
        if (0x20..0x7f).contains(&c) && c != b'\\' {
            s.push(c as char);
        } else {
            s.push_str(&format!("\\x{c:02x}"));
        }
    }
    s
}

/// How far a candidate's bytes were followed (§4).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Strength {
    Signature,
    Structure,
    Parameters,
}

impl Strength {
    pub fn label(self) -> &'static str {
        match self {
            Strength::Signature => "signature",
            Strength::Structure => "structure",
            Strength::Parameters => "parameters",
        }
    }
}

/// A format the bytes fit.
#[derive(Debug, Clone)]
pub struct Candidate {
    pub id: &'static str,
    pub kind: &'static str,
    pub strength: Strength,
    pub evidence: Vec<(u64, u64, String)>,
    pub walked_to: Option<u64>,
    pub stopped: Option<(u64, &'static str)>,
    pub facts: Map<String, Value>,
    pub tracks: Vec<Value>,
    pub capabilities: Vec<&'static str>,
}

impl Candidate {
    pub(crate) fn new(id: &'static str, kind: &'static str) -> Self {
        Self {
            id,
            kind,
            strength: Strength::Signature,
            evidence: Vec::new(),
            walked_to: None,
            stopped: None,
            facts: Map::new(),
            tracks: Vec::new(),
            capabilities: Vec::new(),
        }
    }

    /// Bytes that say so: `len` of them at `at`, which read as `is`.
    pub(crate) fn see(&mut self, at: u64, len: u64, is: impl Into<String>) {
        if self.evidence.len() < MAX_EVIDENCE {
            self.evidence.push((at, len, is.into()));
        }
    }

    pub(crate) fn fact(&mut self, k: &str, v: impl Into<Value>) {
        self.facts.insert(k.to_string(), v.into());
    }

    pub(crate) fn stop(&mut self, at: u64, why: &'static str) {
        self.stopped = Some((at, why));
    }

    pub(crate) fn cap(&mut self, c: &'static str) {
        if !self.capabilities.contains(&c) {
            self.capabilities.push(c);
        }
    }

    /// A track's or stream's row (§5), and the capability its codec is.
    pub(crate) fn track(&mut self, t: Track) {
        if let Some(c) = t.codec.and_then(capability_of) {
            self.cap(c);
        }
        if self.tracks.len() < MAX_TRACKS {
            self.tracks.push(t.value());
        }
    }

    fn value(&self) -> Value {
        let mut capabilities = self.capabilities.clone();
        capabilities.sort_unstable();
        json!({
            "id": self.id,
            "kind": self.kind,
            "strength": self.strength.label(),
            "evidence": self.evidence.iter().map(|(at, len, is)| json!({"at": at, "len": len, "is": is})).collect::<Vec<_>>(),
            "walked_to": self.walked_to,
            "stopped": self.stopped.map(|(at, why)| json!({"at": at, "why": why})),
            "facts": self.facts,
            "tracks": self.tracks,
            "capabilities": capabilities,
        })
    }
}

/// One track of a container, as the container states it (§5): what its samples hold is not read.
#[derive(Debug, Clone, Default)]
pub struct Track {
    pub track: Option<u64>,
    pub media: &'static str,
    pub stated: String,
    pub codec: Option<&'static str>,
    pub width: Option<u64>,
    pub height: Option<u64>,
    pub channels: Option<u64>,
    pub sample_rate: Option<u64>,
    pub protected: Option<Value>,
}

impl Track {
    fn value(&self) -> Value {
        json!({
            "track": self.track,
            "media": self.media,
            "stated": self.stated,
            "codec": self.codec,
            "width": self.width,
            "height": self.height,
            "channels": self.channels,
            "sample_rate": self.sample_rate,
            "protected": self.protected,
        })
    }
}

/// The registered capability whose payload a codec is, where there is one (§5.1).
pub fn capability_of(codec: &str) -> Option<&'static str> {
    Some(match codec {
        "avc" => CAP_VIDEO_AVC,
        "hevc" => CAP_VIDEO_HEVC,
        "vvc" => CAP_VIDEO_VVC,
        "av1" => CAP_VIDEO_AV1,
        "vp8" => CAP_VIDEO_VP8,
        "vp9" => CAP_VIDEO_VP9,
        "evc" => CAP_VIDEO_EVC,
        "lcevc" => CAP_VIDEO_LCEVC,
        "jpeg-xs" => CAP_VIDEO_JXS,
        "jpeg2000" => CAP_VIDEO_J2K,
        "aac" => CAP_AUDIO_AAC,
        "opus" => CAP_AUDIO_OPUS,
        "flac" => CAP_AUDIO_FLAC,
        "ac4" => CAP_AUDIO_AC4,
        "mpegh" => CAP_AUDIO_MPEGH,
        "iamf" => CAP_AUDIO_IAMF,
        _ => return None,
    })
}

/// Every codec a walk may name that has a capability, for the registry's test.
#[cfg(test)]
const CODECS: &[&str] = &["avc", "hevc", "vvc", "av1", "vp8", "vp9", "evc", "lcevc", "jpeg-xs", "jpeg2000", "aac", "opus", "flac", "ac4", "mpegh", "iamf"];

/// A format known only by its fixed bytes (§6): each of `parts` at its offset.
struct Row {
    id: &'static str,
    kind: &'static str,
    parts: &'static [(usize, &'static [u8])],
    caps: &'static [&'static str],
}

const fn row(id: &'static str, kind: &'static str, parts: &'static [(usize, &'static [u8])], caps: &'static [&'static str]) -> Row {
    Row { id, kind, parts, caps }
}

/// §6's table, in its order.
const ROWS: &[Row] = &[
    row("aiff", "audio", &[(0, b"FORM"), (8, b"AIFF")], &[]),
    row("aiff", "audio", &[(0, b"FORM"), (8, b"AIFC")], &[]),
    row("caf", "audio", &[(0, b"caff\x00\x01")], &[]),
    row("wavpack", "audio", &[(0, b"wvpk")], &[]),
    row("amr", "audio", &[(0, b"#!AMR\n")], &[]),
    row("amr-wb", "audio", &[(0, b"#!AMR-WB\n")], &[]),
    row("jpegxl", "image", &[(0, b"\xff\x0a")], &[CAP_IMAGE_JXL]),
    row("jpegxl", "image", &[(0, b"\x00\x00\x00\x0cJXL \x0d\x0a\x87\x0a")], &[CAP_IMAGE_JXL]),
    row("jp2", "image", &[(0, b"\x00\x00\x00\x0cjP  \x0d\x0a\x87\x0a")], &[CAP_IMAGE_JP2]),
    row("j2k", "image", &[(0, b"\xff\x4f\xff\x51")], &[CAP_IMAGE_JP2]),
    row("tiff", "image", &[(0, b"II*\x00")], &[]),
    row("tiff", "image", &[(0, b"MM\x00*")], &[]),
    row("bigtiff", "image", &[(0, b"II+\x00")], &[]),
    row("bigtiff", "image", &[(0, b"MM\x00+")], &[]),
    row("openexr", "image", &[(0, b"\x76\x2f\x31\x01")], &[]),
    row("dpx", "image", &[(0, b"SDPX")], &[]),
    row("dpx", "image", &[(0, b"XPDS")], &[]),
    row("radiance", "image", &[(0, b"#?RADIANCE\n")], &[]),
    row("radiance", "image", &[(0, b"#?RGBE\n")], &[]),
    row("mxf", "container", &[(0, b"\x06\x0e\x2b\x34\x02\x05\x01\x01\x0d\x01\x02")], &[]),
    row("asf", "container", &[(0, b"\x30\x26\xb2\x75\x8e\x66\xcf\x11\xa6\xd9\x00\xaa\x00\x62\xce\x6c")], &[]),
    row("mpeg-ps", "container", &[(0, b"\x00\x00\x01\xba")], &[]),
    row("mpeg-video", "elementary", &[(0, b"\x00\x00\x01\xb3")], &[]),
    row("webvtt", "text", &[(0, b"WEBVTT")], &[]),
    row("gzip", "compression", &[(0, b"\x1f\x8b\x08")], &[]),
    row("zstd", "compression", &[(0, b"\x28\xb5\x2f\xfd")], &[]),
    row("xz", "compression", &[(0, b"\xfd7zXZ\x00")], &[]),
    row("bzip2", "compression", &[(0, b"BZh")], &[]),
    row("zip", "compression", &[(0, b"PK\x03\x04")], &[]),
    row("tar", "compression", &[(257, b"ustar")], &[]),
    row("pcap", "capture", &[(0, b"\xd4\xc3\xb2\xa1")], &[]),
    row("pcap", "capture", &[(0, b"\xa1\xb2\xc3\xd4")], &[]),
    row("pcap", "capture", &[(0, b"\x4d\x3c\xb2\xa1")], &[]),
    row("pcap", "capture", &[(0, b"\xa1\xb2\x3c\x4d")], &[]),
    row("pcapng", "capture", &[(0, b"\x0a\x0d\x0d\x0a")], &[]),
];

/// A row of §6's table: its id, kind, fixed bytes at their offsets, and capabilities.
pub type SignatureRow = (&'static str, &'static str, Vec<(usize, &'static [u8])>, Vec<&'static str>);

/// §6's table, for the test that holds it to the extension.
pub fn signature_table() -> Vec<SignatureRow> {
    ROWS.iter().map(|r| (r.id, r.kind, r.parts.to_vec(), r.caps.to_vec())).collect()
}

/// §7's table, for the test that holds it to the extension.
pub fn embedded_table() -> Vec<(&'static str, &'static [u8], usize)> {
    measure::embeds()
}

/// The probes that can name a format from the bytes (§3.5). A probe of a format with no
/// signature (`wai.image.ieee1857_11`, `wai.meta.projection`) reads a payload declared to be one,
/// and says nothing of a file it is merely given.
#[cfg(feature = "format_probe")]
const IDENTIFYING: &[&str] = &[CAP_IMAGE_JXS, CAP_VIDEO_JXS, CAP_VIDEO_HEVC, CAP_VIDEO_MVHEVC, CAP_VIDEO_VVC, CAP_VIDEO_LCEVC, CAP_AUDIO_MPEGH, CAP_VIDEO_AVS3, CAP_AUDIO_AVS3];

fn wai_envelope(src: &dyn Source, head: &[u8]) -> io::Result<Option<Candidate>> {
    let version = match head.get(..4) {
        Some(b"WAI1") => 1,
        Some(b"WAI2") => 2,
        _ => return Ok(None),
    };
    let mut c = Candidate::new("wai", "wai");
    c.see(0, 4, format!("magic:WAI{version}"));
    c.fact("version", version);
    let Some(ml) = le32(head, 4) else {
        c.walked_to = Some(0);
        c.stop(0, "overrun");
        return Ok(Some(c));
    };
    let manifest = get(src, 8, ml.min(HEAD as u64) as usize)?;
    if ml > HEAD as u64 || (manifest.len() as u64) < ml {
        c.walked_to = Some(8);
        c.stop(8, if ml > HEAD as u64 { "limit" } else { "overrun" });
        return Ok(Some(c));
    }
    let Ok(m) = serde_json::from_slice::<Value>(&manifest) else {
        c.walked_to = Some(8);
        c.stop(8, "invalid");
        return Ok(Some(c));
    };
    c.see(8, ml, "manifest");
    let mut pos = 8 + ml;
    if version == 1 {
        let cap = m.pointer("/model_requirement/capability").and_then(Value::as_str);
        c.fact("capability", cap.map(|s| clean(s.as_bytes())));
        let pl = get(src, pos, 4)?;
        let Some(pl) = le32(&pl, 0) else {
            c.walked_to = Some(pos);
            c.stop(pos, "overrun");
            return Ok(Some(c));
        };
        c.see(pos, 4, "payload_length");
        if pos + 4 + pl > src.size() {
            c.walked_to = Some(pos);
            c.stop(pos, "overrun");
            return Ok(Some(c));
        }
        pos += 4 + pl;
    } else {
        let n = get(src, pos, 2)?;
        let Some(n) = le16(&n, 0) else {
            c.walked_to = Some(pos);
            c.stop(pos, "overrun");
            return Ok(Some(c));
        };
        c.see(pos, 2, "rendition_count");
        c.fact("renditions", n);
        pos += 2;
    }
    c.walked_to = Some(pos);
    c.strength = Strength::Structure;
    Ok(Some(c))
}

fn table(head: &[u8]) -> Vec<Candidate> {
    let mut out: Vec<Candidate> = Vec::new();
    for r in ROWS {
        if !r.parts.iter().all(|(at, bytes)| head.get(*at..at + bytes.len()) == Some(*bytes)) {
            continue;
        }
        let mut c = Candidate::new(r.id, r.kind);
        for (at, bytes) in r.parts {
            c.see(*at as u64, bytes.len() as u64, "signature");
        }
        for k in r.caps {
            c.cap(k);
        }
        out.push(c);
    }
    out
}

/// A probe's reading beside a walk of the same format (§3.5): a candidate of the walk is raised
/// to `parameters`; an accepted probe no walk raised is a candidate of its own.
#[cfg(feature = "format_probe")]
fn probes(head: &[u8], found: &mut Vec<Candidate>) -> Vec<Value> {
    let mut accepted = Vec::new();
    for &cap in IDENTIFYING {
        match crate::format_probe::probe(cap, head) {
            Ok(params) => {
                accepted.push(json!(cap));
                let walked = found.iter_mut().find(|c| {
                    c.kind == "elementary" && (c.capabilities.contains(&cap) || (cap == CAP_VIDEO_MVHEVC && c.capabilities.contains(&CAP_VIDEO_HEVC)))
                });
                match walked {
                    Some(c) => {
                        c.strength = Strength::Parameters;
                        c.cap(cap);
                        let mut p = c.facts.remove("probe").and_then(|v| v.as_object().cloned()).unwrap_or_default();
                        p.insert(cap.to_string(), params);
                        c.fact("probe", Value::Object(p));
                    }
                    None => {
                        let mut c = Candidate::new(cap, "elementary");
                        c.strength = Strength::Parameters;
                        c.see(0, head.len() as u64, "probe");
                        c.cap(cap);
                        c.fact("probe", json!({cap: params}));
                        found.push(c);
                    }
                }
            }
            Err(crate::format_probe::Refusal::NoSignature) => {}
            Err(refusal) => {
                if let Some(c) = found.iter_mut().find(|c| c.kind == "elementary" && c.capabilities.contains(&cap)) {
                    let mut r = c.facts.remove("probe_refused").and_then(|v| v.as_object().cloned()).unwrap_or_default();
                    r.insert(cap.to_string(), json!(refusal.code()));
                    c.fact("probe_refused", Value::Object(r));
                }
            }
        }
    }
    accepted
}

#[cfg(not(feature = "format_probe"))]
fn probes(_: &[u8], _: &mut Vec<Candidate>) -> Vec<Value> {
    Vec::new()
}

/// Every format the bytes fit, in the order of the sections that define them, and the probes that
/// accepted them.
fn candidates(src: &dyn Source, head: &[u8]) -> io::Result<(Vec<Candidate>, Vec<Value>)> {
    let size = src.size();
    let mut found: Vec<Candidate> = Vec::new();
    let mut add = |c: Option<Candidate>| found.extend(c);
    add(wai_envelope(src, head)?);
    add(iso::walk(src, head)?);
    add(matroska::walk(src, head)?);
    add(ts::walk(head, size));
    add(audio::ogg(src, head)?);
    add(audio::flac(src, head)?);
    add(audio::riff(src, head)?);
    add(audio::mpeg_audio(head, size));
    add(audio::adts(head, size));
    add(image::png(src, head)?);
    add(image::jpeg(src, head)?);
    add(image::gif(head, size));
    add(stream::ivf(src, head)?);
    add(stream::y4m(src, head)?);
    add(stream::flv(src, head)?);
    add(stream::av1(head, size));
    add(text::hls(head));
    add(text::dash(head));
    found.extend(stream::annex_b(head, size));
    found.extend(table(head));
    let accepted = probes(head, &mut found);
    // A length may point past the file; what a candidate says of the file may not (§3.3).
    for c in &mut found {
        c.walked_to = c.walked_to.map(|at| at.min(size));
        c.stopped = c.stopped.map(|(at, why)| (at.min(size), why));
    }
    Ok((found, accepted))
}

/// What `src` is (media-identify §2).
pub fn identify(src: &dyn Source) -> io::Result<Value> {
    let size = src.size();
    let scan = get(src, 0, SCAN.min(usize::try_from(size).unwrap_or(SCAN)))?;
    let head = &scan[..scan.len().min(HEAD)];
    let (mut found, accepted) = candidates(src, head)?;

    // Strongest first; within a strength, in the order of the sections that define them.
    found.sort_by_key(|c| std::cmp::Reverse(c.strength));
    let top = found.first().map(|c| c.strength);
    let at_top = found.iter().filter(|c| Some(c.strength) == top).count();
    let verdict = match (top, at_top) {
        (None, _) => "unknown",
        (Some(_), n) if n > 1 => "ambiguous",
        (Some(Strength::Signature), _) => "signature_only",
        _ => "identified",
    };

    let (embedded, embedded_capped) = measure::embedded(&scan);
    Ok(json!({
        "format": "wai.identify/1",
        "size": size,
        "verdict": verdict,
        "candidates": found.iter().map(Candidate::value).collect::<Vec<_>>(),
        "probes_accepted": accepted,
        "embedded": embedded,
        "embedded_capped": embedded_capped,
        "measures": measure::measures(&scan),
    }))
}

/// [`identify`] of bytes in memory, which cannot fail to be read.
pub fn identify_bytes(b: &[u8]) -> Value {
    identify(&b).expect("bytes in memory read")
}

/// What a person reads (`wai identify` without `--json`).
pub fn render(path: &str, v: &Value) -> String {
    let mut s = format!("wai identify — {path}\n  size:     {} B\n  verdict:  {}\n", v["size"], v["verdict"].as_str().unwrap_or(""));
    for c in v["candidates"].as_array().into_iter().flatten() {
        s.push_str(&format!("  {} ({}, {})", c["id"].as_str().unwrap_or(""), c["kind"].as_str().unwrap_or(""), c["strength"].as_str().unwrap_or("")));
        if let Some(n) = c["walked_to"].as_u64() {
            s.push_str(&format!(", walked to {n}"));
        }
        if let Some(why) = c["stopped"]["why"].as_str() {
            s.push_str(&format!(", stopped at {}: {why}", c["stopped"]["at"]));
        }
        s.push('\n');
        for (k, f) in c["facts"].as_object().into_iter().flatten() {
            s.push_str(&format!("    {k}: {f}\n"));
        }
        for t in c["tracks"].as_array().into_iter().flatten() {
            s.push_str(&format!("    {} {} ({})", t["media"].as_str().unwrap_or(""), t["stated"].as_str().unwrap_or(""), t["codec"].as_str().unwrap_or("not known here")));
            if let (Some(w), Some(h)) = (t["width"].as_u64(), t["height"].as_u64()) {
                s.push_str(&format!(" {w}x{h}"));
            }
            if let (Some(n), Some(r)) = (t["channels"].as_u64(), t["sample_rate"].as_u64()) {
                s.push_str(&format!(" {n} channels {r} Hz"));
            }
            if !t["protected"].is_null() {
                s.push_str(&format!(" protected {}", t["protected"]));
            }
            s.push('\n');
        }
        let caps: Vec<&str> = c["capabilities"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
        if !caps.is_empty() {
            s.push_str(&format!("    capabilities: {}\n", caps.join(", ")));
        }
        for e in c["evidence"].as_array().into_iter().flatten() {
            s.push_str(&format!("    @{} +{} {}\n", e["at"], e["len"], e["is"].as_str().unwrap_or("")));
        }
    }
    for e in v["embedded"].as_array().into_iter().flatten() {
        s.push_str(&format!("  embedded at {}: {}\n", e["at"], e["id"].as_str().unwrap_or("")));
    }
    let m = &v["measures"];
    s.push_str(&format!(
        "  measures over {} B: {} distinct bytes, chi-square {}, {} of {} windows uniform, text {}\n",
        m["scanned"], m["distinct_bytes"], m["chi_square"], m["uniform_windows"], m["windows"], m["text"]
    ));
    for p in m["periods"].as_array().into_iter().flatten() {
        s.push_str(&format!("  repeats every {} B (z {})\n", p["lag"], p["z"]));
    }
    s
}

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

    #[test]
    fn every_codec_named_has_a_registered_capability() {
        for c in CODECS {
            let cap = capability_of(c).unwrap_or_else(|| panic!("{c}"));
            assert!(is_known_capability(cap), "{cap}");
        }
        for r in ROWS {
            for k in r.caps {
                assert!(is_known_capability(k), "{k}");
            }
        }
    }

    #[test]
    fn nothing_is_unknown_as_something_and_empty_is_unknown() {
        let v = identify_bytes(&[]);
        assert_eq!(v["verdict"], "unknown");
        assert_eq!(v["candidates"].as_array().unwrap().len(), 0);
        assert_eq!(v["measures"]["scanned"], 0);
    }

    #[test]
    fn a_file_is_read_the_same_as_its_bytes() {
        let dir = std::env::temp_dir().join(format!("wai-identify-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let p = dir.join("x.bin");
        let bytes: Vec<u8> = (0..17_500u32).flat_map(|i| i.to_le_bytes()).collect();
        std::fs::write(&p, &bytes).unwrap();
        let f = FileSource::open(&p).unwrap();
        assert_eq!(identify(&f).unwrap(), identify_bytes(&bytes));
        std::fs::remove_dir_all(&dir).unwrap();
    }

    fn spec_table(header: &str) -> Vec<Vec<String>> {
        let text = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../extensions/media-identify.md")).unwrap();
        let at = text.find(&format!("\n{header}\n")).unwrap_or_else(|| panic!("{header}")) + 1;
        text[at..]
            .lines()
            .skip(2)
            .take_while(|l| l.starts_with('|'))
            .map(|l| l.trim().trim_matches('|').split('|').map(|c| c.trim().replace('`', "")).collect())
            .collect()
    }

    /// A table of `width`-column records set side by side, as records.
    fn records(rows: Vec<Vec<String>>, width: usize) -> Vec<Vec<String>> {
        rows.into_iter().flat_map(|r| r.chunks(width).map(<[String]>::to_vec).collect::<Vec<_>>()).filter(|r| !r[0].is_empty()).collect()
    }

    fn named(s: &str) -> Option<&str> {
        (s != "null").then_some(s)
    }

    #[test]
    fn the_walks_name_codecs_as_section_5s_tables_do() {
        for r in records(spec_table("| entry | codec | entry | codec |"), 2) {
            for entry in r[0].split_whitespace() {
                assert_eq!(iso::codec(entry.as_bytes()), Some(r[1].as_str()), "§5.3 {entry}");
            }
        }
        for r in records(spec_table("| identifier | codec | identifier | codec |"), 2) {
            assert_eq!(matroska::codec(r[0].as_bytes()), Some(r[1].as_str()), "§5.4 {}", r[0]);
        }
        for r in records(spec_table("| type | codec | media | type | codec | media |"), 3) {
            for t in r[0].split_whitespace() {
                let ty = u8::from_str_radix(t.trim_start_matches("0x"), 16).unwrap();
                assert_eq!((ts::codec(ty), ts::media(ty)), (named(&r[1]), r[2].as_str()), "§5.5 {t}");
            }
        }
    }

    #[test]
    fn mpeg_audio_bitrates_and_adts_rates_are_section_5s() {
        for r in spec_table("| | indexes 1 to 14 |") {
            let version: u8 = if r[0].starts_with("version 1,") { 3 } else { 2 };
            let layers: Vec<u8> = [("III", 1u8), ("II", 2), ("I", 3)]
                .iter()
                .filter(|(w, _)| r[0].split(|c: char| !c.is_alphanumeric()).any(|x| x == *w))
                .map(|&(_, l)| l)
                .collect();
            assert!(!layers.is_empty(), "{}", r[0]);
            for layer in layers {
                for (i, kbps) in r[1].split_whitespace().enumerate() {
                    let h = [0xff, 0xe0 | (version << 3) | (layer << 1) | 1, ((i as u8 + 1) << 4), 0];
                    let f = audio::mpeg_frame(&h).unwrap_or_else(|| panic!("{} index {}", r[0], i + 1));
                    assert_eq!(f.2, kbps.parse::<u64>().unwrap(), "{} index {}", r[0], i + 1);
                }
            }
        }
        let text = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../extensions/media-identify.md")).unwrap();
        let at = text.find("rate index (below 13: ").unwrap() + "rate index (below 13: ".len();
        let listed: Vec<u64> = text[at..at + text[at..].find(')').unwrap()].split(|c: char| c == ',' || c.is_whitespace()).filter(|x| !x.is_empty()).map(|x| x.parse().unwrap()).collect();
        assert_eq!(listed, audio::RATES.to_vec());
        let at = text.find("a `profile_idc`").unwrap();
        let from = at + text[at..].find("other than ").unwrap() + "other than ".len();
        let listed: Vec<u64> = text[from..from + text[from..].find(" or a failed").unwrap()]
            .split(|c: char| c == ',' || c.is_whitespace())
            .filter(|x| !x.is_empty() && *x != "or")
            .map(|x| x.parse().unwrap())
            .collect();
        assert_eq!(listed, stream::AVC_PROFILES.to_vec(), "§5.20's profiles");
    }

    /// The beginnings of the formats a walk reads, for the tests that break them.
    const STARTS: [&[u8]; 18] = [
        b"\x00\x00\x00\x18ftypisom\x00\x00\x00\x00isom\x00\x00\x00\x08free\x00\x00\x00\x20moov\x00\x00\x00\x18trak\x00\x00\x00\x10mdia\x00\x00\x00\x08hdlr",
        b"\x1a\x45\xdf\xa3\x88\x42\x82\x84webm\x18\x53\x80\x67\xff\x16\x54\xae\x6b\x8a\xae\x88\xd7\x81\x01\x83\x81\x01",
        b"OggS\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x13OpusHead",
        b"fLaC\x80\x00\x00\x22\x10\x00\x10\x00\x00\x00\x00\x00\x00\x00\x0a\xc4\x42\xf0",
        b"RIFF\x24\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x02\x00\x44\xac\x00\x00",
        b"RIFF\x24\x00\x00\x00WEBPVP8X\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x01\x00\x00",
        b"ID3\x04\x00\x00\x00\x00\x00\x00\xff\xfb\x90\x00\x00\x00\x00\x00",
        b"\xff\xf1\x50\x80\x01\x00\xfc\xff\xf1\x50\x80\x01\x00\xfc",
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\x0dIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x00\x00\x00\x00",
        b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xc0\x00\x0b\x08\x00\x01\x00\x01\x01",
        b"GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\xff\xff\xff\x21\xf9\x04\x00\x00\x00\x00\x00\x2c\x00\x00",
        b"DKIF\x00\x00\x20\x00AV01\x10\x00\x10\x00\x1e\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00",
        b"YUV4MPEG2 W2 H2 F30:1 C420\nFRAME\n\x00\x00\x00\x00\x00\x00",
        b"FLV\x01\x05\x00\x00\x00\x09\x00\x00\x00\x00\x09\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x17",
        b"\x12\x00\x0a\x0b\x00\x00\x00\x04\x3c\xff\xbc\xfb\x80\x20\x00",
        b"\x00\x00\x00\x01\x67\x42\xc0\x1e\xd9\x00\xa0\x47\xfe\xc8\x00\x00\x00\x01\x68\xce\x3c\x80",
        b"#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-STREAM-INF:CODECS=\"hvc1\",METHOD=\n",
        b"<?xml version=\"1.0\"?><MPD type=\"static\" xmlns=\"urn:mpeg:dash:schema:mpd:2011\"><Period codecs=\"",
    ];

    fn fits(b: &[u8]) {
        let (found, _) = candidates(&b, &b[..b.len().min(HEAD)]).unwrap();
        // A walk stays inside the file it read.
        for c in &found {
            if let Some(at) = c.walked_to {
                assert!(at <= b.len() as u64, "{} walked to {at} of {}", c.id, b.len());
            }
            let _ = c.value();
        }
    }

    #[test]
    fn no_prefix_of_a_format_and_no_flipped_byte_makes_a_walk_panic() {
        for s in STARTS {
            for n in 0..=s.len() {
                fits(&s[..n]);
                for i in 0..n {
                    let mut b = s[..n].to_vec();
                    b[i] ^= 0xff;
                    fits(&b);
                }
            }
        }
    }

    #[test]
    fn hostile_bytes_make_no_walk_panic_or_run_past_the_file() {
        // Deterministic: bytes a parser's edge cases are made of, over each beginning.
        let mut x = 0x2545_f491_4f6c_dd1du64;
        let mut next = move || {
            x ^= x << 13;
            x ^= x >> 7;
            x ^= x << 17;
            x
        };
        const EDGE: [u8; 12] = [0, 0, 0, 1, 0xff, 0x7f, 0x80, 0x47, 0x01, 0xfe, 0x10, 0xb0];
        for round in 0..40_000u32 {
            let s = STARTS[round as usize % STARTS.len()];
            let keep = next() as usize % (s.len() + 1);
            let mut b = s[..keep].to_vec();
            for _ in 0..next() as usize % 96 {
                let r = next();
                b.push(if r & 1 == 0 { EDGE[(r >> 8) as usize % EDGE.len()] } else { (r >> 16) as u8 });
            }
            fits(&b);
        }
    }

    #[test]
    fn a_stream_cut_by_the_head_is_not_called_cut_short_by_the_file() {
        // Five packets and a file longer than the bytes seen: the walk says it stopped at its limit.
        let mut b = Vec::new();
        for _ in 0..6 {
            let mut p = vec![0x47u8, 0x1f, 0xff, 0x10];
            p.resize(188, 0xff);
            b.extend_from_slice(&p);
        }
        let c = ts::walk(&b, b.len() as u64 + 1).unwrap();
        assert_eq!(c.stopped.map(|(_, why)| why), Some("limit"));
        let c = ts::walk(&b, b.len() as u64).unwrap();
        assert_eq!(c.stopped, None);
    }
}
