Created
December 8, 2015 16:41
-
-
Save pwaller/8d72b11ccd77af619bd5 to your computer and use it in GitHub Desktop.
Experimental code for reading/writing VMDK files directly
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/binary" | |
"fmt" | |
"io" | |
"log" | |
"os" | |
"reflect" | |
) | |
const ( | |
// VMDKMagic is equal to "KDMV" | |
VMDKMagic = 0x564d444b | |
// SectorBytes is the bytes per sector. | |
SectorBytes = 512 | |
) | |
// Sector represents a sector count. | |
type Sector uint64 | |
// AsBytes returns the number of bytes this sector spans. | |
func (s Sector) AsBytes() int64 { return int64(s) * SectorBytes } | |
func (s Sector) String() string { return fmt.Sprintf("0x%x", s.AsBytes()) } | |
// Header represents the bytes appearing at the beginning of a VMDK file. | |
// https://www.vmware.com/support/developer/vddk/vmdk_50_technote.pdf | |
type Header struct { | |
MagicNumber uint32 | |
Version uint32 | |
Flags uint32 | |
Capacity Sector | |
GrainSize Sector | |
DescriptorOffset Sector | |
DescriptorSize Sector | |
NumGTEsPerGT uint32 | |
RgdOffset Sector | |
GdOffset Sector | |
OverHead Sector | |
UncleanShutdown uint8 | |
SingleEndLineChar byte | |
NonEndLineChar byte | |
DoubleEndLineChar1 byte | |
DoubleEndLineChar2 byte | |
CompressAlgorithm uint16 | |
Pad [433]uint8 | |
} | |
func (Header) Size() int64 { return int64(reflect.TypeOf(&Header{}).Size()) } | |
// Table represents a binary table of integers | |
type Table struct { | |
rs io.ReadSeeker | |
} | |
// NewTable constructs a Table from a ReadSeeker at offset. | |
func NewTable(rs io.ReaderAt, sector Sector) *Table { | |
const bigInteger = (1 << 62) - 1 | |
return &Table{io.NewSectionReader(rs, sector.AsBytes(), bigInteger)} | |
} | |
// ReadInt32 reads the n'th int32 from a table | |
func (t *Table) ReadInt32(n int64) (int32, error) { | |
var result int32 | |
const sizeOfInt32 = 4 | |
_, err := t.rs.Seek(int64(n)*sizeOfInt32, os.SEEK_SET) | |
if err != nil { | |
return -1, err | |
} | |
err = binary.Read(t.rs, binary.LittleEndian, &result) | |
if err != nil { | |
return -1, err | |
} | |
return result, nil | |
} | |
// ReadSector reads a number from 't' which represents a sector | |
func (t *Table) ReadSector(n int64) (Sector, error) { | |
s, err := t.ReadInt32(n) | |
return Sector(s), err | |
} | |
// VMDKReader implements read operations on certain VMDK files. | |
// https://www.vmware.com/support/developer/vddk/vmdk_50_technote.pdf | |
type VMDKReader struct { | |
fd io.ReaderAt | |
header Header | |
grainDirectory *Table | |
} | |
func NewVMDKReader(fd io.ReaderAt) (*VMDKReader, error) { | |
r := &VMDKReader{fd: fd} | |
hr := io.NewSectionReader(fd, 0, r.header.Size()+1024) | |
err := binary.Read(hr, binary.LittleEndian, &r.header) | |
if err != nil { | |
return nil, err | |
} | |
r.grainDirectory = NewTable(fd, r.header.GdOffset) | |
log.Printf("%#+v", r.header) | |
return r, nil | |
} | |
// ReadAt implements io.ReaderAt on a VMDK | |
func (r *VMDKReader) ReadAt(bs []byte, off int64) (int, error) { | |
offSector := Sector(off / SectorBytes) | |
const nGTEPerGT = 512 | |
grainTableCoverage := r.header.GrainSize * nGTEPerGT | |
gde := offSector / grainTableCoverage | |
grainTableSector, err := r.grainDirectory.ReadSector(int64(gde)) | |
if err != nil { | |
return 0, err | |
} | |
grainTable := NewTable(r.fd, grainTableSector) | |
entryIdx := (offSector % grainTableCoverage) / r.header.GrainSize | |
grainTableEntry, err := grainTable.ReadSector(int64(entryIdx)) | |
log.Printf("%v, %v", int64(gde), int64(entryIdx)) | |
const ( | |
GrainUnallocated = 0 | |
GrainZeros = 1 | |
) | |
switch grainTableEntry { | |
case GrainUnallocated, GrainZeros: | |
// should always return zeros. | |
panic("Unimplemented: reading from empty grain") | |
default: | |
} | |
// TODO(pwaller): ensure BS doesn't go over the size of the grain. | |
n, err := r.fd.ReadAt(bs, grainTableEntry.AsBytes()+off) | |
log.Printf("ReadAt %v: %q", off, bs[:n]) | |
return n, err | |
} | |
func main() { | |
fd, err := os.Open(os.Args[1]) | |
if err != nil { | |
log.Fatal(err) | |
} | |
r, err := NewVMDKReader(fd) | |
if err != nil { | |
log.Fatalf("NewVMDKReader: %v", err) | |
} | |
rd := io.NewSectionReader(r, 1*SectorBytes, 1024*1024) | |
io.Copy(os.Stdout, rd) | |
// t, err := gpt.ReadTable(rd, SectorBytes) | |
// if err != nil { | |
// log.Fatalf("Table read fail: %v", err) | |
// } | |
// | |
// log.Printf("Table %v parts", len(t.Partitions)) | |
// n, err := io.Copy(os.Stdout, rd) | |
// log.Printf("%v, %v", n, err) | |
// header, err := readVMDK(fd) | |
// if err != nil { | |
// log.Printf("Failed to read: %v", err) | |
// } | |
// | |
// bs := make([]byte, header.DescriptorSize*SectorBytes) | |
// _, err = io.ReadFull(fd, bs) | |
// if err != nil { | |
// log.Fatal(err) | |
// } | |
// | |
// if header.CompressAlgorithm != 0 { | |
// log.Fatal("Compressed grains unsupported") | |
// } | |
// | |
// gdTable := NewTable(fd, header.GdOffset) | |
// sector, err := gdTable.ReadSector(0) | |
// if err != nil { | |
// log.Fatalf("fatal reading sector: %v", err) | |
// } | |
// | |
// gd1Table := NewTable(fd, sector) | |
// sector, err = gd1Table.ReadSector(0) | |
// if err != nil { | |
// log.Fatalf("fatal reading sector: %v", err) | |
// } | |
// | |
// log.Printf("Sector: %v", sector) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment