mooniso

    纯 MoonBit 的 ISO 9660 光盘映像读写库:卷描述符、目录树、Joliet、Rock Ridge 与镜像生成

    iso9660
    iso
    cd
    image
    filesystem
    disc
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    13 hours ago
    Downloads
    10

    #mooniso

    纯 MoonBit 实现的 ISO 9660(ECMA-119)光盘映像读写库,零第三方运行依赖。

    #状态

    已实现(读取侧):

    • 卷描述符链扫描与主卷描述符(PVD)解析
    • 目录记录解析(both-endian 字段校验、时间戳、特殊记录 . / ..)
    • 路径解析(大小写不敏感)与目录列举
    • 命令行 info / ls

    规划中:

    • 文件内容读取(cat / 提取)
    • Joliet(UCS-2 名称)与 Rock Ridge 扩展
    • 映像生成(写入侧)

    #安装

    moon add LL728/mooniso

    #命令行

    moon run cmd/main -- info tools/corpus/minimal.iso moon run cmd/main -- ls tools/corpus/minimal.iso moon run cmd/main -- ls tools/corpus/minimal.iso /SUB

    输出示例:

    volume_id=MOONISO_TEST block_size=2048 volume_space_size=24 root_extent=20 root_size=2048

    HELLO.TXT file 17 2026-01-02T03:04:05 SUB dir 2048 2026-01-02T03:04:05

    #库 API

    let image = @mooniso.IsoImage::open(@fs.read_file_to_bytes("image.iso"))
    println(image.volume_id())
    for entry in image.list_dir("/SUB") {
    println("\{entry.name}\t\{entry.size}")
    }

    #测试与对拍

    moon test --target wasm-gc # 单元测试:字节序、目录记录、名称规范化 python tools/gen_fixtures.py # 生成 tools/corpus/minimal.iso python tools/check_e2e.py # 用 CLI 读映像,与期望输出逐行比对

    测试映像由标准库脚本按 ECMA-119 布局生成,并已用 Windows CDFS 驱动挂载验证 (卷标、目录树、嵌套文件内容一致)。CI 在 Linux 上额外用内核 iso9660 驱动挂载同一映像, 把挂载结果与 CLI 输出独立对照。

    #许可证

    Apache-2.0,见 LICENSE。

    IsoError

    pub suberror IsoError {
    Truncated
    BadBothEndian
    BadSignature
    BadDescriptor(Int)
    NotFound(String)
    NotDirectory(String)
    BadDirectoryRecord(Int)
    }

    ByteReader

    pub struct ByteReader {
    data : Bytes
    pos : Int
    }

    ByteReader::new

    fn ByteReader::new(data : Bytes) -> ByteReader

    ByteReader::position

    fn ByteReader::position(self : ByteReader) -> Int

    ByteReader::read_ascii

    fn ByteReader::read_ascii(self : ByteReader, n : Int) -> String raise IsoError

    读取定长 ASCII 字段:遇 NUL 截断,尾部空格忽略。

    ByteReader::read_bytes

    fn ByteReader::read_bytes(self : ByteReader, n : Int) -> Bytes raise IsoError

    ByteReader::read_u16_be

    fn ByteReader::read_u16_be(self : ByteReader) -> Int raise IsoError

    ByteReader::read_u16_both

    fn ByteReader::read_u16_both(self : ByteReader) -> Int raise IsoError

    ByteReader::read_u16_le

    fn ByteReader::read_u16_le(self : ByteReader) -> Int raise IsoError

    ByteReader::read_u32_be

    fn ByteReader::read_u32_be(self : ByteReader) -> Int raise IsoError

    ByteReader::read_u32_both

    fn ByteReader::read_u32_both(self : ByteReader) -> Int raise IsoError

    ISO 9660 的 both-endian 4 字节整数:前 4 字节小端,后 4 字节大端。 两份必须一致,否则映像结构不可信。

    ByteReader::read_u32_le

    fn ByteReader::read_u32_le(self : ByteReader) -> Int raise IsoError

    ByteReader::read_u8

    fn ByteReader::read_u8(self : ByteReader) -> UInt raise IsoError

    ByteReader::remaining

    fn ByteReader::remaining(self : ByteReader) -> Int

    ByteReader::seek

    fn ByteReader::seek(self : ByteReader, pos : Int) -> Unit raise IsoError

    ByteReader::skip

    fn ByteReader::skip(self : ByteReader, n : Int) -> Unit raise IsoError

    DirectoryRecord

    pub struct DirectoryRecord {
    extent : Int
    size : Int
    flags : Int
    name : String
    date : RecordDate
    }

    一条目录记录。

    • extent:文件内容起始的逻辑块号(LBA),目录则是目录数据起始 LBA;
    • size:数据长度(字节),目录则是目录数据总长度;
    • name:去掉 ;版本号 后的名字,目录为原始目录名;
    • is_dir:flags 的 bit 1。

    DirectoryRecord::is_dir

    fn DirectoryRecord::is_dir(self : DirectoryRecord) -> Bool

    DirectoryRecord::is_special

    fn DirectoryRecord::is_special(self : DirectoryRecord) -> Bool

    特殊记录:0x00 是「本目录」,0x01 是「父目录」。

    IsoImage

    pub struct IsoImage {
    data : Bytes
    volume_id : String
    block_size : Int
    volume_space_size : Int
    root : DirectoryRecord
    }

    IsoImage::block_size

    fn IsoImage::block_size(self : IsoImage) -> Int

    IsoImage::find_record

    fn IsoImage::find_record(self : IsoImage, path : String) -> DirectoryRecord raise IsoError

    按路径查找一条记录;路径以 / 分隔,忽略开头结尾的 / 与连续 /。

    IsoImage::list_dir

    fn IsoImage::list_dir(self : IsoImage, path : String) -> Array[DirectoryRecord] raise IsoError

    列出目录内容(默认过滤掉 . 与 ..),保持映像里的物理顺序。

    IsoImage::open

    fn IsoImage::open(data : Bytes) -> IsoImage raise IsoError

    打开一个 ISO 9660 映像(内存中的完整字节流)。

    IsoImage::root

    fn IsoImage::root(self : IsoImage) -> DirectoryRecord

    IsoImage::volume_id

    fn IsoImage::volume_id(self : IsoImage) -> String

    IsoImage::volume_space_size

    fn IsoImage::volume_space_size(self : IsoImage) -> Int

    PrimaryVolumeDescriptor

    pub struct PrimaryVolumeDescriptor {
    volume_id : String
    volume_space_size : Int
    logical_block_size : Int
    path_table_size : Int
    root : DirectoryRecord
    }

    RecordDate

    pub struct RecordDate {
    year : Int
    month : Int
    day : Int
    hour : Int
    minute : Int
    second : Int
    gmt_offset_quarters : Int
    }

    目录记录的 7 字节时间戳(§9.1.5): 年 = 字段值 + 1900,最后 1 字节是有符号的 15 分钟时区偏移。

    RecordDate::to_iso_string

    fn RecordDate::to_iso_string(self : RecordDate) -> String

    SECTOR_SIZE

    let SECTOR_SIZE : Int

    VOLUME_DESCRIPTOR_START_LBA

    let VOLUME_DESCRIPTOR_START_LBA : Int

    normalize_iso_name

    fn normalize_iso_name(raw : String) -> String

    去掉 ISO 9660 的 ;版本号,并去掉「无扩展名」时残留的点号。

    parse_directory_record

    fn parse_directory_record(reader : ByteReader) -> (DirectoryRecord, Int) raise IsoError

    从 reader 当前位置解析一条目录记录,返回记录本身与消耗的字节数。 返回长度为 0 表示目录数据在本扇区后面是填充。

    parse_primary_volume_descriptor

    fn parse_primary_volume_descriptor(data : Bytes, lba : Int) -> PrimaryVolumeDescriptor raise IsoError

    解析主卷描述符(类型 1)。

    scan_volume_descriptors

    fn scan_volume_descriptors(data : Bytes) -> Array[(Int, Int)] raise IsoError

    扫描卷描述符链,返回 (类型, 扇区号) 列表,直到遇到结束符。 结构不完整(没有结束符)视为损坏。