Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
159 changes: 92 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
@@ -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) | ✅ | ❌ |

<img src="https://codecov.io/gh/bgrewell/iso-kit/graphs/sunburst.svg?token=D15C46IECF"/>
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.
See [docs/ROADMAP.md](docs/ROADMAP.md) for the detailed phase plan and
remaining work (UDF write support, multi-extent write, and more).
134 changes: 115 additions & 19 deletions cmd/isocreate/main.go
Original file line number Diff line number Diff line change
@@ -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())
}
4 changes: 2 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package main

import (
"fmt"
"fmt"
)

func main() {
fmt.Println("main template")
fmt.Println("main template")
}
Loading
Loading