190 lines
4.2 KiB
Go
190 lines
4.2 KiB
Go
|
|
// Package iso provides utilities for reading ISO 9660 image files.
|
||
|
|
package iso
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/kdomanski/iso9660"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Entry represents a file or directory inside an ISO image.
|
||
|
|
type Entry struct {
|
||
|
|
Name string
|
||
|
|
Path string // Full path within the ISO (forward-slash separated)
|
||
|
|
IsDir bool
|
||
|
|
Size int64
|
||
|
|
ModTime time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reader wraps an ISO image file.
|
||
|
|
type Reader struct {
|
||
|
|
path string
|
||
|
|
image *iso9660.Image
|
||
|
|
file *os.File
|
||
|
|
}
|
||
|
|
|
||
|
|
// Open opens an ISO file for reading. Call Close() when done.
|
||
|
|
func Open(isoPath string) (*Reader, error) {
|
||
|
|
f, err := os.Open(isoPath)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("opening iso: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
img, err := iso9660.OpenImage(f)
|
||
|
|
if err != nil {
|
||
|
|
f.Close()
|
||
|
|
return nil, fmt.Errorf("parsing iso: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return &Reader{path: isoPath, image: img, file: f}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Close releases the underlying file handle.
|
||
|
|
func (r *Reader) Close() error {
|
||
|
|
return r.file.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
// ListDir returns the entries in the given directory path within the ISO.
|
||
|
|
// Use an empty string or "/" for the root directory.
|
||
|
|
func (r *Reader) ListDir(dirPath string) ([]Entry, error) {
|
||
|
|
dirPath = cleanPath(dirPath)
|
||
|
|
|
||
|
|
root, err := r.image.RootDir()
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("reading root: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Navigate to the target directory.
|
||
|
|
dir, err := navigate(root, dirPath)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
children, err := dir.GetChildren()
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("listing directory: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
entries := make([]Entry, 0, len(children))
|
||
|
|
for _, child := range children {
|
||
|
|
name := child.Name()
|
||
|
|
// Skip the "." and ".." entries that some ISOs include.
|
||
|
|
if name == "." || name == ".." || name == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
var entryPath string
|
||
|
|
if dirPath == "" {
|
||
|
|
entryPath = name
|
||
|
|
} else {
|
||
|
|
entryPath = dirPath + "/" + name
|
||
|
|
}
|
||
|
|
|
||
|
|
entries = append(entries, Entry{
|
||
|
|
Name: name,
|
||
|
|
Path: entryPath,
|
||
|
|
IsDir: child.IsDir(),
|
||
|
|
Size: child.Size(),
|
||
|
|
ModTime: child.ModTime(),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
return entries, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// FileReader opens a file inside the ISO for streaming.
|
||
|
|
// The caller is responsible for closing the returned ReadCloser.
|
||
|
|
func (r *Reader) FileReader(filePath string) (io.ReadCloser, int64, error) {
|
||
|
|
filePath = cleanPath(filePath)
|
||
|
|
if filePath == "" {
|
||
|
|
return nil, 0, fmt.Errorf("no file path specified")
|
||
|
|
}
|
||
|
|
|
||
|
|
root, err := r.image.RootDir()
|
||
|
|
if err != nil {
|
||
|
|
return nil, 0, fmt.Errorf("reading root: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Split path into directory + filename.
|
||
|
|
dir := filepath.Dir(filePath)
|
||
|
|
name := filepath.Base(filePath)
|
||
|
|
|
||
|
|
var parent *iso9660.File
|
||
|
|
if dir == "." || dir == "/" {
|
||
|
|
parent = root
|
||
|
|
} else {
|
||
|
|
parent, err = navigate(root, cleanPath(dir))
|
||
|
|
if err != nil {
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
children, err := parent.GetChildren()
|
||
|
|
if err != nil {
|
||
|
|
return nil, 0, fmt.Errorf("listing parent dir: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, child := range children {
|
||
|
|
if strings.EqualFold(child.Name(), name) {
|
||
|
|
if child.IsDir() {
|
||
|
|
return nil, 0, fmt.Errorf("%q is a directory", filePath)
|
||
|
|
}
|
||
|
|
r := child.Reader()
|
||
|
|
if r == nil {
|
||
|
|
return nil, 0, fmt.Errorf("could not open reader for %q", filePath)
|
||
|
|
}
|
||
|
|
return io.NopCloser(r), child.Size(), nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil, 0, fmt.Errorf("file not found: %q", filePath)
|
||
|
|
}
|
||
|
|
|
||
|
|
// navigate traverses the directory tree inside an ISO to find the
|
||
|
|
// directory at cleanedPath (e.g. "foo/bar/baz").
|
||
|
|
func navigate(root *iso9660.File, cleanedPath string) (*iso9660.File, error) {
|
||
|
|
if cleanedPath == "" {
|
||
|
|
return root, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
parts := strings.Split(cleanedPath, "/")
|
||
|
|
current := root
|
||
|
|
|
||
|
|
for _, part := range parts {
|
||
|
|
if part == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
children, err := current.GetChildren()
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("listing dir: %w", err)
|
||
|
|
}
|
||
|
|
found := false
|
||
|
|
for _, child := range children {
|
||
|
|
if strings.EqualFold(child.Name(), part) {
|
||
|
|
if !child.IsDir() {
|
||
|
|
return nil, fmt.Errorf("%q is not a directory", part)
|
||
|
|
}
|
||
|
|
current = child
|
||
|
|
found = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !found {
|
||
|
|
return nil, fmt.Errorf("directory not found: %q", part)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return current, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// cleanPath normalises a path: strips leading slash and cleans traversal.
|
||
|
|
func cleanPath(p string) string {
|
||
|
|
p = filepath.ToSlash(filepath.Clean("/" + p))
|
||
|
|
p = strings.TrimPrefix(p, "/")
|
||
|
|
return p
|
||
|
|
}
|