vorteil/vorteil

View on GitHub
pkg/vmdk/sparse.go

Summary

Maintainability
C
1 day
Test Coverage
package vmdk

/**
 * SPDX-License-Identifier: Apache-2.0
 * Copyright 2020 vorteil.io Pty Ltd
 */

import (
    "encoding/binary"
    "fmt"
    "io"
    "strings"
)

type HolePredictor interface {
    Size() int64
    RegionIsHole(begin, size int64) bool
}

type SparseWriter struct {
    w io.WriteSeeker
    h HolePredictor

    totalDataSectors int64
    totalDataGrains  int64
    totalTables      int64
    totalGDSectors   int64
    totalGTSectors   int64

    hdr          *Header
    cursor       int64
    grainOffsets []int64
}

func sparseDescriptor(name string, totalDataGrains int64) string {

    template := `# Disk DescriptorFile
version=1
CID=%s
parentCID=ffffffff
createType="monolithicSparse"

# Extent description
RW %d SPARSE "%s.vmdk"

# The Disk Data Base
#DDB

ddb.virtualHWVersion = "10"
ddb.adapterType = "ide"
`

    uid := generateDiskUID()
    description := fmt.Sprintf(template, uid, totalDataGrains*SectorsPerGrain, name)
    return description
}

func (w *SparseWriter) writeSparseHeader() error {

    hdr := new(Header)
    hdr.MagicNumber = Magic
    hdr.Version = 1
    hdr.Flags = 0x3
    hdr.GrainSize = SectorsPerGrain
    hdr.DescriptorOffset = 1
    hdr.DescriptorSize = 20
    hdr.NumGTEsPerGT = TableMaxRows
    hdr.RGDOffset = 21
    hdr.SingleEndLineChar = '\n'
    hdr.NonEndLineChar = ' '
    hdr.DoubleEndLineChar1 = '\r'
    hdr.DoubleEndLineChar2 = '\n'
    hdr.CompressAlgorithm = 0

    w.totalTables = (w.totalDataGrains + TableMaxRows - 1) / TableMaxRows

    w.totalGDSectors = (w.totalTables*4 + SectorSize - 1) / SectorSize
    w.totalGTSectors = w.totalTables * TableSectors

    // GDOffset comes after the redundant grain directory
    // and its grain tables.
    hdr.GDOffset = hdr.RGDOffset + uint64(w.totalGDSectors+w.totalGTSectors)

    // Overhead is measured in grains, not sectors.
    // Includes everything before the start of the disk contents.
    // TODO: this seems like a bug...
    hdr.OverHead = (((uint64(2*(w.totalGDSectors+w.totalGTSectors)) + hdr.RGDOffset) + SectorsPerGrain - 1) / SectorsPerGrain) * SectorsPerGrain

    hdr.Capacity = uint64(w.totalDataSectors)

    w.hdr = hdr

    err := binary.Write(w.w, binary.LittleEndian, w.hdr)
    if err != nil {
        return err
    }

    return nil

}

