mooniso

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

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

    #mooniso

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

    #能力

    读取:

    • 卷描述符链扫描、主卷描述符(PVD)与补充卷描述符(Joliet SVD)解析
    • 目录记录解析(both-endian 校验、时间戳、. / ..、多段文件合并)
    • 路径解析(大小写不敏感)与目录列举
    • 文件内容读取(跨扇区、多段文件、0 字节文件、越界拒绝)
    • Joliet(UCS-2,含代理对)与 Rock Ridge(NM / PX / SL / CE 续接区)
    • El Torito 启动目录(启动标志、平台、媒体类型、加载地址、UEFI section header)
    • UDF(ECMA-167)读取:Anchor、描述符序列、File Set、File Entry、FID(含 UTF-16 名字)

    写入:

    • 从目录树生成映像:ISO 9660 level 1 名字(自动 8.3 化与去重)+ Joliet + Rock Ridge
    • 可生成 El Torito 可启动映像:BIOS 启动项 + UEFI section header(双启动),含校验和
    • Rock Ridge:NM(原名)、PX(POSIX 权限)、SL(符号链接)
    • 可分别关闭 Joliet / Rock Ridge;L、M 路径表按规范生成

    #演示

    demo

    终端动画演示(真实命令、真实输出):docs/demo/mooniso-demo.mp4, 可用 python tools/make_demo_video.py 重新生成。

    #安装

    moon add LL728/mooniso

    #命令行

    下列命令针对本仓库源码,请在仓库根目录执行(cmd/main 是相对的)。 作为依赖使用时请用上面的 moon add,按「库 API」一节调用。

    moon run cmd/main -- info image.iso # 卷信息 + 启动信息 moon run cmd/main -- ls image.iso /SUB # 列目录 moon run cmd/main -- cat image.iso /HELLO.TXT # 打印文本 moon run cmd/main -- extract image.iso /BIG.BIN out # 落盘(二进制安全) moon run cmd/main -- tree image.iso # 递归列出整棵树 moon run cmd/main -- extract-all image.iso ./out # 整树导出(跳过符号链接) moon run cmd/main -- verify image.iso # 结构体检 moon run cmd/main -- udf-ls image.iso /Docs # 走 UDF 视角列目录 moon run cmd/main -- udf-cat image.iso /hello.txt # 走 UDF 视角读文件 moon run cmd/main -- create out.iso ./some-dir # 从目录生成映像 moon run cmd/main -- create out.iso ./some-dir --boot /boot.bin --efi /efi.img # BIOS+UEFI 双启动 moon run cmd/main -- create out.iso ./some-dir --link /link.txt=/hello.txt # 写入 RR 符号链接

    最小可运行示例:

    moon run examples/basic # 内存里造映像 -> 读回 -> 列出 -> 校验

    #库 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}")
    }
    let bytes = image.read_file("/HELLO.TXT")

    // 写
    let builder = @mooniso.IsoBuilder::new()
    builder.volume_id("MY_VOLUME")
    builder.add_directory("/Docs")
    builder.add_file("/Hello.txt", b"hi")
    let image_bytes = builder.build()

    #设计说明

    • 分层:byte_reader(both-endian 数值)→ volume(卷描述符 / El Torito)→ dir_record + susp(目录记录 / Rock Ridge)→ image(树遍历、文件读取、校验)→ writer(布局与写出)→ udf(独立于 ISO 9660 的 UDF 路径)。
    • 错误模型:所有解析入口返回 IsoError,区分「结构损坏」(Truncated / BadBothEndian / BadSignature / BadDescriptor / BadDirectoryRecord)与「语义问题」(NotFound / NotDirectory / IsDirectory / IsSymlink),调用方不必猜。
    • 写入侧:先把目录树摊平成表 → 分配逻辑块(描述符 / 路径表 / ISO 目录 / Joliet 目录 / 文件 / 启动目录)→ 再回填记录。目录记录按名字升序,符合 ECMA-119 对顺序的要求。
    • 手写实现:核心解析与生成逻辑全部手写自规范;Python 脚本只用于生成测试映像与对拍,不参与库的构建。

    #参考与来源

    • 规范:ECMA-119(ISO 9660)、ECMA-167 / ISO 13346(UDF)、El Torito Bootable CD-ROM Format Specification 1.0、IEEE P1282(Rock Ridge / SUSP)。
    • 对拍基线(仅测试期使用,不随包分发、未移植任何代码):pycdlib(Python,LGPL-2.1)、libarchive(BSD-2-Clause)、Windows CDFS 驱动、Linux 内核 iso9660 驱动。
    • 测试映像:tools/gen_fixtures.py 按规范自行构造;Joliet / Rock Ridge / El Torito / UDF 映像由 tools/gen_fixtures_pycdlib.py、tools/gen_fixtures_udf.py 调用 pycdlib 生成,作为独立实现的基准。
    • 本项目采用 Apache-2.0,与上述规范及参考实现的许可证无冲突。

    #测试与对拍

    moon test --target wasm-gc # 单元测试:字节序、目录记录、Joliet、SUSP、El Torito、生成器回读 python tools/gen_fixtures.py # 生成 tools/corpus/*.iso(自身格式,可复现) python tools/check_e2e.py # CLI 输出与期望逐行比对 python tools/gen_fixtures_pycdlib.py # 用 pycdlib 生成 Joliet/Rock Ridge/El Torito 映像与基准清单 python tools/gen_fixtures_udf.py # 用 pycdlib 生成 UDF 2.60 映像与基准清单 python tools/check_compat.py # 读取侧与 pycdlib 对拍 python tools/check_roundtrip.py # 写入侧:生成的映像由 pycdlib 读回比对(含可启动映像)

    对拍基线来自独立实现:pycdlib(Python)、libarchive、Windows CDFS 驱动、 Linux 内核 iso9660 驱动(CI 中挂载后逐字节比对)。

    #已知边界

    • 写入侧名字级别可选:level 1(8.3 + ;1,默认)/ level 2(≤31 字符)/ level 3(≤207 字符),--level 切换
    • Rock Ridge 写入可选 1.09 / 1.12(--rr 112,PX 带序列号、ER 用 IEEE_P1282);读取兼容两者的常见子集
    • UDF 为只读,覆盖 UDF 2.01–2.60 的常规单区段映像(多区段 VAT、元数据分区、>4 GiB 单文件不在范围内)
    • 写入侧不生成 UDF;El Torito 支持 BIOS 启动项 + 单个 UEFI section(多 section 与启动信息表不在范围内)

    #许可证

    Apache-2.0,见 LICENSE。

    IsoError

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

    BootEntry

    pub struct BootEntry {
    platform_id : Int
    bootable : Bool
    media_type : Int
    sector_count : Int
    load_lba : Int
    }

    El Torito 启动信息(从启动目录里读出的第一个启动项)。

    BootInfo

    pub struct BootInfo {
    catalog_lba : Int
    platform_id : Int
    bootable : Bool
    media_type : Int
    load_segment : Int
    sector_count : Int
    load_lba : Int
    entries : Array[BootEntry]
    }

    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
    system_use : Bytes
    posix_mode : Int?
    symlink_target : String?
    }

    一条目录记录。

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

    DirectoryRecord::is_dir

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

    DirectoryRecord::is_multi_extent

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

    flags 的 bit 7:多段文件,后面还有同名的连续记录。

    DirectoryRecord::is_special

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

    特殊记录:0x00 是「本目录」,0x01 是「父目录」。
    fn DirectoryRecord::is_symlink(self : DirectoryRecord) -> Bool

    是否为 Rock Ridge 符号链接。

    IsoBuilder

    pub struct IsoBuilder {
    volume_id : String
    joliet : Bool
    rock_ridge : Bool
    boot_path : String?
    efi_path : String?
    boot_load_sectors : Int
    boot_platform : Int
    name_level : Int
    rr_version : Int
    dirs : Array[String]
    files : Array[(String, Bytes)]
    symlinks : Array[(String, String)]
    }

    IsoBuilder::add_directory

    fn IsoBuilder::add_directory(self : IsoBuilder, path : String) -> Unit

    IsoBuilder::add_file

    fn IsoBuilder::add_file(self : IsoBuilder, path : String, data : Bytes) -> Unit

    fn IsoBuilder::add_symlink(self : IsoBuilder, path : String, target : String) -> Unit

    添加一个 Rock Ridge 符号链接(目标按原样写进 SL 条目)。

    IsoBuilder::boot_image

    fn IsoBuilder::boot_image(self : IsoBuilder, path : String, load_sectors : Int, platform : Int) -> Unit

    指定 El Torito 启动映像(必须是已加入的文件路径),生成可启动 ISO。

    IsoBuilder::boot_image_efi

    fn IsoBuilder::boot_image_efi(self : IsoBuilder, path : String, load_sectors : Int) -> Unit

    追加一个 UEFI 启动项(写入启动目录的 section header,平台 0xEF)。

    IsoBuilder::build

    fn IsoBuilder::build(self : IsoBuilder) -> Bytes raise IsoError

    生成完整的 ISO 9660 映像字节。

    IsoBuilder::joliet

    fn IsoBuilder::joliet(self : IsoBuilder, enable : Bool) -> Unit

    IsoBuilder::name_level

    fn IsoBuilder::name_level(self : IsoBuilder, level : Int) -> Unit

    设置 ISO 9660 名字级别(1/2/3),影响写入的 ISO 树名字,不影响 Joliet / Rock Ridge 原名。

    IsoBuilder::new

    fn IsoBuilder::new() -> IsoBuilder

    IsoBuilder::rock_ridge

    fn IsoBuilder::rock_ridge(self : IsoBuilder, enable : Bool) -> Unit

    IsoBuilder::rr_version

    fn IsoBuilder::rr_version(self : IsoBuilder, version : Int) -> Unit

    设置 Rock Ridge 版本(109 或 112):1.12 的 PX 记录多 8 字节序列号。

    IsoBuilder::volume_id

    fn IsoBuilder::volume_id(self : IsoBuilder, id : String) -> Unit

    IsoImage

    pub struct IsoImage {
    data : Bytes
    volume_id : String
    block_size : Int
    volume_space_size : Int
    root : DirectoryRecord
    joliet_root : DirectoryRecord?
    joliet_level : Int
    rock_ridge : Bool
    }

    IsoImage::block_size

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

    IsoImage::boot_info

    fn IsoImage::boot_info(self : IsoImage) -> BootInfo? raise IsoError

    El Torito 启动信息;映像不可启动时为 None。

    IsoImage::file_size

    fn IsoImage::file_size(self : IsoImage, path : String) -> Int raise IsoError

    文件大小(多段文件为各段之和)。

    IsoImage::find_record

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

    按路径查找一条记录(多段文件返回第一段)。

    IsoImage::find_records

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

    按路径查找「组成同一实体的连续记录」。

    ISO 9660 的多段文件(flags bit 7 = 1 表示后面还有一段)用同名连续记录描述, 目录永远只有一条记录。

    IsoImage::has_rock_ridge

    fn IsoImage::has_rock_ridge(self : IsoImage) -> Bool

    映像是否带 Rock Ridge 扩展。

    IsoImage::joliet_level

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

    Joliet 级别(1/2/3),无 Joliet 时为 0。

    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::read_file

    fn IsoImage::read_file(self : IsoImage, path : String) -> Bytes raise IsoError

    读取文件内容:多段文件按记录顺序拼接,目录以 IsDirectory 失败。 全程做边界检查,越界一律 Truncated,不会读到映像之外。

    IsoImage::root

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

    IsoImage::uses_joliet

    fn IsoImage::uses_joliet(self : IsoImage) -> Bool

    是否使用 Joliet 名字(存在补充卷描述符时为真)。

    IsoImage::validate

    fn IsoImage::validate(self : IsoImage) -> ValidationReport

    遍历整棵目录树做结构校验:记录能否解析、extent 是否越界、文件能否读完。 返回全部问题而不是遇错即停,方便一次性给出体检结论。

    IsoImage::volume_id

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

    IsoImage::volume_space_size

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

    IsoImage::walk

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

    递归遍历目录树(路径形如 /Docs/note.md,按物理顺序)。

    JolietDescriptor

    pub struct JolietDescriptor {
    level : Int
    root : DirectoryRecord
    }

    Joliet 补充卷描述符。

    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

    RockRidge

    pub struct RockRidge {
    name : String?
    mode : Int?
    symlink : String?
    present : Bool
    }

    UdfEntry

    pub struct UdfEntry {
    name : String
    is_dir : Bool
    size : Int
    }

    UDF 目录项。

    UdfExtent

    pub struct UdfExtent {
    length : Int
    location : Int
    partition : Int
    }

    长分配描述符(long_ad):长度、逻辑块号、分区引用号。

    UdfImage

    pub struct UdfImage {
    data : Bytes
    block_size : Int
    partition_start : Int
    root : UdfExtent
    }

    UDF 映像。

    UdfImage::block_size

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

    UdfImage::list_dir

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

    列出一级目录。

    UdfImage::open

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

    打开 UDF 映像;不是 UDF(没有有效 Anchor)时以 BadDescriptor 失败。

    UdfImage::read_file

    fn UdfImage::read_file(self : UdfImage, path : String) -> Bytes raise IsoError

    读取文件内容。

    ValidationReport

    pub struct ValidationReport {
    directories : Int
    files : Int
    symlinks : Int
    total_bytes : Int
    problems : Array[String]
    }

    结构校验结果。

    WalkEntry

    pub struct WalkEntry {
    path : String
    is_dir : Bool
    is_symlink : Bool
    size : Int
    }

    遍历结果的一项。

    SECTOR_SIZE

    let SECTOR_SIZE : Int

    VOLUME_DESCRIPTOR_START_LBA

    let VOLUME_DESCRIPTOR_START_LBA : Int

    decode_ucs2_be

    fn decode_ucs2_be(raw : Bytes) -> String

    解码 Joliet 的 UCS-2 大端名字(含非 BMP 的代理对),遇到 NUL 结束。

    find_boot_catalog_lba

    fn find_boot_catalog_lba(data : Bytes) -> Int? raise IsoError

    在卷描述符链里找启动记录卷描述符(类型 0),返回启动目录所在的逻辑块号。

    find_joliet_descriptor

    fn find_joliet_descriptor(data : Bytes) -> JolietDescriptor? raise IsoError

    查找 Joliet 补充卷描述符:类型 2,偏移 88 的转义序列为 %/@、%/C 或 %/E。

    normalize_iso_name

    fn normalize_iso_name(raw : String) -> String

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

    normalize_path

    fn normalize_path(path : String) -> String

    规整路径:保证以 / 开头、去掉结尾 /、折叠重复 /。

    parse_boot_catalog

    fn parse_boot_catalog(data : Bytes, catalog_lba : Int) -> BootInfo raise IsoError

    解析 El Torito 启动目录:校验项 + 第一个启动项。

    parse_directory_record

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

    解析一条 ISO 9660(ASCII 名字)目录记录。

    parse_directory_record_in

    fn parse_directory_record_in(reader : ByteReader, joliet : Bool) -> (DirectoryRecord, Int) raise IsoError

    从 reader 当前位置解析一条目录记录,返回记录本身与消耗的字节数。

    joliet 为 true 时名字按 UCS-2 大端解码(补充卷描述符里的记录)。 返回长度为 0 表示目录数据在本扇区后面是填充。

    parse_primary_volume_descriptor

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

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

    parse_rock_ridge

    fn parse_rock_ridge(data : Bytes, block_size : Int, system_use : Bytes) -> RockRidge raise IsoError

    解析一条记录的 system use area,得到 Rock Ridge 信息。

    parse_susp

    fn parse_susp(data : Bytes, block_size : Int, area : Bytes, info : RockRidge, depth : Int) -> Unit raise IsoError

    解析一段 SUSP 条目流,把结果累加到 info 上。

    data / block_size 用于跟随 CE 续接区;depth 限制续接层数,防止畸形映像造成死循环。

    scan_volume_descriptors

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

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