diff --git a/extractor/filesystem/embeddedfs/vmdk/vmdk.go b/extractor/filesystem/embeddedfs/vmdk/vmdk.go index 1998fa86..214b7e3b 100644 --- a/extractor/filesystem/embeddedfs/vmdk/vmdk.go +++ b/extractor/filesystem/embeddedfs/vmdk/vmdk.go @@ -20,35 +20,38 @@ import ( "compress/zlib" "context" "encoding/binary" - "errors" "fmt" "io" + "io/fs" "os" - "path/filepath" + "path" "strings" "sync" + "time" + "github.com/diskfs/go-diskfs" + diskfsfilesystem "github.com/diskfs/go-diskfs/filesystem" + "github.com/diskfs/go-diskfs/filesystem/fat32" "github.com/google/osv-scalibr/extractor/filesystem" - "github.com/google/osv-scalibr/extractor/filesystem/embeddedfs/common" + scalibrfs "github.com/google/osv-scalibr/fs" "github.com/google/osv-scalibr/inventory" "github.com/google/osv-scalibr/plugin" + "github.com/masahiro331/go-ext4-filesystem/ext4" ) const ( // Name is the unique identifier for the vmdk extractor. - Name = "embeddedfs/vmdk" - // SectorSize is the default sector size (512 bytes). - SectorSize = 512 - // SparseMagic is always 'KDMV'. - SparseMagic = 0x564d444b - // GDAtEnd indicates that the Grain Directory is stored in the footer at the end of the VMDK file. - GDAtEnd = 0xFFFFFFFFFFFFFFFF - // DefaultGrainSec is default sectors if header invalid (64KiB). - DefaultGrainSec = 128 + Name = "embeddedfs/vmdk" + SectorSize = 512 + SPARSE_MAGIC = 0x564d444b // 'KDMV' + GDAtEnd = 0xFFFFFFFFFFFFFFFF + DefaultGrainSec = 128 // sectors if header invalid (64KiB) + defaultPageSize = 1024 * 1024 + defaultCacheSize = 100 * 1024 * 1024 ) -// sparseExtentHeader defines the VMDK sparse extent header structure. -type sparseExtentHeader struct { +// SparseExtentHeader defines the VMDK sparse extent header structure. +type SparseExtentHeader struct { MagicNumber uint32 Version uint32 Flags uint32 @@ -57,8 +60,8 @@ type sparseExtentHeader struct { DescriptorOffset uint64 DescriptorSize uint64 NumGTEsPerGT uint32 - RGDOffset uint64 - GDOffset uint64 + RgdOffset uint64 + GdOffset uint64 OverHead uint64 UncleanShutdown byte SingleEndLineChar byte @@ -69,8 +72,8 @@ type sparseExtentHeader struct { Pad [433]byte } -// gdgtInfo holds GD/GT allocation information. -type gdgtInfo struct { +// GDGTInfo holds GD/GT allocation information. +type GDGTInfo struct { GTEs uint64 GTs uint32 GDsectors uint32 @@ -81,7 +84,7 @@ type gdgtInfo struct { // Extractor implements the filesystem.Extractor interface for vmdk. type Extractor struct{} -// New returns a new VMDK extractor. +// New returns a new ova extractor. func New() filesystem.Extractor { return &Extractor{} } @@ -103,7 +106,13 @@ func (e *Extractor) Version() int { // Requirements returns the requirements for the extractor. func (e *Extractor) Requirements() *plugin.Capabilities { - return &plugin.Capabilities{} + return &plugin.Capabilities{ + OS: plugin.OSAny, + Network: plugin.NetworkAny, + DirectFS: true, // Requires direct filesystem access for GetRealPath + RunningSystem: false, + ExtractFromDirs: false, + } } // FileRequired checks if the file is a .vmdk file based on its extension. @@ -112,52 +121,118 @@ func (e *Extractor) FileRequired(api filesystem.FileAPI) bool { return strings.HasSuffix(strings.ToLower(path), ".vmdk") } -// Extract returns an Inventory with embedded filesystems which contains mount functions for each filesystem in the .vmdk file. +// Extract returns an Inventory with a DiskImage for each partition in the .vmdk file. func (e *Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { vmdkPath, err := input.GetRealPath() if err != nil { - return inventory.Inventory{}, fmt.Errorf("failed to get real path for %s: %w", input.Path, err) - } - // If called on a virtual FS, clean up the temporary directory - if input.Root == "" { - defer func() { - dir := filepath.Dir(vmdkPath) - if err := os.RemoveAll(dir); err != nil { - fmt.Printf("os.RemoveAll(%q): %v\n", dir, err) - } - }() + return inventory.Inventory{}, fmt.Errorf("failed to get real path for %s: %v", input.Path, err) } // Create a temporary file for the raw disk image tmpRaw, err := os.CreateTemp("", "scalibr-vmdk-raw-*.raw") if err != nil { - return inventory.Inventory{}, fmt.Errorf("failed to create temporary raw file: %w", err) + return inventory.Inventory{}, fmt.Errorf("failed to create temporary raw file: %v", err) } tmpRawPath := tmpRaw.Name() // Convert VMDK to raw if err := convertVMDKToRaw(vmdkPath, tmpRawPath); err != nil { os.Remove(tmpRawPath) - return inventory.Inventory{}, fmt.Errorf("failed to convert %s to raw image: %w", vmdkPath, err) + return inventory.Inventory{}, fmt.Errorf("failed to convert %s to raw image: %v", vmdkPath, err) + } + + // Open the raw disk image with go-diskfs + disk, err := diskfs.Open(tmpRawPath, diskfs.WithOpenMode(diskfs.ReadOnly)) + if err != nil { + os.Remove(tmpRawPath) + return inventory.Inventory{}, fmt.Errorf("failed to open raw disk image %s: %v", tmpRawPath, err) } - // Retrieve all partitions and the associated disk handle from the raw disk image. - partitionList, disk, err := common.GetDiskPartitions(tmpRawPath) + // Get the partition table + partitions, err := disk.GetPartitionTable() if err != nil { disk.Close() os.Remove(tmpRawPath) - return inventory.Inventory{}, err + return inventory.Inventory{}, fmt.Errorf("failed to get partition table: %v", err) + } + partitionList := partitions.GetPartitions() + if len(partitionList) == 0 { + disk.Close() + os.Remove(tmpRawPath) + return inventory.Inventory{}, fmt.Errorf("no partitions found in raw disk image") } // Create a reference counter for the temporary file var refCount int32 var refMu sync.Mutex - // Create an Embedded filesystem for each valid partition + // Create a DiskImage for each valid partition var embeddedFSs []*inventory.EmbeddedFS for i, p := range partitionList { partitionIndex := i + 1 // go-diskfs uses 1-based indexing - getEmbeddedFS := common.NewPartitionEmbeddedFSGetter("vmdk", partitionIndex, p, disk, tmpRawPath, &refMu, &refCount) + + getEmbeddedFS := func(ctx context.Context) (scalibrfs.FS, error) { + + // Open raw image for ext4 parser + f, err := os.Open(tmpRawPath) + if err != nil { + return nil, fmt.Errorf("failed to open raw image %s: %v", tmpRawPath, err) + } + + // Get partition offset and size (They are already multiplied by sector size) + offset := p.GetStart() + size := p.GetSize() + section := io.NewSectionReader(f, offset, size) + fsType := detectFilesystem(section, 0) + + switch fsType { + case "ext4": + fs, err := ext4.NewFS(*section, nil) + if err != nil { + f.Close() + return nil, fmt.Errorf("failed to create ext4 filesystem for partition %d: %v", partitionIndex, err) + } + refMu.Lock() + refCount++ + refMu.Unlock() + ext4fs := &ext4FS{ + fs: fs, + file: f, + tmpRawPath: tmpRawPath, + refCount: &refCount, + refMu: &refMu, + } + return ext4fs, nil + case "FAT32": + f.Close() // Close the file as GetFilesystem reopens it + fs, err := disk.GetFilesystem(partitionIndex) + if err != nil { + return nil, fmt.Errorf("failed to get filesystem for partition %d: %v", partitionIndex, err) + } + fat32fs, ok := fs.(*fat32.FileSystem) + if !ok { + return nil, fmt.Errorf("partition %d is not a FAT32 filesystem", partitionIndex) + } + f, err = os.Open(tmpRawPath) + if err != nil { + return nil, fmt.Errorf("failed to reopen raw image %s: %v", tmpRawPath, err) + } + refMu.Lock() + refCount++ + refMu.Unlock() + return &fat32FS{ + fs: fat32fs, + file: f, + tmpRawPath: tmpRawPath, + refCount: &refCount, + refMu: &refMu, + }, nil + default: + f.Close() + return nil, fmt.Errorf("unsupported filesystem type %s for partition %d", fsType, partitionIndex) + } + } + embeddedFSs = append(embeddedFSs, &inventory.EmbeddedFS{ Path: fmt.Sprintf("%s:%d", vmdkPath, partitionIndex), GetEmbeddedFS: getEmbeddedFS, @@ -166,14 +241,302 @@ func (e *Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (i return inventory.Inventory{EmbeddedFSs: embeddedFSs}, nil } +// detectFilesystem identifies the filesystem type by magic bytes +func detectFilesystem(r io.ReaderAt, offset int64) string { + buf := make([]byte, 4096) + _, err := r.ReadAt(buf, offset) + if err != nil { + return fmt.Sprintf("read error: %v", err) + } + // EXT4 magic at offset 0x438 + if len(buf) > 0x438+2 { + if binary.LittleEndian.Uint16(buf[0x438:0x43A]) == 0xEF53 { + return "ext4" + } + } + // FAT32: "FAT32 " at offset 0x52 + if len(buf) > 0x52+8 { + if string(buf[0x52:0x52+8]) == "FAT32 " { + return "FAT32" + } + } + return "unknown" +} + +// ext4FS wraps go-ext4-filesystem to implement scalibrfs.FS +type ext4FS struct { + fs *ext4.FileSystem + file *os.File + tmpRawPath string + refCount *int32 + refMu *sync.Mutex +} + +func (e *ext4FS) Open(name string) (fs.File, error) { + //fmt.Printf("ext4FS Open(%s)", name) + file, err := e.fs.Open(name) + if err != nil { + //fmt.Printf("ext4FS.Open(%q) failed: %v\n", name, err) + return nil, fmt.Errorf("failed to open file %s: %v", name, err) + } + ext4File, ok := file.(*ext4.File) + if !ok { + return nil, fmt.Errorf("opened file %s is not an ext4.File", name) + } + return &ext4FileWrapper{file: ext4File, name: name}, nil +} + +func (e *ext4FS) ReadDir(name string) ([]fs.DirEntry, error) { + entries, err := e.fs.ReadDir(name) + if err != nil { + fmt.Printf("ext4.ReadDir(%q) failed: %v\n", name, err) + return nil, fmt.Errorf("failed to read directory %s: %v", name, err) + } + return entries, nil +} + +func (e *ext4FS) Stat(name string) (fs.FileInfo, error) { + if name == "." || name == "" || name == "/" { + // Return synthetic FileInfo for root directory + return &fileInfo{ + name: name, + isDir: true, + modTime: time.Now(), + }, nil + } + info, err := e.fs.Stat(name) + if err != nil { + fmt.Printf("ext4FS.Stat(%q) failed: %v\n", name, err) + return nil, fmt.Errorf("failed to stat %s: %v", name, err) + } + return info, nil +} + +func (e *ext4FS) Close() error { + e.refMu.Lock() + defer e.refMu.Unlock() + if e.file == nil { + return nil // Already closed + } + *e.refCount-- + if *e.refCount == 0 { + err := e.file.Close() + e.file = nil // Prevent double close + if err != nil { + return fmt.Errorf("failed to close raw file %s: %v", e.tmpRawPath, err) + } + if err := os.Remove(e.tmpRawPath); err != nil { + return fmt.Errorf("failed to remove temporary raw file %s: %v", e.tmpRawPath, err) + } + } + return nil +} + +// ext4FileWrapper wraps ext4.File to implement fs.File and io.ReaderAt +type ext4FileWrapper struct { + file *ext4.File + name string +} + +func (e *ext4FileWrapper) Read(p []byte) (int, error) { + return e.file.Read(p) +} + +func (e *ext4FileWrapper) Close() error { + return e.file.Close() +} + +func (e *ext4FileWrapper) Stat() (fs.FileInfo, error) { + return e.file.Stat() +} + +// Implement io.ReaderAt for scalibrfs.File (assumed to require it) +func (e *ext4FileWrapper) ReadAt(p []byte, off int64) (int, error) { + // Read the entire file into memory (suitable for small files like private-key.pem) + data, err := io.ReadAll(e.file) + if err != nil { + return 0, fmt.Errorf("failed to read file %s: %v", e.name, err) + } + if off >= int64(len(data)) { + return 0, io.EOF + } + n := copy(p, data[off:]) + if n < len(p) { + return n, io.EOF + } + return n, nil +} + +// fat32FS wraps go-diskfs fat32.FileSystem to implement scalibrfs.FS +type fat32FS struct { + fs *fat32.FileSystem + file *os.File + tmpRawPath string + refCount *int32 + refMu *sync.Mutex +} + +func (f *fat32FS) Open(name string) (fs.File, error) { + file, err := f.fs.OpenFile(name, os.O_RDONLY) + if err != nil { + //fmt.Printf("fat32FS.Open(%q) failed: %v\n", name, err) + return nil, fmt.Errorf("failed to open file %s: %v", name, err) + } + return &fat32FileWrapper{file: file, name: name, fs: f.fs}, nil +} + +func (f *fat32FS) ReadDir(name string) ([]fs.DirEntry, error) { + if name == "." || name == "" { + // Return synthetic FileInfo for root directory + name = "/" + } + fis, err := f.fs.ReadDir(name) + if err != nil { + //fmt.Printf("fat32FS.ReadDir(%q) failed: %v\n", name, err) + return nil, fmt.Errorf("failed to read directory %s: %v", name, err) + } + entries := make([]fs.DirEntry, 0, len(fis)) + for _, fi := range fis { + entries = append(entries, fs.FileInfoToDirEntry(fi)) + } + return entries, nil +} + +func (f *fat32FS) Stat(name string) (fs.FileInfo, error) { + if name == "/" || name == "" || name == "." { + // Return synthetic FileInfo for root directory + return &fileInfo{ + name: name, + isDir: true, + modTime: time.Now(), + }, nil + } + fis, err := f.fs.ReadDir(path.Dir(name)) + if err != nil { + //fmt.Printf("fat32FS.Stat(%q) failed: %v\n", name, err) + return nil, fmt.Errorf("failed to stat %s: %v", name, err) + } + base := path.Base(name) + for _, fi := range fis { + if fi.Name() == base { + return fi, nil + } + } + return nil, fmt.Errorf("file %s not found", name) +} + +func (f *fat32FS) Close() error { + f.refMu.Lock() + defer f.refMu.Unlock() + if f.file == nil { + return nil + } + *f.refCount-- + if *f.refCount == 0 { + err := f.file.Close() + f.file = nil + if err != nil { + return fmt.Errorf("failed to close raw file %s: %v", f.tmpRawPath, err) + } + if err := os.Remove(f.tmpRawPath); err != nil { + return fmt.Errorf("failed to remove temporary raw file %s: %v", f.tmpRawPath, err) + } + } + return nil +} + +// fat32FileWrapper wraps diskfsfilesystem.File to implement scalibrfs.FS and io.ReaderAt +type fat32FileWrapper struct { + file diskfsfilesystem.File + name string + fs *fat32.FileSystem +} + +func (f *fat32FileWrapper) Read(p []byte) (int, error) { + return f.file.Read(p) +} + +func (f *fat32FileWrapper) Close() error { + return f.file.Close() +} + +func (f *fat32FileWrapper) Stat() (fs.FileInfo, error) { + if f.name == "/" || f.name == "" || f.name == "." { + // Return synthetic FileInfo for root directory + return &fileInfo{ + name: f.name, + isDir: true, + modTime: time.Now(), + }, nil + } + fis, err := f.fs.ReadDir(path.Dir(f.name)) + if err != nil { + return nil, fmt.Errorf("failed to read directory %s: %v", path.Dir(f.name), err) + } + base := path.Base(f.name) + for _, fi := range fis { + if fi.Name() == base { + return fi, nil + } + } + return nil, fmt.Errorf("file %s not found", f.name) +} + +func (f *fat32FileWrapper) ReadAt(p []byte, off int64) (int, error) { + // diskfsfilesystem.File implements io.ReadWriteSeeker, so we can use Seek and Read + _, err := f.file.Seek(off, io.SeekStart) + if err != nil { + return 0, fmt.Errorf("failed to seek to offset %d in file %s: %v", off, f.name, err) + } + n, err := f.file.Read(p) + if err != nil { + return n, fmt.Errorf("failed to read at offset %d in file %s: %v", off, f.name, err) + } + return n, nil +} + +// fileInfo is a simple implementation of fs.FileInfo for the root directory +type fileInfo struct { + name string + isDir bool + modTime time.Time +} + +func (fi *fileInfo) Name() string { + return fi.name +} + +func (fi *fileInfo) Size() int64 { + return 0 +} + +func (fi *fileInfo) Mode() fs.FileMode { + if fi.isDir { + return fs.ModeDir | 0755 + } + return 0644 +} + +func (fi *fileInfo) ModTime() time.Time { + return fi.modTime +} + +func (fi *fileInfo) IsDir() bool { + return fi.isDir +} + +func (fi *fileInfo) Sys() interface{} { + return nil +} + // VMDK conversion functions // readHeaderAt reads the 512-byte header at the given offset. -func readHeaderAt(r io.ReaderAt, offset int64) (sparseExtentHeader, error) { - var hdr sparseExtentHeader +func readHeaderAt(r io.ReaderAt, offset int64) (SparseExtentHeader, error) { + var hdr SparseExtentHeader buf := make([]byte, SectorSize) n, err := r.ReadAt(buf, offset) - if err != nil && !errors.Is(err, io.EOF) { + if err != nil && err != io.EOF { return hdr, fmt.Errorf("read header at %d: %w", offset, err) } if n < SectorSize { @@ -183,15 +546,15 @@ func readHeaderAt(r io.ReaderAt, offset int64) (sparseExtentHeader, error) { if err := binary.Read(br, binary.LittleEndian, &hdr); err != nil { return hdr, fmt.Errorf("parse header: %w", err) } - if hdr.MagicNumber != SparseMagic { + if hdr.MagicNumber != SPARSE_MAGIC { return hdr, fmt.Errorf("invalid magic: 0x%x", hdr.MagicNumber) } return hdr, nil } -// readFooterIfGDAtEnd reads the footer header near EOF if GDOffset is GDAtEnd. -func readFooterIfGDAtEnd(f *os.File, hdr *sparseExtentHeader) error { - if hdr.GDOffset != GDAtEnd { +// readFooterIfGDAtEnd reads the footer header near EOF if GdOffset is GDAtEnd. +func readFooterIfGDAtEnd(f *os.File, hdr *SparseExtentHeader) error { + if hdr.GdOffset != GDAtEnd { return nil } fi, err := f.Stat() @@ -199,17 +562,17 @@ func readFooterIfGDAtEnd(f *os.File, hdr *sparseExtentHeader) error { return err } if fi.Size() < 1536 { - return errors.New("file too small to contain footer/EOS") + return fmt.Errorf("file too small to contain footer/EOS") } base := fi.Size() - 1536 footerHeaderBlock := make([]byte, 512) if _, err := f.ReadAt(footerHeaderBlock, base+512); err != nil { return fmt.Errorf("read footer header block: %w", err) } - if binary.LittleEndian.Uint32(footerHeaderBlock[0:4]) != SparseMagic { + if binary.LittleEndian.Uint32(footerHeaderBlock[0:4]) != SPARSE_MAGIC { return fmt.Errorf("footer magic mismatch: 0x%x", binary.LittleEndian.Uint32(footerHeaderBlock[0:4])) } - var foot sparseExtentHeader + var foot SparseExtentHeader r := bytes.NewReader(footerHeaderBlock[4:]) if err := binary.Read(r, binary.LittleEndian, &foot); err != nil { return fmt.Errorf("parse footer header: %w", err) @@ -218,6 +581,46 @@ func readFooterIfGDAtEnd(f *os.File, hdr *sparseExtentHeader) error { return nil } +// alignUp aligns to sector boundary (upwards). +func alignUp(x int64, sector int64) int64 { + if x%sector == 0 { + return x + } + return ((x / sector) + 1) * sector +} + +// copyNToFileAt copies n bytes from src to out at offset. +func copyNToFileAt(out *os.File, src io.Reader, offset int64, n int64) error { + const chunk = 1 << 20 // 1MiB + buf := make([]byte, chunk) + written := int64(0) + for written < n { + toRead := chunk + if n-written < int64(toRead) { + toRead = int(n - written) + } + read, rerr := io.ReadFull(src, buf[:toRead]) + if read > 0 { + if wn, werr := out.WriteAt(buf[:read], offset+written); werr != nil { + return werr + } else if wn != read { + return io.ErrShortWrite + } + written += int64(read) + } + if rerr != nil { + if rerr == io.EOF || rerr == io.ErrUnexpectedEOF { + if written == n { + break + } + return rerr + } + return rerr + } + } + return nil +} + // readStreamMarker reads a VMDK stream marker. func readStreamMarker(f *os.File) (val uint64, size uint32, typ uint32, data []byte, err error) { head := make([]byte, 12) @@ -259,8 +662,8 @@ func readStreamMarker(f *os.File) (val uint64, size uint32, typ uint32, data []b } // convertStreamOptimizedExtent converts a stream-optimized VMDK extent. -func convertStreamOptimizedExtent(f *os.File, out *os.File, hdr sparseExtentHeader) error { - if hdr.GDOffset == GDAtEnd { +func convertStreamOptimizedExtent(f *os.File, out *os.File, hdr SparseExtentHeader) error { + if hdr.GdOffset == GDAtEnd { if err := readFooterIfGDAtEnd(f, &hdr); err != nil { return fmt.Errorf("read footer: %w", err) } @@ -282,7 +685,7 @@ func convertStreamOptimizedExtent(f *os.File, out *os.File, hdr sparseExtentHead for { val, size, typ, payload, err := readStreamMarker(f) if err != nil { - if errors.Is(err, io.EOF) { + if err == io.EOF { break } return fmt.Errorf("read marker: %w", err) @@ -301,7 +704,7 @@ func convertStreamOptimizedExtent(f *os.File, out *os.File, hdr sparseExtentHead } dec, derr := io.ReadAll(zr) zr.Close() - if derr != nil && !errors.Is(derr, io.EOF) { + if derr != nil && derr != io.EOF { return fmt.Errorf("zlib read at lba %d: %w", lba, derr) } if int64(len(dec)) < grainBytes { @@ -342,8 +745,8 @@ func convertStreamOptimizedExtent(f *os.File, out *os.File, hdr sparseExtentHead if _, err := io.ReadFull(f, meta); err != nil { return fmt.Errorf("read footer meta: %w", err) } - if len(meta) >= 4 && binary.LittleEndian.Uint32(meta[0:4]) == SparseMagic { - var foot sparseExtentHeader + if len(meta) >= 4 && binary.LittleEndian.Uint32(meta[0:4]) == SPARSE_MAGIC { + var foot SparseExtentHeader br := bytes.NewReader(meta[4:]) if err := binary.Read(br, binary.LittleEndian, &foot); err == nil { hdr = foot @@ -375,7 +778,7 @@ func convertStreamOptimizedExtent(f *os.File, out *os.File, hdr sparseExtentHead } // getGDGT computes GD/GT sizes and allocates structures. -func getGDGT(hdr sparseExtentHeader) (*gdgtInfo, error) { +func getGDGT(hdr SparseExtentHeader) (*GDGTInfo, error) { if hdr.GrainSize < 1 || hdr.GrainSize > 128 || (hdr.GrainSize&(hdr.GrainSize-1)) != 0 { return nil, fmt.Errorf("invalid grainSize %d", hdr.GrainSize) } @@ -402,7 +805,7 @@ func getGDGT(hdr sparseExtentHeader) (*gdgtInfo, error) { return nil, fmt.Errorf("gd/gt allocation too large: %d bytes", totalBytes) } gdarr := make([]uint32, (GDsectors*SectorSize)/4+(GTsectors*GTs*SectorSize)/4) - info := &gdgtInfo{ + info := &GDGTInfo{ GTEs: GTEs, GTs: GTs, GDsectors: GDsectors, @@ -413,34 +816,34 @@ func getGDGT(hdr sparseExtentHeader) (*gdgtInfo, error) { } // readGD reads GD sectors from file. -func readGD(f *os.File, hdr sparseExtentHeader, info *gdgtInfo) error { - if hdr.GDOffset == 0 { - return errors.New("no GD offset") +func readGD(f *os.File, hdr SparseExtentHeader, info *GDGTInfo) error { + if hdr.GdOffset == 0 { + return fmt.Errorf("no GD offset") } - start := int64(hdr.GDOffset) * SectorSize + start := int64(hdr.GdOffset) * SectorSize totalBytes := int64(info.GDsectors) * SectorSize buf := make([]byte, totalBytes) if _, err := f.ReadAt(buf, start); err != nil { return fmt.Errorf("read GD at %d: %w", start, err) } - for i := range int(info.GDsectors * SectorSize / 4) { + for i := 0; i < int(info.GDsectors*SectorSize/4); i++ { info.gd[i] = binary.LittleEndian.Uint32(buf[i*4 : i*4+4]) } return nil } // convertMonolithicSparse converts a monolithic sparse VMDK. -func convertMonolithicSparse(f *os.File, out *os.File, hdr sparseExtentHeader) error { +func convertMonolithicSparse(f *os.File, out *os.File, hdr SparseExtentHeader) error { info, err := getGDGT(hdr) if err != nil { return err } - GDOffset := hdr.GDOffset - if hdr.RGDOffset != 0 { - GDOffset = hdr.RGDOffset + gdOffset := hdr.GdOffset + if hdr.RgdOffset != 0 { + gdOffset = hdr.RgdOffset } - if GDOffset == 0 || GDOffset == GDAtEnd { - return errors.New("gd offset missing for monolithicSparse") + if gdOffset == 0 || gdOffset == GDAtEnd { + return fmt.Errorf("gd offset missing for monolithicSparse") } if err := readGD(f, hdr, info); err != nil { return fmt.Errorf("readGD: %w", err) @@ -451,7 +854,7 @@ func convertMonolithicSparse(f *os.File, out *os.File, hdr sparseExtentHeader) e return fmt.Errorf("truncate out: %w", err) } numGTEsPerGT := int64(hdr.NumGTEsPerGT) - for g := range totalGrains { + for g := int64(0); g < totalGrains; g++ { gdIdx := int(g / numGTEsPerGT) gtIdx := int(g % numGTEsPerGT) if gdIdx >= len(info.gd) { @@ -492,13 +895,13 @@ func convertMonolithicSparse(f *os.File, out *os.File, hdr sparseExtentHeader) e } grainSector := int64(gte) grainOffset := grainSector * SectorSize - var toRead = grainBytes + var toRead int64 = grainBytes if g == totalGrains-1 { lastSectors := int64(hdr.Capacity % hdr.GrainSize) if lastSectors == 0 { lastSectors = int64(hdr.GrainSize) } - toRead = lastSectors * SectorSize + toRead = int64(lastSectors) * SectorSize } grainData := make([]byte, toRead) if _, err := f.ReadAt(grainData, grainOffset); err != nil {