Skip to content

Instantly share code, notes, and snippets.

@pwaller
Created December 8, 2015 16:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pwaller/8d72b11ccd77af619bd5 to your computer and use it in GitHub Desktop.
Save pwaller/8d72b11ccd77af619bd5 to your computer and use it in GitHub Desktop.
Experimental code for reading/writing VMDK files directly
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