// 一致性语料:识别行为的单一事实来源。
//
// `moon test` 与 `cmd/verify` 共用同一份用例,避免测试与自检脚本漂移。
//
// 这里刻意用「构造器」生成结构合法的容器(ZIP 中央目录、RIFF chunk、
// ISO-BMFF box、EBML vint、Ogg 页、tar header、PE 头),
// 而不是只写几个魔数字节——否则测的只是魔数表,而不是解析能力。
///|
priv struct Case {
label : String
expected : String
data : Bytes
}
///|
fn hex_value(c : Char) -> Int {
if c >= '0' && c <= '9' {
c.to_int() - 48
} else if c >= 'a' && c <= 'f' {
c.to_int() - 87
} else if c >= 'A' && c <= 'F' {
c.to_int() - 55
} else {
-1
}
}
///|
fn bytes_of_hex(hex : String) -> Bytes {
let out : Array[Byte] = []
let chars = hex.iter().to_array()
let mut i = 0
while i + 1 < chars.length() {
let hi = hex_value(chars[i])
let lo = hex_value(chars[i + 1])
if hi < 0 || lo < 0 {
break
}
out.push((hi * 16 + lo).to_byte())
i = i + 2
}
Bytes::from_array(out.exact_view())
}
///|
fn ascii_bytes(s : String) -> Bytes {
let out : Array[Byte] = []
for c in s {
out.push(c.to_int().to_byte())
}
Bytes::from_array(out.exact_view())
}
///|
fn concat_bytes(a : Bytes, b : Bytes) -> Bytes {
let out : Array[Byte] = []
let mut i = 0
while i < a.length() {
out.push(a[i])
i = i + 1
}
let mut j = 0
while j < b.length() {
out.push(b[j])
j = j + 1
}
Bytes::from_array(out.exact_view())
}
///|
fn bytes_to_array(data : Bytes) -> Array[Byte] {
let out : Array[Byte] = []
let mut i = 0
while i < data.length() {
out.push(data[i])
i = i + 1
}
out
}
///|
fn push_u16_le(out : Array[Byte], value : Int) -> Unit {
out.push((value % 256).to_byte())
out.push((value / 256 % 256).to_byte())
}
///|
fn push_u32_le(out : Array[Byte], value : Int) -> Unit {
out.push((value % 256).to_byte())
out.push((value / 256 % 256).to_byte())
out.push((value / 65536 % 256).to_byte())
out.push((value / 16777216 % 256).to_byte())
}
///|
fn push_u32_be(out : Array[Byte], value : Int) -> Unit {
out.push((value / 16777216 % 256).to_byte())
out.push((value / 65536 % 256).to_byte())
out.push((value / 256 % 256).to_byte())
out.push((value % 256).to_byte())
}
///|
fn push_ascii(out : Array[Byte], text : String) -> Unit {
for c in text {
out.push(c.to_int().to_byte())
}
}
///|
fn repeat_ascii(text : String, count : Int) -> String {
let builder = StringBuilder()
let mut i = 0
while i < count {
builder.write_string(text)
i = i + 1
}
builder.to_string()
}
// ---------------------------------------------------------------------------
// 容器构造器
// ---------------------------------------------------------------------------
///|
/// 构造结构合法的 ZIP(全部 stored,带中央目录与 EOCD)。
fn zip_of(entries : Array[(String, String)]) -> Bytes {
let out : Array[Byte] = []
let local_offsets : Array[Int] = []
for entry in entries {
let (name, content) = entry
let name_bytes = ascii_bytes(name)
let data_bytes = ascii_bytes(content)
local_offsets.push(out.length())
// local file header
push_u32_le(out, 0x04034b50)
push_u16_le(out, 20)
push_u16_le(out, 0)
push_u16_le(out, 0)
push_u16_le(out, 0)
push_u16_le(out, 0)
push_u32_le(out, 0)
push_u32_le(out, data_bytes.length())
push_u32_le(out, data_bytes.length())
push_u16_le(out, name_bytes.length())
push_u16_le(out, 0)
push_ascii(out, name)
for b in bytes_to_array(data_bytes) {
out.push(b)
}
}
let cd_start = out.length()
let mut i = 0
while i < entries.length() {
let (name, content) = entries[i]
let name_bytes = ascii_bytes(name)
let data_bytes = ascii_bytes(content)
push_u32_le(out, 0x02014b50)
push_u16_le(out, 20)
push_u16_le(out, 20)
push_u16_le(out, 0)
push_u16_le(out, 0)
push_u16_le(out, 0)
push_u16_le(out, 0)
push_u32_le(out, 0)
push_u32_le(out, data_bytes.length())
push_u32_le(out, data_bytes.length())
push_u16_le(out, name_bytes.length())
push_u16_le(out, 0)
push_u16_le(out, 0)
push_u16_le(out, 0)
push_u16_le(out, 0)
push_u32_le(out, 0)
push_u32_le(out, local_offsets[i])
push_ascii(out, name)
i = i + 1
}
let cd_size = out.length() - cd_start
push_u32_le(out, 0x06054b50)
push_u16_le(out, 0)
push_u16_le(out, 0)
push_u16_le(out, entries.length())
push_u16_le(out, entries.length())
push_u32_le(out, cd_size)
push_u32_le(out, cd_start)
push_u16_le(out, 0)
Bytes::from_array(out.exact_view())
}
///|
fn riff_bytes(tag : String, form : String, chunks : Array[(String, Int)]) -> Bytes {
let body : Array[Byte] = []
for chunk in chunks {
let (id, size) = chunk
push_ascii(body, id)
push_u32_le(body, size)
let mut i = 0
while i < size {
body.push((0).to_byte())
i = i + 1
}
if size % 2 == 1 {
body.push((0).to_byte())
}
}
let out : Array[Byte] = []
push_ascii(out, tag)
push_u32_le(out, 4 + body.length())
push_ascii(out, form)
for b in body {
out.push(b)
}
Bytes::from_array(out.exact_view())
}
///|
fn bmff_box(typ : String, payload : Bytes) -> Bytes {
let out : Array[Byte] = []
push_u32_be(out, 8 + payload.length())
push_ascii(out, typ)
for b in bytes_to_array(payload) {
out.push(b)
}
Bytes::from_array(out.exact_view())
}
///|
fn ftyp_bytes(major : String) -> Bytes {
let out : Array[Byte] = []
push_u32_be(out, 16)
push_ascii(out, "ftyp")
push_ascii(out, major)
push_u32_be(out, 0)
Bytes::from_array(out.exact_view())
}
///|
fn hdlr_bytes(handler : String) -> Bytes {
let out : Array[Byte] = []
push_u32_be(out, 0)
push_u32_be(out, 0)
push_ascii(out, handler)
push_u32_be(out, 0)
push_u32_be(out, 0)
push_u32_be(out, 0)
out.push((0).to_byte())
Bytes::from_array(out.exact_view())
}
///|
/// ftyp + moov/trak/mdia/hdlr,用来验证真正的 box 树下钻。
fn bmff_with_handler(brand : String, handler : String) -> Bytes {
let hdlr = bmff_box("hdlr", hdlr_bytes(handler))
let mdia = bmff_box("mdia", hdlr)
let trak = bmff_box("trak", mdia)
let moov = bmff_box("moov", trak)
concat_bytes(concat_bytes(ftyp_bytes(brand), moov), b"")
}
///|
/// EBML 头 + DocType 元素(vint 编码)。
fn ebml_bytes(doc_type : String) -> Bytes {
let payload : Array[Byte] = []
payload.push((66).to_byte()) // 0x42
payload.push((130).to_byte()) // 0x82
// DocType 长度用 1 字节 vint:0x80 | len
payload.push((128 + doc_type.length()).to_byte())
push_ascii(payload, doc_type)
let out : Array[Byte] = []
out.push((26).to_byte()) // EBML ID 1A
out.push((69).to_byte()) // 45
out.push((223).to_byte()) // DF
out.push((163).to_byte()) // A3
out.push((128 + payload.length()).to_byte())
for b in payload {
out.push(b)
}
Bytes::from_array(out.exact_view())
}
///|
fn tar_bytes() -> Bytes {
let out : Array[Byte] = []
let mut i = 0
while i < 512 {
out.push((0).to_byte())
i = i + 1
}
let name = ascii_bytes("sample.txt")
let mut j = 0
while j < name.length() {
out[j] = name[j]
j = j + 1
}
// mode = 0000644
let mode = ascii_bytes("0000644")
let mut k = 0
while k < mode.length() {
out[100 + k] = mode[k]
k = k + 1
}
// size = 00000000000
let size = ascii_bytes("00000000000")
let mut s = 0
while s < size.length() {
out[124 + s] = size[s]
s = s + 1
}
// magic "ustar\0" + version "00"
let magic = ascii_bytes("ustar")
let mut m = 0
while m < magic.length() {
out[257 + m] = magic[m]
m = m + 1
}
out[262] = (0).to_byte()
out[263] = (48).to_byte()
out[264] = (48).to_byte()
// chksum:先按空格计算,再写回 6 位八进制 + NUL + 空格
let mut sum = 0
let mut c = 0
while c < 512 {
if c >= 148 && c < 156 {
sum = sum + 32
} else {
sum = sum + out[c].to_int()
}
c = c + 1
}
let digits : Array[Int] = []
let mut value = sum
let mut d = 0
while d < 6 {
digits.push(value % 8)
value = value / 8
d = d + 1
}
let mut e = 0
while e < 6 {
out[148 + e] = (48 + digits[5 - e]).to_byte()
e = e + 1
}
out[154] = (0).to_byte()
out[155] = (32).to_byte()
Bytes::from_array(out.exact_view())
}
///|
fn pe_bytes() -> Bytes {
let out : Array[Byte] = []
let mut i = 0
while i < 512 {
out.push((0).to_byte())
i = i + 1
}
// MZ
out[0] = (77).to_byte()
out[1] = (90).to_byte()
// e_lfanew = 128(小端)
out[60] = (128).to_byte()
out[61] = (0).to_byte()
out[62] = (0).to_byte()
out[63] = (0).to_byte()
// PE\0\0
out[128] = (80).to_byte()
out[129] = (69).to_byte()
// machine = 0x8664 (x86-64)
out[132] = (0x64).to_byte()
out[133] = (0x86).to_byte()
// numberOfSections = 3
out[134] = (3).to_byte()
// characteristics = 0x0022
out[150] = (0x22).to_byte()
// optional header magic = 0x20b (PE32+)
out[152] = (0x0b).to_byte()
out[153] = (0x02).to_byte()
// subsystem = 3(console)
out[220] = (3).to_byte()
Bytes::from_array(out.exact_view())
}
///|
fn iso_bytes() -> Bytes {
let out : Array[Byte] = []
let mut i = 0
while i < 32774 {
out.push((0).to_byte())
i = i + 1
}
out[32769] = (67).to_byte()
out[32770] = (68).to_byte()
out[32771] = (48).to_byte()
out[32772] = (48).to_byte()
out[32773] = (49).to_byte()
Bytes::from_array(out.exact_view())
}
///|
fn ts_bytes() -> Bytes {
let out : Array[Byte] = []
let mut i = 0
while i < 377 {
out.push((0).to_byte())
i = i + 1
}
out[0] = (71).to_byte()
out[188] = (71).to_byte()
out[376] = (71).to_byte()
Bytes::from_array(out.exact_view())
}
///|
fn mobi_bytes() -> Bytes {
let out : Array[Byte] = []
let mut i = 0
while i < 60 {
out.push((0).to_byte())
i = i + 1
}
for c in "BOOKMOBI" {
out.push(c.to_int().to_byte())
}
Bytes::from_array(out.exact_view())
}
///|
/// 结构合法的 Ogg 页:page_segments = 1,首段长度 19,首包以 OpusHead 开头。
fn ogg_opus_bytes() -> Bytes {
let out : Array[Byte] = []
push_ascii(out, "OggS")
let mut i = 0
while i < 22 {
out.push((0).to_byte())
i = i + 1
}
out.push((1).to_byte()) // 偏移 26:page_segments
out.push((19).to_byte()) // 偏移 27:首段长度
push_ascii(out, "OpusHead")
let mut j = 0
while j < 11 {
out.push((0).to_byte())
j = j + 1
}
Bytes::from_array(out.exact_view())
}
///|
fn set_u16_le(out : Array[Byte], offset : Int, value : Int) -> Unit {
out[offset] = (value % 256).to_byte()
out[offset + 1] = (value / 256 % 256).to_byte()
}
///|
fn set_u32_le(out : Array[Byte], offset : Int, value : Int) -> Unit {
out[offset] = (value % 256).to_byte()
out[offset + 1] = (value / 256 % 256).to_byte()
out[offset + 2] = (value / 65536 % 256).to_byte()
out[offset + 3] = (value / 16777216 % 256).to_byte()
}
///|
/// 写一个 128 字节的 OLE2 目录项:UTF-16LE 名称 + 名称长度 + 类型。
fn write_dir_entry(
out : Array[Byte],
base : Int,
name : String,
kind : Int,
) -> Unit {
let mut i = 0
while i < 128 {
out[base + i] = (0).to_byte()
i = i + 1
}
let name_bytes : Array[Byte] = []
for c in name {
let code = c.to_int()
name_bytes.push((code % 256).to_byte())
name_bytes.push((code / 256 % 256).to_byte())
}
let mut j = 0
while j < name_bytes.length() && j < 64 {
out[base + j] = name_bytes[j]
j = j + 1
}
// 名称长度包含结尾的 NUL(2 字节)
set_u16_le(out, base + 64, name_bytes.length() + 2)
out[base + 66] = kind.to_byte()
}
///|
/// 构造结构合法的 OLE2 复合文档:
/// 512 字节 header + 1 个 FAT 扇区 + 1 个目录扇区,目录里放一个指定名称的流。
fn ole2_bytes(stream_name : String) -> Bytes {
let out : Array[Byte] = []
let mut i = 0
while i < 1536 {
out.push((0).to_byte())
i = i + 1
}
// 头部 magic
out[0] = (0xd0).to_byte()
out[1] = (0xcf).to_byte()
out[2] = (0x11).to_byte()
out[3] = (0xe0).to_byte()
out[4] = (0xa1).to_byte()
out[5] = (0xb1).to_byte()
out[6] = (0x1a).to_byte()
out[7] = (0xe1).to_byte()
set_u16_le(out, 26, 3) // major version
set_u16_le(out, 28, 0xfffe) // byte order
set_u16_le(out, 30, 9) // sector shift -> 512
set_u16_le(out, 32, 6) // mini sector shift
set_u32_le(out, 44, 1) // FAT 扇区数
set_u32_le(out, 48, 1) // 目录起始扇区
set_u32_le(out, 56, 4096) // mini stream cutoff
// 60..76:mini FAT / DIFAT 均置为 ENDOFCHAIN 或 0
out[60] = (0xfe).to_byte()
out[61] = (0xff).to_byte()
out[62] = (0xff).to_byte()
out[63] = (0xff).to_byte()
out[68] = (0xfe).to_byte()
out[69] = (0xff).to_byte()
out[70] = (0xff).to_byte()
out[71] = (0xff).to_byte()
set_u32_le(out, 76, 0) // DIFAT[0] = FAT 扇区 0
// 其余 DIFAT 位置填 FREESECT
let mut d = 80
while d < 512 {
out[d] = (0xff).to_byte()
d = d + 1
}
// 扇区 0:FAT
out[512] = (0xfd).to_byte() // [0] = FATSECT
out[513] = (0xff).to_byte()
out[514] = (0xff).to_byte()
out[515] = (0xff).to_byte()
out[516] = (0xfe).to_byte() // [1] = ENDOFCHAIN(目录链到此结束)
out[517] = (0xff).to_byte()
out[518] = (0xff).to_byte()
out[519] = (0xff).to_byte()
let mut f = 520
while f < 1024 {
out[f] = (0xff).to_byte()
f = f + 1
}
// 扇区 1:目录(4 个 128 字节目录项)
write_dir_entry(out, 1024, "Root Entry", 5)
write_dir_entry(out, 1152, stream_name, 2)
Bytes::from_array(out.exact_view())
}
///|
fn case_hex(label : String, expected : String, hex : String) -> Case {
{ label, expected, data: bytes_of_hex(hex), }
}
///|
fn make_cases() -> Array[Case] {
let cases : Array[Case] = [
// 图片
case_hex("png", "png", "89504e470d0a1a0a0000000d49484452"),
case_hex("jpeg", "jpeg", "ffd8ffe000104a46494600010100000100010000"),
case_hex("gif87a", "gif", "47494638376101000100"),
case_hex("gif89a", "gif", "47494638396101000100"),
case_hex("tiff-le", "tiff", "49492a000800000000000000"),
case_hex("tiff-be", "tiff", "4d4d002a0000000800000000"),
case_hex("psd", "psd", "384250530001000000000000"),
case_hex("xcf", "xcf", "67696d70207863662000"),
case_hex("ico", "ico", "000001000100101000000000"),
case_hex("bmp", "bmp", "424d3600000000000000"),
// BMP 结构有效:保留字段为 0,DIB header = 40
case_hex("bmp-dib40", "bmp", "424d360000000000000036000000280000000100000001000000010018000000000000000000000000000000000000"),
case_hex("jp2", "jp2", "0000000c6a5020200d0a870a00000000"),
case_hex("jxl", "jxl", "0000000c4a584c200d0a870a00000000"),
case_hex("icns", "icns", "69636e7300000000"),
case_hex("cur", "cur", "000002000100101000000000"),
case_hex("dds", "dds", "4444532000000000"),
case_hex("ktx", "ktx", "ab4b5458203131bb0d0a1a0a"),
case_hex("ktx2", "ktx2", "ab4b5458203230bb0d0a1a0a"),
case_hex("exr", "exr", "762f310100000000"),
case_hex("hdr", "hdr", "233f52414449414e43450a"),
case_hex("pnm", "pnm", "50360a3120310a"),
// 文档
case_hex("pdf", "pdf", "255044462d312e370a"),
case_hex("postscript", "postscript", "252150532d41646f62652d332e300a"),
case_hex("rtf", "rtf", "7b5c727466315c616e7369"),
case_hex("djvu", "djvu", "41542654464f524d00000000"),
case_hex("ole2", "ole2", "d0cf11e0a1b11ae1"),
case_hex(
"pem", "pem", "2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a",
),
case_hex(
"pgp", "pgp", "2d2d2d2d2d424547494e20504750205055424c4943204b455920424c4f434b2d2d2d2d2d",
),
case_hex("chm", "chm", "495453460300000060000000"),
case_hex("pfa", "pfa", "252150532d41646f6265466f6e742d312e300a"),
// 归档
case_hex("zip", "zip", "504b0304"),
case_hex("zip-empty", "zip", "504b0506"),
case_hex("gzip", "gzip", "1f8b0800000000000003"),
case_hex("bzip2", "bzip2", "425a6839314159265359"),
case_hex("xz", "xz", "fd377a585a0000000000"),
case_hex("zstd", "zstd", "28b52ffd00000000"),
case_hex("7z", "7z", "377abcaf271c00000000"),
case_hex("rar4", "rar", "526172211a0700"),
case_hex("rar5", "rar", "526172211a070100"),
case_hex("cab", "cab", "4d53434600000000"),
case_hex("lz4", "lz4", "04224d1800000000"),
case_hex("cpio-newc", "cpio", "30373037303130303030303030"),
case_hex("cpio-odc", "cpio", "30373037303730303030303030"),
case_hex("rpm", "rpm", "edabeedb00000000"),
case_hex("deb", "deb", "213c617263683e0a64656269616e2d62696e617279"),
case_hex("ar", "ar", "213c617263683e0a"),
case_hex("xar", "xar", "7861722100000000"),
case_hex("squashfs", "squashfs", "6873717300000000"),
case_hex("crx", "crx", "4372323403000000"),
case_hex("compress-z", "compress", "1f9d000000000000"),
// 可执行
case_hex("elf", "elf", "7f454c46020101000000000000000000"),
case_hex("wasm", "wasm", "0061736d01000000"),
case_hex("macho64", "macho", "cffaedfe0700000103000080"),
case_hex("macho-fat", "macho", "cafebabe00000002"),
case_hex("java-class", "class", "cafebabe0000004112340000"),
case_hex("dex", "dex", "6465780a3033350000000000"),
case_hex("luac", "luac", "1b4c756153000000"),
case_hex("pcf", "pcf", "0166637000000000"),
case_hex("bdf", "bdf", "5354415254464f4e54202d"),
case_hex("hdf4", "hdf4", "0e03130100000000"),
case_hex("grib", "grib", "4752494200000000"),
case_hex("mat", "mat", "4d41544c414220352e30204d41542d66696c65"),
case_hex("npy", "npy", "934e554d5059010000"),
case_hex("parquet", "parquet", "5041523100000000"),
case_hex("arrow", "arrow", "4152524f573100000000"),
case_hex("avro", "avro", "4f626a0100000000"),
case_hex("orc", "orc", "4f52430000000000"),
case_hex("jks", "jks", "feedfeed00000000"),
case_hex("pcap", "pcap", "0a0d0d0a00000000"),
// 字体
case_hex("ttf", "ttf", "00010000000a000000000000"),
case_hex("otf", "otf", "4f54544f00000000"),
case_hex("woff", "woff", "774f464600010000"),
case_hex("woff2", "woff2", "774f463200010000"),
case_hex("ttc", "ttc", "7474636600010000"),
// 数据库与科学数据
case_hex("sqlite", "sqlite", "53514c69746520666f726d6174203300"),
case_hex("hdf5", "hdf5", "894844460d0a1a0a00000000"),
case_hex("netcdf", "netcdf", "43444601000000000000"),
case_hex("fits", "fits", "53494d504c4520203d20202020"),
// 音视频
case_hex("flac", "flac", "664c614300000022"),
case_hex("au", "au", "2e736e640000000000000017"),
case_hex("wavpack", "wavpack", "7776706b00000000"),
case_hex("ape", "ape", "4d41432000000000"),
case_hex("amr", "amr", "2321414d520a"),
case_hex("asf", "asf", "3026b2758e66cf11a6d900aa0062ce6c"),
case_hex("midi", "midi", "4d5468640000000600000000"),
case_hex("mp3-id3", "mp3", "49443303000000000000"),
// 文本启发式
case_hex("json-object", "json", "7b2261223a317d"),
case_hex("json-array", "json", "5b312c322c335d"),
case_hex("xml", "xml", "3c3f786d6c2076657273696f6e3d22312e30223f3e"),
case_hex("html", "html", "3c21444f43545950452068746d6c3e"),
case_hex("csv", "csv", "612c622c630a312c322c330a"),
case_hex("tsv", "tsv", "61096209630a31093209330a"),
case_hex("plain", "text", "68656c6c6f0a"),
case_hex("plain-utf8", "text", "e4b8ade696870a"),
// 未知
case_hex("empty", "unknown", ""),
case_hex("unknown-nul", "unknown", "00010203"),
case_hex("unknown-ff", "unknown", "ffffffffffffffff"),
case_hex("unknown-invalid-utf8", "unknown", "c0c1c2c3"),
// 结构合法性要求:短样本不能凭魔数就被认成容器
case_hex("truncated-png", "unknown", "89504e47"),
// 对抗样本:文本内容长得像弱签名的格式
case_hex(
"bmp-lookalike-text",
"text",
"424d57203332306920e8bda6e4b8bbe6898be5868c20e4bdbfe794a8e8afb4e6988e20e8afb7e58bbfe794a8e58a9b0a",
),
case_hex(
"mz-lookalike-text",
"text",
"4d5a20e698afe58e8be7bca9e6a0bce5bc8fe6a087e8aeb00a",
),
]
// Ogg / EBML / RIFF / IFF / ISO-BMFF / tar / PE / ISO / TS
cases.push({ label: "ogg-opus", expected: "opus", data: ogg_opus_bytes(), })
cases.push({ label: "webm", expected: "webm", data: ebml_bytes("webm"), })
cases.push({
label: "matroska",
expected: "mkv",
data: ebml_bytes("matroska"),
})
cases.push({
label: "wav",
expected: "wav",
data: riff_bytes("RIFF", "WAVE", [("fmt ", 16), ("data", 0)]),
})
cases.push({
label: "avi",
expected: "avi",
data: riff_bytes("RIFF", "AVI ", [("hdrl", 4), ("movi", 0)]),
})
cases.push({
label: "webp",
expected: "webp",
data: riff_bytes("RIFF", "WEBP", [("VP8 ", 4)]),
})
cases.push({
label: "aiff",
expected: "aiff",
data: riff_bytes("FORM", "AIFF", [("COMM", 18)]),
})
cases.push({
label: "aifc",
expected: "aiff",
data: riff_bytes("FORM", "AIFC", [("COMM", 18)]),
})
cases.push({ label: "tar", expected: "tar", data: tar_bytes(), })
// OLE2:目录项里的流名决定 doc / xls / ppt
cases.push({
label: "ole2-doc",
expected: "doc",
data: ole2_bytes("WordDocument"),
})
cases.push({ label: "ole2-xls", expected: "xls", data: ole2_bytes("Workbook") })
cases.push({
label: "ole2-ppt",
expected: "ppt",
data: ole2_bytes("PowerPoint Document"),
})
cases.push({
label: "ole2-unknown-stream",
expected: "ole2",
data: ole2_bytes("SummaryInformation"),
})
// 对抗样本:流名只是前缀相似,不能当成 doc
cases.push({
label: "ole2-lookalike-stream",
expected: "ole2",
data: ole2_bytes("WordDocumentBackup"),
})
cases.push({ label: "pe", expected: "pe", data: pe_bytes(), })
cases.push({ label: "mobi", expected: "mobi", data: mobi_bytes(), })
cases.push({ label: "iso9660", expected: "iso", data: iso_bytes(), })
cases.push({ label: "mpeg-ts", expected: "ts", data: ts_bytes(), })
// ISO-BMFF:brand 判定
cases.push({ label: "m4a", expected: "m4a", data: ftyp_bytes("M4A "), })
cases.push({ label: "3gp", expected: "3gp", data: ftyp_bytes("3gp4"), })
cases.push({ label: "avif", expected: "avif", data: ftyp_bytes("avif"), })
cases.push({ label: "heic", expected: "heic", data: ftyp_bytes("heic"), })
cases.push({ label: "mov-qt", expected: "mov", data: ftyp_bytes("qt "), })
cases.push({ label: "mp4-isom", expected: "mp4", data: ftyp_bytes("isom"), })
// ISO-BMFF:handler 下钻
cases.push({
label: "mp4-hdlr-vide",
expected: "mp4",
data: bmff_with_handler("isom", "vide"),
})
cases.push({
label: "m4a-hdlr-soun",
expected: "m4a",
data: bmff_with_handler("isom", "soun"),
})
// ZIP:结构 + 规范约束
cases.push({
label: "zip-docx",
expected: "docx",
data: zip_of([
("[Content_Types].xml", ""), ("word/document.xml", ""),
]),
})
cases.push({
label: "zip-xlsx",
expected: "xlsx",
data: zip_of([
("[Content_Types].xml", ""), ("xl/workbook.xml", ""),
]),
})
cases.push({
label: "zip-pptx",
expected: "pptx",
data: zip_of([
("[Content_Types].xml", ""),
("ppt/presentation.xml", ""),
]),
})
cases.push({
label: "zip-epub",
expected: "epub",
data: zip_of([
("mimetype", "application/epub+zip"), ("META-INF/container.xml", ""),
]),
})
cases.push({
label: "zip-odt",
expected: "odt",
data: zip_of([
("mimetype", "application/vnd.oasis.opendocument.text"),
("content.xml", ""),
]),
})
cases.push({
label: "zip-ods",
expected: "ods",
data: zip_of([
("mimetype", "application/vnd.oasis.opendocument.spreadsheet"),
("content.xml", ""),
]),
})
cases.push({
label: "zip-odp",
expected: "odp",
data: zip_of([
("mimetype", "application/vnd.oasis.opendocument.presentation"),
("content.xml", ""),
]),
})
cases.push({
label: "zip-xps",
expected: "xps",
data: zip_of([("FixedDocSeq.fdseq", "")]),
})
cases.push({
label: "zip-apk",
expected: "apk",
data: zip_of([("AndroidManifest.xml", ""), ("classes.dex", "")]),
})
cases.push({
label: "zip-jar",
expected: "jar",
data: zip_of([("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n")]),
})
// 对抗样本:容器里出现字符串,但结构不成立
cases.push({
label: "zip-with-word-in-content",
expected: "zip",
data: zip_of([
("notes.txt", "see word/document.xml and xl/workbook.xml for details"),
]),
})
cases.push({
label: "zip-epub-string-in-content",
expected: "zip",
data: zip_of([("notes.txt", "application/epub+zip")]),
})
cases.push({
label: "zip-ooxml-without-content-types",
expected: "zip",
data: zip_of([("word/document.xml", "")]),
})
cases.push({
label: "zip-mimetype-not-first",
expected: "zip",
data: zip_of([
("README.txt", "hi"), ("mimetype", "application/epub+zip"),
]),
})
// 中央目录在 64 KiB 之后:字符串扫描会漏,结构解析不会
cases.push({
label: "zip-docx-beyond-64k",
expected: "docx",
data: zip_of([
("padding.bin", repeat_ascii("A", 70000)),
("[Content_Types].xml", ""),
("word/document.xml", ""),
]),
})
cases
}
///|
fn actual_id(detection : Detection) -> String {
match detection.file_type {
Some(file_type) => file_type.id
None => "unknown"
}
}
///|
/// 一致性语料总条数。
pub fn conformance_case_count() -> Int {
make_cases().length()
}
///|
/// 运行一致性语料,返回失败描述(空数组表示全部通过)。
pub fn run_conformance() -> Array[String] {
let failures : Array[String] = []
for case in make_cases() {
let detection = detect(case.data)
let actual = actual_id(detection)
if actual != case.expected {
failures.push(
"\{case.label}: expected \{case.expected}, got \{actual} (\{detection.reason})",
)
}
}
failures
}