func (w *SparseWriter) writeGrainData() error {
    var err error

    firstDataSector := int64(w.hdr.OverHead) * SectorsPerGrain

    // rgd
    firstDirSector := int64(w.hdr.RGDOffset)
    firstTableSector := firstDirSector + w.totalGDSectors
    offset := firstDirSector * SectorSize
    _, err = w.w.Seek(offset, io.SeekStart)
    if err != nil {
        return err
    }

    for i := int64(0); i < w.totalTables; i++ {
        tableSector := int64(0)
        if i < w.totalTables {
            tableSector = firstTableSector + i*4
        }
        err := binary.Write(w.w, binary.LittleEndian, uint32(tableSector))
        if err != nil {
            return err
        }
    }

    // rgt
    offset = firstTableSector * SectorSize
    _, err = w.w.Seek(offset, io.SeekStart)
    if err != nil {
        return err
    }

    for i := int64(0); i < w.totalDataGrains; i++ {

        grainSector := int64(w.grainOffsets[i])
        if i+1 < int64(len(w.grainOffsets)) && grainSector == int64(w.grainOffsets[i+1]) {
            grainSector = 0
        }
        grainSector /= SectorSize

        if i%TableMaxRows == 0 {
            table := i / TableMaxRows
            offset = firstTableSector*SectorSize + TableSectors*table
        }

        err := binary.Write(w.w, binary.LittleEndian, uint32(grainSector))
        if err != nil {
            return err
        }
    }

    // gd
    firstDirSector = int64(w.hdr.GDOffset)
    firstTableSector = firstDirSector + w.totalGDSectors
    offset = firstDirSector * SectorSize
    _, err = w.w.Seek(offset, io.SeekStart)
    if err != nil {
        return err
    }

    for i := int64(0); i < w.totalTables; i++ {
        tableSector := int64(0)
        if i < w.totalTables {
            tableSector = firstTableSector + i*4
        }
        err := binary.Write(w.w, binary.LittleEndian, uint32(tableSector))
        if err != nil {
            return err
        }
    }

    // gt
    offset = firstTableSector * SectorSize
    _, err = w.w.Seek(offset, io.SeekStart)
    if err != nil {
        return err
    }

    for i := int64(0); i < w.totalDataGrains; i++ {

        grainSector := int64(w.grainOffsets[i])
        if i+1 < int64(len(w.grainOffsets)) && grainSector == int64(w.grainOffsets[i+1]) {
            grainSector = 0
        }
        grainSector /= SectorSize

        if i%TableMaxRows == 0 {
            table := i / TableMaxRows
            offset = firstTableSector*SectorSize + TableSectors*table
        }

        err := binary.Write(w.w, binary.LittleEndian, uint32(grainSector))
        if err != nil {
            return err
        }
    }

    // pad
    offset = firstDataSector * SectorSize
    _, err = w.w.Seek(firstDataSector*SectorSize, io.SeekStart)
    if err != nil {
        return err
    }

    return nil
}

func (w *SparseWriter) init() error {
    w.totalDataSectors = (w.h.Size() + SectorSize - 1) / SectorSize
    w.totalDataGrains = (w.totalDataSectors + SectorsPerGrain - 1) / SectorsPerGrain

    // write header
    err := w.writeSparseHeader()
    if err != nil {
        return err
    }

    // write descriptor
    name := "disk"
    description := sparseDescriptor(name, w.totalDataGrains)
    _, err = io.Copy(w.w, strings.NewReader(description))
    if err != nil {
        return err
    }

    firstDataSector := int64(w.hdr.OverHead) * SectorsPerGrain
    w.grainOffsets = make([]int64, w.totalDataGrains, w.totalDataGrains)
    offset := firstDataSector * SectorSize
    for i := int64(0); i < w.totalDataGrains; i++ {
        w.grainOffsets[i] = offset
        if w.h.RegionIsHole(i*GrainSize, GrainSize) {
            continue
        }
        offset += GrainSize
    }

    // grain directories & tables
    err = w.writeGrainData()
    if err != nil {
        return err
    }

    return nil
}

func (w *SparseWriter) Write(p []byte) (int, error) {

    k, err := w.w.Write(p)
    w.cursor += int64(k)
    return k, err

    // TODO: check not writing into a forbidden grain
}

func (w *SparseWriter) Seek(offset int64, whence int) (int64, error) {
    var abs int64
    switch whence {
    case io.SeekStart:
        abs = offset
    case io.SeekCurrent:
        abs = w.cursor + offset
    case io.SeekEnd:
        abs = w.h.Size() + offset
    default:
        panic("bad seek whence")
    }

    grain := abs / GrainSize
    delta := abs % GrainSize
    x := w.grainOffsets[grain] + delta
    _, err := w.w.Seek(x, io.SeekStart)
    w.cursor = abs
    if err != nil {
        return 0, err
    }
    return abs, nil

}

func (w *SparseWriter) Close() error {
    return nil
}

func NewSparseWriter(w io.WriteSeeker, h HolePredictor) (*SparseWriter, error) {

    x := &SparseWriter{
        w: w,
        h: h,
    }

    err := x.init()
    if err != nil {
        return nil, err
    }

    return x, nil

}