Initial commit
This commit is contained in:
commit
1cc55d91ef
7 changed files with 912 additions and 0 deletions
190
internal/iso/reader.go
Normal file
190
internal/iso/reader.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue