From 5840d2998ec896f0e3b0dd4b3b2fbc5c8170b051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=B6rje=20Granberg?= Date: Thu, 30 Apr 2026 13:55:20 +0200 Subject: [PATCH 1/2] fix: allow Open() on external files and add OpenDiskImage API DirItem.Open() previously rejected DirItemExtFib (external files) with ErrIsDirectory. ExtFib uses the same block-vector layout as IntFib so the fix is to widen the guard to IsFile(). Also adds VBK.OpenDiskImage(path) which opens a .vmdk/.vhd/.vhdx entry and returns a flat readable DiskImage. Multi-extent VMDKs are reassembled automatically via the existing openVirtualDiskReader logic, with a raw-FibStream fallback for unrecognised disk formats. Co-Authored-By: Claude Sonnet 4.6 --- dir.go | 2 +- reader.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/dir.go b/dir.go index 089ced6..f17e1f3 100644 --- a/dir.go +++ b/dir.go @@ -219,7 +219,7 @@ func (d *DirItem) ListDir() (map[string]*DirItem, error) { } func (d *DirItem) Open() (*FibStream, error) { - if !d.IsInternalFile() { + if !d.IsFile() { return nil, fmt.Errorf("%w: %s", ErrIsDirectory, d.Name) } size, _ := d.Size() diff --git a/reader.go b/reader.go index 11cc2ac..69d97ab 100644 --- a/reader.go +++ b/reader.go @@ -151,6 +151,74 @@ func (v *VBK) Get(p string, base *DirItem) (*DirItem, error) { return item, nil } +// DiskImage is a readable, closeable view of a virtual disk stored in the VBK. +type DiskImage struct { + r io.ReaderAt + size uint64 + pos int64 + closers []io.Closer +} + +func (d *DiskImage) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + if uint64(d.pos) >= d.size { + return 0, io.EOF + } + limit := int64(d.size) - d.pos + if int64(len(p)) > limit { + p = p[:limit] + } + n, err := d.r.ReadAt(p, d.pos) + d.pos += int64(n) + return n, err +} + +func (d *DiskImage) Size() uint64 { return d.size } + +func (d *DiskImage) Close() error { + var first error + for _, c := range d.closers { + if err := c.Close(); err != nil && first == nil { + first = err + } + } + d.closers = nil + return first +} + +// OpenDiskImage opens the virtual disk at diskPath (a .vmdk, .vhd, or .vhdx entry +// inside the VBK) and returns a DiskImage that streams the full logical disk content. +// Multi-extent VMDKs are reassembled transparently. +func (v *VBK) OpenDiskImage(diskPath string) (*DiskImage, error) { + item, err := v.Get(diskPath, nil) + if err != nil { + return nil, err + } + + stream, err := item.Open() + if err != nil { + return nil, err + } + + disk := guestDiskFile{Path: diskPath, Item: item} + locked := &lockedReadSeekerAt{r: stream} + virtualReader, _, virtualDiskSize, closers, err := openVirtualDiskReader(v, disk, locked) + if err != nil { + // Fall back to the raw FibStream if the disk format is not recognised. + size, sizeErr := item.Size() + if sizeErr != nil { + stream.Close() + return nil, sizeErr + } + return &DiskImage{r: locked, size: size, closers: []io.Closer{stream}}, nil + } + + allClosers := append([]io.Closer{stream}, closers...) + return &DiskImage{r: virtualReader, size: virtualDiskSize, closers: allClosers}, nil +} + type SnapshotSlot struct { vbk *VBK offset int64 From 0a296fce6b7242ab32e1579b161a1705836f3be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=B6rje=20Granberg?= Date: Tue, 5 May 2026 07:50:29 +0200 Subject: [PATCH 2/2] docs: document OpenDiskImage in README Co-Authored-By: Claude Sonnet 4.6 --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 6658833..21fbcd5 100644 --- a/README.md +++ b/README.md @@ -85,12 +85,30 @@ func main() { - `(*DirItem).IterDir() ([]*DirItem, error)` - `(*DirItem).Open() (*FibStream, error)` - `(*DirItem).Properties() (PropertiesDictionary, error)` +- `(*VBK).OpenDiskImage(path string) (*DiskImage, error)` - `(*VBK).DiscoverGuest() (*Guest, error)` - `(*Guest).Volumes() []*GuestVolume` - `(*Guest).DefaultIndex() int` - `(*GuestVolume).ListDir(path string) ([]GuestEntry, error)` - `(*GuestVolume).ReadFile(path string, limit int64) ([]byte, error)` +## Disk Image Example + +```go +disk, err := backup.OpenDiskImage("/vm/disk0.vmdk") +if err != nil { + log.Fatal(err) +} +defer disk.Close() + +fmt.Printf("disk size: %d bytes\n", disk.Size()) + +buf := make([]byte, 512) +if _, err := disk.Read(buf); err != nil { + log.Fatal(err) +} +``` + ## Guest Volume Example ```go