diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..26c184a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + # xorriso, fdisk, and parted power the interoperability tests + # (Rock Ridge / Joliet / El Torito / hybrid partition verification). + # Tests skip gracefully when tools are missing, so this step failing + # soft would silently reduce coverage; keep it required. + - name: Install interop tools + run: sudo apt-get update && sudo apt-get install -y xorriso fdisk parted genisoimage + + - name: Check formatting + run: | + unformatted=$(gofmt -l ./pkg ./cmd) + if [ -n "$unformatted" ]; then + echo "gofmt required for:" && echo "$unformatted" && exit 1 + fi + + - name: Build + run: go build ./... + + - name: Vet + run: go vet ./... + + - name: Test + run: go test -race -count=1 ./... + + - name: Coverage + run: go test -count=1 -coverprofile=coverage.out -coverpkg=./... ./... && go tool cover -func=coverage.out | tail -1 diff --git a/README.md b/README.md index b7ef8ca..27c19c8 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,109 @@ # iso-kit +[![CI](https://github.com/bgrewell/iso-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/bgrewell/iso-kit/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/bgrewell/iso-kit/graph/badge.svg?token=D15C46IECF)](https://codecov.io/gh/bgrewell/iso-kit) -> **Notice:** This project is in an early development phase and may not yet be fully stable or feature complete. As it evolves, you may encounter significant changes to the API, behavior, and overall functionality. - -**iso-kit** is a Go library designed to simplify working with ISO 9660 disk images. Whether you're creating, extracting, or inspecting ISO files, iso-kit provides a reliable and feature-rich solution with advanced support for key extensions like Rock Ridge and El Torito. - -In addition to being a library for developers, **iso-kit** also includes some command line tools for working with ISO -files that can be installed via the command line using the commands below. +**iso-kit** is a Go library for working with ISO 9660 disk images: open existing +images, modify them, or build new ones from scratch — with Rock Ridge, Joliet, +El Torito boot, hybrid (USB-bootable) layouts, and read-only UDF support. + +> **Notice:** The API is pre-1.0 and may still change between releases. + +## Features + +- **Read**: parse ISO 9660 images including Rock Ridge (POSIX metadata, + symlinks), Joliet (Unicode names), El Torito boot catalogs, multi-extent + (>4 GiB) files, and path tables. Extract full trees to disk, symlinks + included. +- **Create**: build images from scratch or from a local directory tree. + Rock Ridge is written by default; Joliet is opt-in; identifiers can be + enforced at ISO 9660 interchange levels 1–3. +- **Modify**: open an image, add/remove files and directories, and save — + existing file content is streamed and relocated, never fully loaded into + memory. +- **Boot**: register BIOS and EFI El Torito boot entries (with isolinux + boot-info-table patching), and write hybrid MBR/GPT partition structures so + images boot from USB media. +- **UDF**: read-only support for ECMA-167 / UDF images (listing, reading, + extraction). + +## Library usage + +```go +import ( + "os" + + "github.com/bgrewell/iso-kit/pkg/iso9660" + "github.com/bgrewell/iso-kit/pkg/option" +) + +// Create an image from scratch. +img, _ := iso9660.Create("MYVOLUME", option.WithJolietEnabled(true)) +img.AddFile("docs/readme.txt", []byte("hello\n")) +img.AddLocalDirectory("./payload", "/payload") +out, _ := os.Create("out.iso") +img.Save(out) + +// Open, modify, save. +f, _ := os.Open("existing.iso") +img2, _ := iso9660.Open(f) +data, _ := img2.ReadFile("some/file.txt") +img2.AddFile("added.txt", data) +img2.RemoveFile("obsolete.txt") +out2, _ := os.Create("modified.iso") +img2.Save(out2) +``` -#### isoextract +Bootable, USB-writable images: + +```go +img.AddBootImage(iso9660.BootImageConfig{ + Path: "isolinux/isolinux.bin", Platform: boot.BIOS, + Emulation: boot.NoEmulation, LoadSize: 4, BootInfoTable: true, +}) +img.AddBootImage(iso9660.BootImageConfig{ + Path: "EFI/BOOT/efiboot.img", Platform: boot.EFI, Emulation: boot.NoEmulation, +}) +img.SetHybridBoot(iso9660.HybridBootConfig{ + MBRBootCode: isohdpfx, // e.g. syslinux isohdpfx.bin + EFIBootImagePath: "EFI/BOOT/efiboot.img", + AddGPT: true, +}) +``` -**isoextract** is a command line tool for extracting files from an ISO image. It can be installed using the following command: +## Command line tools ```bash go install github.com/bgrewell/iso-kit/cmd/isoextract@latest +go install github.com/bgrewell/iso-kit/cmd/isocreate@latest +go install github.com/bgrewell/iso-kit/cmd/isoview@latest ``` -*note: you may need to ensure that `$GOBIN` is in your `$PATH` you can do that by adding `export PATH=$PATH:$(go env GOPATH)/bin` -to your shell profile.* - - -## Project Goals - -The primary goals of **iso-kit** include: - -1. **Comprehensive ISO Handling**: - - Support for creating, extracting, and modifying ISO 9660 disk images. - - Advanced parsing and inspection tools for ISO metadata and structure. - -2. **Extension Support**: - - Full compatibility with Rock Ridge extensions and Joliet for enhanced file attributes. - - Support for El Torito extensions for handling bootable images. - -3. **Simplicity and Usability**: - - An intuitive API designed for developers. - - Detailed documentation and examples to accelerate integration. - -4. **Performance and Reliability**: - - Efficient handling of large ISO files. - - Robust error handling and validation for edge cases. - -5. **Future-Proof Design**: - - Modular and extensible architecture to accommodate future enhancements. - - Potential for additional features like UDF support or hybrid ISO handling. - ---- - -This library is ideal for developers building tools for ISO manipulation, virtual disk creation, or custom filesystem operations. - -Stay tuned for updates as we continue to expand functionality and refine the library! - -## ISO9660 Details - -### Support - - - [x] ISO 9660 - - [x] El Torito - - [x] Joliet - - [x] System Use Sharing Protocol (SUSP) - - [x] Rock Ridge - - [ ] CE (SUSP 5.1): - - [ ] PD (SUSP 5.2): - - [ ] SP (SUSP 5.3): - - [ ] ST (SUSP 5.4): - - [ ] ER (SUSP 5.5): - - [ ] ES (SUSP 5.6): +- **isoextract** — extract files and boot images from an ISO +- **isocreate** — build an ISO from a directory tree + (`isocreate -V MYVOL -o out.iso ./srcdir`, plus `--bios-boot`, + `--efi-boot`, `--isohybrid`, `--gpt`, `--joliet`, `--level`) +- **isoview** — inspect image structure and layout -### Current Limitations +*Note: ensure `$GOBIN` is in your `$PATH` +(`export PATH=$PATH:$(go env GOPATH)/bin`).* - - **Extract Only** - Currently this library only supports extraction of files and boot images. Support for creating ISOs is coming soon. - - **Rock Ridge** - While Rock Ridge is supported, some features may not be fully implemented. Please report any issues you encounter. - - **Joliet** - Joliet is supported, but some edge cases may not be fully implemented. Please report any issues you encounter. - - **El Torito** - El Torito is supported, but some edge cases may not be fully implemented. Please report any issues you encounter. - - **Validation** - This library has not been extensively tested and does not currently have any unit or functional tests so again, report any issues you encouter. +## Format support -## Test Coverage +| Capability | Read | Write | +|---|---|---| +| ISO 9660 | ✅ | ✅ | +| Rock Ridge (SUSP/RRIP: SP, CE, ER, PX, NM, SL, TF, PN, CL, PL, RE) | ✅ | ✅ | +| Joliet (UCS-2 hierarchy) | ✅ | ✅ | +| El Torito (multi-boot, BIOS + EFI sections, boot info table) | ✅ | ✅ | +| Hybrid MBR / GPT (USB boot) | ✅ | ✅ | +| Multi-extent files (>4 GiB) | ✅ | ❌ | +| UDF (ECMA-167) | ✅ | ❌ | - +Interoperability is verified in CI against xorriso (Rock Ridge, Joliet, +El Torito reporting) and util-linux fdisk / parted (hybrid partition tables). -## Development Notes +## Roadmap - - **Comments** - Comments have been added to structs and other parts of the code where it makes sense to document the purpose of fields including details such as encoding. \ No newline at end of file +See [docs/ROADMAP.md](docs/ROADMAP.md) for the detailed phase plan and +remaining work (UDF write support, multi-extent write, and more). diff --git a/cmd/isocreate/main.go b/cmd/isocreate/main.go index 686bc5d..f09709a 100644 --- a/cmd/isocreate/main.go +++ b/cmd/isocreate/main.go @@ -1,38 +1,134 @@ package main import ( - "github.com/bgrewell/iso-kit" - "github.com/bgrewell/iso-kit/pkg/option" + "fmt" "os" + "strings" + + "github.com/bgrewell/iso-kit/pkg/iso9660" + "github.com/bgrewell/iso-kit/pkg/iso9660/boot" + "github.com/bgrewell/iso-kit/pkg/option" + "github.com/bgrewell/iso-kit/pkg/version" + "github.com/bgrewell/usage" ) +func fail(u *usage.Usage, err error) { + u.PrintError(err) + os.Exit(1) +} + func main() { + u := usage.NewUsage( + usage.WithApplicationVersion(version.Version()), + usage.WithApplicationBranch(version.Branch()), + usage.WithApplicationBuildDate(version.Date()), + usage.WithApplicationCommitHash(version.Revision()), + usage.WithApplicationName("isocreate"), + usage.WithApplicationDescription("isocreate builds ISO9660 images from a directory tree, with support for Rock Ridge, Joliet, El Torito boot entries, and hybrid (USB-bootable) layouts."), + ) - name := "ubuntu-test-iso" - source := "/tmp/ubuntu-iso" - dest := "/tmp/created-ubuntu.iso" - _ = source //TODO: this will be passed in via an 'AddDir' command - _ = dest + help := u.AddBooleanOption("h", "help", false, "Show this help message", "optional", nil) - // Use values from the real ISO to simplify testing - name = "Ubuntu-Server 24.04.1 LTS amd64" - preparer := "XORRISO-1.5.4 2021.01.30.150001, LIBISOBURN-1.5.4, LIBISOFS-1.5.4, LIBBURN-1.5.4" + volumeID := u.AddStringOption("V", "volid", "ISOIMAGE", "Volume identifier", "", nil) + preparer := u.AddStringOption("p", "preparer", "", "Data preparer identifier", "", nil) + output := u.AddStringOption("o", "output", "", "Output ISO file path (required)", "", nil) - i, err := iso.Create(name, - option.WithPreparerID(preparer), - ) - if err != nil { - panic(err) + rockRidge := u.AddBooleanOption("rr", "rockridge", true, "Write Rock Ridge (POSIX metadata) extensions", "", nil) + joliet := u.AddBooleanOption("J", "joliet", false, "Write a Joliet (Windows Unicode names) hierarchy", "", nil) + level := u.AddIntegerOption("l", "level", 0, "ISO9660 interchange level to enforce (1, 2, or 3; 0 = relaxed)", "", nil) + + biosBoot := u.AddStringOption("b", "bios-boot", "", "Path (inside the image) of the BIOS boot image (El Torito, no emulation, 4-sector load, boot info table)", "", nil) + efiBoot := u.AddStringOption("e", "efi-boot", "", "Path (inside the image) of the EFI boot image (El Torito EFI platform entry)", "", nil) + hybrid := u.AddBooleanOption("H", "isohybrid", false, "Write hybrid MBR partition structures for USB boot", "", nil) + hybridMBR := u.AddStringOption("m", "isohybrid-mbr", "", "File containing MBR boot code for the hybrid layout (e.g. isohdpfx.bin)", "", nil) + gpt := u.AddBooleanOption("g", "gpt", false, "Add a GPT with an EFI System Partition entry (requires --efi-boot)", "", nil) + + sourceDir := u.AddArgument(1, "source-dir", "Directory tree to build the image from", "") + + if !u.Parse() { + fail(u, fmt.Errorf("failed to parse arguments")) + } + if *help { + u.PrintUsage() + os.Exit(0) + } + if *sourceDir == "" { + fail(u, fmt.Errorf("source directory is required")) + } + if *output == "" { + fail(u, fmt.Errorf("output path is required (-o)")) + } + if *gpt && *efiBoot == "" { + fail(u, fmt.Errorf("--gpt requires --efi-boot")) + } + + opts := []option.CreateOption{ + option.WithCreateRockRidgeEnabled(*rockRidge), + option.WithJolietEnabled(*joliet), + option.WithInterchangeLevel(*level), + } + if *preparer != "" { + opts = append(opts, option.WithPreparerID(*preparer)) } - f, err := os.Create(dest) + iso, err := iso9660.Create(*volumeID, opts...) if err != nil { - panic(err) + fail(u, fmt.Errorf("failed to create image: %w", err)) } - err = i.Save(f) + if err := iso.AddLocalDirectory(*sourceDir, "/"); err != nil { + fail(u, fmt.Errorf("failed to import %s: %w", *sourceDir, err)) + } + + if *biosBoot != "" { + err := iso.AddBootImage(iso9660.BootImageConfig{ + Path: strings.TrimPrefix(*biosBoot, "/"), + Platform: boot.BIOS, + Emulation: boot.NoEmulation, + LoadSize: 4, + BootInfoTable: true, + }) + if err != nil { + fail(u, fmt.Errorf("failed to add BIOS boot image: %w", err)) + } + } + if *efiBoot != "" { + err := iso.AddBootImage(iso9660.BootImageConfig{ + Path: strings.TrimPrefix(*efiBoot, "/"), + Platform: boot.EFI, + Emulation: boot.NoEmulation, + }) + if err != nil { + fail(u, fmt.Errorf("failed to add EFI boot image: %w", err)) + } + } + + if *hybrid || *gpt || *hybridMBR != "" { + cfg := iso9660.HybridBootConfig{ + EFIBootImagePath: strings.TrimPrefix(*efiBoot, "/"), + AddGPT: *gpt, + } + if *hybridMBR != "" { + code, err := os.ReadFile(*hybridMBR) + if err != nil { + fail(u, fmt.Errorf("failed to read MBR boot code: %w", err)) + } + cfg.MBRBootCode = code + } + if err := iso.SetHybridBoot(cfg); err != nil { + fail(u, fmt.Errorf("failed to configure hybrid boot: %w", err)) + } + } + + out, err := os.Create(*output) if err != nil { - panic(err) + fail(u, fmt.Errorf("failed to create output file: %w", err)) + } + defer out.Close() + + if err := iso.Save(out); err != nil { + fail(u, fmt.Errorf("failed to write image: %w", err)) } + fmt.Printf("Wrote %s (volume %q, %d sectors)\n", *output, *volumeID, iso.GetVolumeSize()) } diff --git a/cmd/main.go b/cmd/main.go index 1984689..8ca102b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,9 +1,9 @@ package main import ( - "fmt" + "fmt" ) func main() { - fmt.Println("main template") + fmt.Println("main template") } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 34bc62b..b845451 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -209,9 +209,13 @@ Goal: UDF support, USB-bootable hybrid ISOs, production CLI, comprehensive testi GPT mode writes a protective MBR + GPT with the EFI System Partition and a backup GPT appended after the ISO data - Verified with fdisk and parted: GPT disklabel and ESP recognized -- [ ] Rebuild `isocreate` CLI with proper argument parsing +- [x] Rebuild `isocreate` CLI with proper argument parsing + - Volume/preparer identity, Rock Ridge/Joliet/interchange-level toggles, + BIOS + EFI boot entries, isohybrid MBR/GPT flags - [ ] Comprehensive unit tests (directory, parser, pathtable, extensions, eltorito) -- [ ] CI pipeline (GitHub Actions) + README update +- [x] CI pipeline (GitHub Actions) + README update + - Build, vet, gofmt gate, race-enabled tests with xorriso/fdisk/parted + installed so interop tests run, coverage summary - [ ] Split `VolumeDescriptor` interface into base + filesystem-aware sub-interface ## Architectural Concerns diff --git a/pkg/udf/udf_test.go b/pkg/udf/udf_test.go index 2829af0..75fb0bd 100644 --- a/pkg/udf/udf_test.go +++ b/pkg/udf/udf_test.go @@ -114,7 +114,7 @@ func buildTestImage(t *testing.T) []byte { writeFE := func(block int, fileType byte, infoLength uint64, perms uint32, ads []shortAD) { off := (partStart + block) * SectorSize image[off+16+11] = fileType - image[off+16+18] = 0 // short_ad allocation + image[off+16+18] = 0 // short_ad allocation binary.LittleEndian.PutUint32(image[off+36:], 1000) // uid binary.LittleEndian.PutUint32(image[off+40:], 1000) // gid binary.LittleEndian.PutUint32(image[off+44:], perms) // permissions