Sign in

    mbt-filetype

    零依赖的 MoonBit 文件类型识别引擎:容器结构解析、证据链判定与可扩展规则,附 CLI。

    filetype
    mime
    content-sniffing
    structure-parsing
    magic-bytes
    cli
    Download zip
    Author
    Version
    0.2.0
    License
    MIT
    Last updated
    12 hours ago
    Downloads
    20

    Dependencies

    #mbt-filetype

    零依赖的 MoonBit 文件类型识别与内容嗅探引擎:结构解析、证据链判定、可扩展规则,附命令行工具。

    mbt-filetype 回答一个很具体的问题:这段字节到底是什么文件? 它不根据扩展名猜测, 而是按格式规范解析字节结构,并给出可核对、可加权的证据链。

    #特性

    • 基于结构的识别,而不是字符串搜索:ZIP 走 EOCD → 中央目录 → 条目名;OLE2 走 FAT → 目录扇区 → 流名;ISO-BMFF 走 box 树 → mdia/hdlr;RIFF 遍历 chunk;EBML 解 vint 取 DocType;tar 校验 header checksum;PE 校验 COFF/Optional Header
    • 证据链 + 派生评分:每条证据带种类、偏移、权重;置信度由总分推导,不是人工标注的标签
    • 可扩展规则:Registry + Rule + Matcher,调用方可以注册自己的签名与检测器,与内置规则走同一套流程
    • 不猜原则:结构校验不通过就降级或返回 unknown,歧义时返回候选列表
    • 确定性:纯函数识别,不访问网络、不依赖时区与 locale
    • 边界安全:所有字节访问都先检查长度,任意输入都不会越界;附带固定种子 fuzz
    • CI 友好:--expect 断言类型,--scan 报告扩展名与内容不符的文件

    #安装

    moon add Mr-Houjie/mbt-filetype

    #库 API

    let detection = @Mr-Houjie/mbt-filetype.detect(bytes)
    match detection.file_type {
    Some(file_type) => {
    println(file_type.id) // "docx"
    println(file_type.mime) // "application/vnd.openxmlformats-officedocument..."
    println(detection.score) // 100
    for item in detection.evidence {
    println("\{item.kind} +\{item.weight}: \{item.label}")
    }
    }
    None => println("unknown")
    }

    #置信度如何得出

    评分是证据的累加,阈值是公开契约(confidence_of_score):

    证据类型权重例子
    强魔数90PK\x03\x04、%PDF-
    弱魔数20BM、MZ、P4
    结构验证40 ~ 70中央目录解析成功、OLE2 目录流命中、PE 头校验通过
    文本启发式30JSON/CSV 形状

    • 总分 ≥ 90 → certain,≥ 20 → probable,否则 unknown
    • 所以「BM 开头的文本」只有 20 分且被 DIB header 校验拦下,会回落到 text; 「结构完整的 docx」是 90 + 40 分,判定为 certain

    --explain 会打印逐条证据:

    examples/sample.png type: png mime: image/png extension: .png category: image confidence: certain (90/100) evidence: PNG signature offset: 0 bytes: 8 [1] magic +90 @0: PNG signature

    #扩展:自定义规则与检测器

    let registry = @Mr-Houjie/mbt-filetype.Registry::new()

    // 声明式规则:支持固定偏移、尾部偏移、位掩码、ASCII、与/或组合
    registry.add_rule(
    @Mr-Houjie/mbt-filetype.Rule::new(
    "myfmt", "application/x-myfmt", "myfmt", @Mr-Houjie/mbt-filetype.Other,
    "MYFMT v1 魔数", 90,
    @Mr-Houjie/mbt-filetype.Matcher::all([
    @Mr-Houjie/mbt-filetype.Matcher::magic_at(0, b"MYFMT"),
    @Mr-Houjie/mbt-filetype.Matcher::masked_at(5, b"\x01\x00", b"\xff\x00"),
    ]),
    ),
    )

    // 命令式检测器:规则与内置签名都没命中时兜底
    registry.add_detector(fn(data) { ... })

    let detection = @Mr-Houjie/mbt-filetype.detect_with(registry, bytes)

    优先级:用户规则 → 内置签名与容器判定 → 用户检测器 → 文本启发式。

    #CLI

    # 识别单个文件 moon run cmd/main -- examples/sample.png # 以 JSON 输出 moon run cmd/main -- --json examples/sample.json # 显示证据链、偏移与评分 moon run cmd/main -- --explain examples/sample.png # 断言类型:不匹配时退出码为 1 moon run cmd/main -- --expect image/png examples/sample.png # 递归扫描目录,报告扩展名与内容不符的文件 moon run cmd/main -- --scan examples # 列出全部已知类型 moon run cmd/main -- --list-types

    #支持等级与判定终点

    支持范围分三级,不是所有类型都走结构解析——这一点写在前面,避免误解:

    等级判定方式覆盖定位
    A. 结构解析按格式规范解析容器结构ZIP、OLE2、ISO-BMFF、RIFF/IFF、EBML、Ogg、tar、PE、BMP项目的实质贡献
    B. 魔数 + 字段校验魔数命中后校验长度、版本或枚举字段其余图像、归档、字体、数据库、科学数据兼容层
    C. 保守启发式文本结构判定,一律 probablejson / xml / html / csv / tsv / text显式标注不确定

    A 级容器必须同时满足:结构合法的正例、对抗样本不误判、真实文件对拍记录。缺一项就不算「已支持」。

    #支持的结构判定

    容器解析方式可区分
    ZIPEOCD → 中央目录 → 条目名/压缩方法docx / xlsx / pptx / epub / odt / ods / odp / xps / apk / jar / zip
    OLE2512 字节头 → DIFAT → FAT → 目录扇区 → UTF-16LE 流名doc / xls / ppt / ole2
    ISO-BMFFbox 树 → ftyp brand + moov/trak/mdia/hdlrmp4 / mov / m4a / heic / avif / 3gp
    RIFF / IFFchunk 遍历 + 必需块校验wav / avi / webp / aiff / riff / iff
    EBMLvint 解码 → DocTypemkv / webm
    Ogg页头 + segment table → 首包opus / ogg
    tar512 字节 header 八进制 checksumtar
    PEe_lfanew → COFF → Optional Headerpe(含机器类型、EXE/DLL、节数)
    BMP保留字段 + DIB header 大小bmp

    B 级的实现细节见 signatures.mbt(魔数表 + 字段校验),A 级见 structures.mbt。

    #一致性语料与测试

    识别行为由 conformance.mbt 中的 142 条「输入 → 期望类型」用例定义,测试与自检共用同一份数据。 语料不是手写的几个魔数字节,而是用构造器生成结构合法的容器:

    • 完整 ZIP(本地头 + 中央目录 + EOCD),含 64 KiB 之后的中央目录用例
    • 完整 OLE2(header + FAT + 目录扇区 + 流名),含 doc / xls / ppt 与「前缀相似流名」对抗样本
    • 完整 RIFF/IFF chunk、ISO-BMFF box 树、EBML vint、Ogg 页、tar header、PE 头
    • 对抗样本:真 ZIP 里塞 word/document.xml 文本、mimetype 不在首位、 缺少 [Content_Types].xml、BMW 320i … 文本、MZ … 文本

    真实文件语料目前覆盖 ZIP / RIFF / tar;OLE2、ISO-BMFF、EBML、Ogg、PE、BMP 由结构构造器与对抗样本覆盖,真实样本对拍列入下一轮。

    moon test # 标准测试(部分 Windows 环境受嵌套进程限制) moon run cmd/verify # 独立自检入口,与 moon test 等价

    #跨库一致性对拍

    tools/crosscheck.py 把本库的输出与 libmagic / npm file-type 逐文件对比:

    # 准备一个真实文件语料目录,然后: python tools/crosscheck.py corpus/

    脚本会输出一致率、全部分歧清单,便于逐条归因。分歧不是失败——是下一轮规则修正的输入。

    #已知边界(都有明确的降级路径)

    这些不是「没做」,而是有确定行为的降级——边界之外返回更保守的结果,而不是猜:

    场景行为
    deflate 条目内容条目名仍从中央目录读取;内容只读 stored 条目(EPUB/ODF 规范要求 mimetype 必须 stored)
    ZIP64 / 单条 > 2 GiB中央目录解析失败 → 降级为 zip,不猜具体子类型
    > 2 GiB 的 ISO-BMFF boxbox 遍历停止 → 退回 ftyp brand 判定;64 位 largesize 仅支持高 32 位为 0
    归档内部不递归;--scan 只报告外层文件
    文本类一律 probable,不与二进制判定混用
    恶意构造 / 完整性不检测,本库不是安全扫描器

    #许可证

    MIT

    Category

    pub(all) enum Category {
    Image
    Document
    Archive
    Audio
    Video
    Executable
    Font
    Database
    Scientific
    Wasm
    Text
    Other
    }

    文件大类。

    Confidence

    pub(all) enum Confidence {
    Certain
    Probable
    Unknown
    }

    判定置信度。

    • Certain:命中强签名,或容器结构可以明确消歧;
    • Probable:弱签名、结构校验不完整或文本启发式;
    • Unknown:没有足够证据,宁可返回未知也不猜。

    具体取值由 confidence_of_score 从证据评分推导。

    Detection

    pub(all) struct Detection {
    file_type : FileType?
    confidence : Confidence
    reason : String
    offset : Int
    matched_len : Int
    candidates : Array[FileType]
    score : Int
    evidence : Array[Evidence]
    }

    一次识别结果。

    Evidence

    pub(all) struct Evidence {
    kind : String
    label : String
    offset : Int
    matched_len : Int
    weight : Int
    }

    一条判定证据。

    置信度不是人工标注的标签,而是由证据累加得到的评分: 强魔数 90 分、弱魔数 20 分、结构验证 40~70 分、文本启发式 30 分; 总分 ≥ 90 判 Certain,≥ 20 判 Probable,否则 Unknown。

    FileType

    pub(all) struct FileType {
    id : String
    mime : String
    ext : String
    category : Category
    }

    一种文件类型。

    Matcher

    pub enum Matcher {
    MagicAt(Int, Bytes)
    MagicAtEnd(Int, Bytes)
    MaskedAt(Int, Bytes, Bytes)
    AsciiAt(Int, String)
    All(Array[Matcher])
    Any(Array[Matcher])
    }

    规则匹配器。

    组合子让规则可以表达「强魔数 + 掩码」「多条件与/或」, 而不是为每种变体在签名表里堆一条硬编码。

    Matcher::all

    fn Matcher::all(items : Array[Matcher]) -> Matcher

    Matcher::any

    fn Matcher::any(items : Array[Matcher]) -> Matcher

    Matcher::ascii_at

    fn Matcher::ascii_at(offset : Int, text : String) -> Matcher

    Matcher::magic_at

    fn Matcher::magic_at(offset : Int, magic : Bytes) -> Matcher

    具名构造器。比起直接写枚举变体,调用方更容易读写,也让文档示例更短。

    Matcher::magic_at_end

    fn Matcher::magic_at_end(back : Int, magic : Bytes) -> Matcher

    Matcher::masked_at

    fn Matcher::masked_at(offset : Int, magic : Bytes, mask : Bytes) -> Matcher

    PeInfo

    pub struct PeInfo {
    machine : Int
    is_64 : Bool
    is_dll : Bool
    subsystem : Int
    sections : Int
    }

    Registry

    pub struct Registry {
    rules : Array[Rule]
    detectors : Array[(Bytes) -> Detection?]
    }

    用户扩展入口。

    Registry::add_detector

    fn Registry::add_detector(self : Registry, detector : (Bytes) -> Detection?) -> Unit

    追加一个检测器。它只在规则与内置签名都没命中时被调用, 返回 Some 即作为最终结果。

    Registry::add_rule

    fn Registry::add_rule(self : Registry, rule : Rule) -> Unit

    追加一条规则。用户规则优先于内置签名表。

    Registry::detector_count

    fn Registry::detector_count(self : Registry) -> Int

    Registry::new

    fn Registry::new() -> Registry

    Registry::rule_count

    fn Registry::rule_count(self : Registry) -> Int

    RiffInfo

    pub struct RiffInfo {
    form : String
    chunks : Array[String]
    complete : Bool
    }

    Rule

    pub struct Rule {
    id : String
    mime : String
    ext : String
    category : Category
    label : String
    weight : Int
    matcher : Matcher
    }

    一条声明式规则。

    Rule::new

    fn Rule::new(id : String, mime : String, ext : String, category : Category, label : String, weight : Int, matcher : Matcher) -> Rule

    ZipArchive

    pub struct ZipArchive {
    entries : Array[ZipEntry]
    declared_entries : Int
    complete : Bool
    }

    ZipEntry

    pub struct ZipEntry {
    name : String
    compression : Int
    compressed_size : Int
    uncompressed_size : Int
    data_offset : Int
    local_offset : Int
    }

    bmff_brands

    fn bmff_brands(data : Bytes) -> Array[String]

    读 ftyp:major brand + compatible brands。

    bmff_handlers

    fn bmff_handlers(data : Bytes) -> Array[String]

    收集 handler 类型(vide / soun),路径为 moov → trak* → mdia → hdlr。

    category_name

    fn category_name(category : Category) -> String

    大类名称。

    confidence_name

    fn confidence_name(confidence : Confidence) -> String

    置信度名称。

    confidence_of_score

    fn confidence_of_score(score : Int) -> Confidence

    由评分推导置信度。阈值是公开契约,不随调用点变化。

    conformance_case_count

    fn conformance_case_count() -> Int

    一致性语料总条数。

    detect

    fn detect(data : Bytes) -> Detection

    根据字节内容识别文件类型(使用内置规则集)。

    该函数是纯函数:不读文件、不访问网络、不依赖系统状态。

    detect_with

    fn detect_with(registry : Registry, data : Bytes) -> Detection

    使用自定义规则集与检测器识别文件类型。

    优先级:用户规则 → 内置签名与容器判定 → 用户检测器 → 文本启发式。

    ebml_doctype

    fn ebml_doctype(data : Bytes) -> String?

    解析 EBML 头部里的 DocType(webm / matroska)。

    eval_matcher

    fn eval_matcher(matcher : Matcher, data : Bytes) -> Int?

    求值匹配器,命中返回证据偏移。

    is_text_category

    fn is_text_category(file_type : FileType) -> Bool

    该类型是否属于文本大类。

    known_types

    fn known_types() -> Array[FileType]

    全部已知类型(按首次出现顺序去重),用于 --list-types。

    parse_iff

    fn parse_iff(data : Bytes) -> RiffInfo?

    IFF(AIFF/AIFC 等)用的是 FORM 容器标记,chunk 布局与 RIFF 相同。

    parse_ole2_streams

    fn parse_ole2_streams(data : Bytes) -> Array[String]?

    解析 OLE2 复合文档的目录项名称(WordDocument / Workbook / PowerPoint Document …)。

    返回 None 表示头部或 FAT 结构不成立;返回空数组表示结构成立但没有可读流名。

    parse_pe

    fn parse_pe(data : Bytes) -> PeInfo?

    解析 PE 结构。返回 None 表示 e_lfanew 指向的位置没有合法的 PE 头。

    parse_riff

    fn parse_riff(data : Bytes) -> RiffInfo?

    parse_zip

    fn parse_zip(data : Bytes) -> ZipArchive?

    解析 ZIP 中央目录。返回 None 表示这不是结构完整的 ZIP。

    pe_machine_name

    fn pe_machine_name(info : PeInfo) -> String

    render_json

    fn render_json(path : String, detection : Detection) -> String

    渲染单行 JSON。

    render_text

    fn render_text(path : String, detection : Detection, explain : Bool) -> String

    渲染人类可读的识别结果。

    render_types

    fn render_types() -> String

    渲染类型清单(--list-types)。

    rule_evidence

    fn rule_evidence(rule : Rule, offset : Int) -> Evidence

    命中规则时构造证据。

    run_conformance

    fn run_conformance() -> Array[String]

    运行一致性语料,返回失败描述(空数组表示全部通过)。

    signature_count

    fn signature_count() -> Int

    内置签名条数。

    tar_checksum_ok

    fn tar_checksum_ok(data : Bytes) -> Bool

    校验 POSIX tar header 的八进制 checksum(字段 148..155,计算时按空格处理)。

    zip_entry_content

    fn zip_entry_content(data : Bytes, entry : ZipEntry) -> Bytes?

    取出 stored 条目的原始内容(deflate 条目不解压,返回 None)。

    zip_entry_names

    fn zip_entry_names(archive : ZipArchive) -> Array[String]

    条目名快照,用于算法判定与证据输出。

    zip_find_entry

    fn zip_find_entry(archive : ZipArchive, name : String) -> ZipEntry?

    按名字精确查找条目(不是子串搜索)。