vorteil/vorteil

View on GitHub
pkg/qcow2/qcow2.go

Summary

Maintainability
A
1 hr
Test Coverage
package qcow2

import (
    "bytes"
    "encoding/binary"
    "io"
)

const (
    SectorSize = 0x200
)

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

type Writer struct {
    w io.WriteSeeker
    h HolePredictor

    cursor int64

    totalDataSectors      int64
    totalDataClusters     int64
    metadataClusters      int64
    clusterSize           int64
    sectorsPerCluster     int64
    clusterOffsets        []int64
    clusterInUse          []bool
    l1Size                int64
    l2Size                int64
    l2Blocks              int64
    l2Offset              int64
    refcountBlocks        int64
    refcountTableClusters int64
}

func NewWriter(w io.WriteSeeker, h HolePredictor) (*Writer, error) {

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

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

    return x, nil

}

func divide(x, y int64) int64 {
    return (x + y - 1) / y
}

func (w *Writer) init() error {

    w.clusterSize = 0x10000
    w.sectorsPerCluster = w.clusterSize / SectorSize

    w.totalDataSectors = divide(w.h.Size(), SectorSize)
    w.totalDataClusters = divide(w.totalDataSectors, w.sectorsPerCluster)

    w.l2Blocks = divide(w.totalDataClusters, w.clusterSize/8)
    w.l2Size = w.clusterSize
    w.l1Size = divide(w.l2Blocks, w.clusterSize/8)

    w.refcountBlocks = divide(w.totalDataClusters, w.clusterSize/2)
    w.refcountTableClusters = divide(w.refcountBlocks, w.clusterSize/8)

    // NOTE: I think refcounts refer to the host file including metadata, so this is an attempt to include that.
    w.metadataClusters = 1 + w.l1Size + w.l2Blocks + w.refcountBlocks + w.refcountTableClusters
    for {
        before := w.metadataClusters
        w.refcountBlocks = divide(w.totalDataClusters+w.metadataClusters, w.clusterSize/2)
        w.refcountTableClusters = divide(w.refcountBlocks, w.clusterSize/8)
        w.metadataClusters = 1 + w.l1Size + w.l2Blocks + w.refcountBlocks + w.refcountTableClusters
        if w.metadataClusters == before {
            break
        }
    }

    w.l2Offset = w.clusterSize * (1 + w.l1Size + w.refcountTableClusters + w.refcountBlocks)

    err := w.writeHeader()
    if err != nil {
        return err
    }

    err = w.writeRefcountTable()
    if err != nil {
        return err
    }

    err = w.writeRefcountBlocks()
    if err != nil {
        return err
    }

    err = w.writeL1Table()
    if err != nil {
        return err
    }

    err = w.writeL2Tables()
    if err != nil {
        return err
    }

    return nil
}

type Header struct {
    Magic                 uint32 //     [0:3] magic: QCOW magic string ("QFI\xfb")
    Version               uint32 //     [4:7] Version number
    BackingFileOffset     uint64 //    [8:15] Offset into the image file at which the backing file name is stored.
    BackingFileSize       uint32 //   [16:19] Length of the backing file name in bytes.
    ClusterBits           uint32 //   [20:23] Number of bits that are used for addressing an offset whithin a cluster.
    Size                  uint64 //   [24:31] Virtual disk size in bytes
    CryptMethod           uint32 //   [32:35] Crypt method
    L1Size                uint32 //   [36:39] Number of entries in the active L1 table
    L1TableOffset         uint64 //   [40:47] Offset into the image file at which the active L1 table starts
    RefcountTableOffset   uint64 //   [48:55] Offset into the image file at which the refcount table starts
    RefcountTableClusters uint32 //   [56:59] Number of clusters that the refcount table occupies
    NbSnapshots           uint32 //   [60:63] Number of snapshots contained in the image
    SnapshotsOffset       uint64 //   [64:71] Offset into the image file at which the snapshot table starts
    IncompatibleFeatures  uint64 //   [72:79] for version >= 3: Bitmask of incomptible feature
    CompatibleFeatures    uint64 //   [80:87] for version >= 3: Bitmask of compatible feature
    AutoclearFeatures     uint64 //   [88:95] for version >= 3: Bitmask of auto-clear feature
    RefcountOrder         uint32 //   [96:99] for version >= 3: Describes the width of a reference count block entry
    HeaderLength          uint32 // [100:103] for version >= 3: Length of the header structure in bytes
}

func (w *Writer) writeHeader() error {

    hdr := &Header{
        Magic:                 0x514649FB,
        Version:               2,
        ClusterBits:           16, // Number of trailing zeroes on the w.clusterSize in binary (sys.Ctz)
        Size:                  uint64(w.h.Size()),
        L1Size:                uint32(w.l2Blocks), // TODO: should this be divided by 8? (bytes per entry)
        L1TableOffset:         uint64((1 + w.refcountBlocks + w.refcountTableClusters) * w.clusterSize),
        RefcountTableOffset:   uint64(w.clusterSize),
        RefcountTableClusters: uint32(w.refcountTableClusters),
        // SnapshotsOffset:       uint64(w.snapshotsOffset),
    }

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

    _, err = w.w.Seek(w.clusterSize, io.SeekStart)
    if err != nil {
        return err
    }

    return nil

}

func (w *Writer) writeRefcountTable() error {

    buf := new(bytes.Buffer)
    first := w.clusterSize * (1 + w.refcountTableClusters)
    offset := first
    for block := int64(0); block < w.refcountBlocks; block++ {
        err := binary.Write(buf, binary.BigEndian, uint64(offset))
        if err != nil {
            return err
        }
        offset += w.clusterSize
    }

    _, err := io.Copy(w.w, bytes.NewReader(buf.Bytes()))
    if err != nil {
        return err
    }

    _, err = w.w.Seek(first, io.SeekStart)
    if err != nil {
        return err
    }

    return nil

}

func (w *Writer) writeRefcountBlocks() error {

    buf := new(bytes.Buffer)

    var totalHostClusters int64

    // NOTE: we also calculate cluster offsets here to avoid checking for holes twice
    clusterOffset := w.l2Offset + w.l2Blocks*w.clusterSize
    w.clusterOffsets = make([]int64, w.totalDataClusters)
    w.clusterInUse = make([]bool, w.totalDataClusters)

    totalHostClusters = w.metadataClusters

    for cluster := int64(0); cluster < w.totalDataClusters; cluster++ {
        w.clusterOffsets[cluster] = clusterOffset
        if !w.h.RegionIsHole(cluster*w.clusterSize, w.clusterSize) {
            totalHostClusters++
            w.clusterInUse[cluster] = true
            clusterOffset += w.clusterSize
        }
    }

    for cluster := int64(0); cluster < totalHostClusters; cluster++ {
        err := binary.Write(buf, binary.BigEndian, uint16(1))
        if err != nil {
            return err
        }
    }

    _, err := io.Copy(w.w, bytes.NewReader(buf.Bytes()))
    if err != nil {
        return err
    }

    _, err = w.w.Seek((1+w.refcountBlocks+w.refcountTableClusters)*w.clusterSize, io.SeekStart)
    if err != nil {
        return err
    }

    return nil

}

func (w *Writer) writeL1Table() error {

    l2Capacity := w.clusterSize * (w.clusterSize / 8)

    buf := new(bytes.Buffer)

    var l2 int64
    var capacity int64
    var required = w.h.Size()

    for capacity < required {
        l2Offset := w.l2Offset + w.clusterSize*l2

        if w.h.RegionIsHole(l2*w.clusterSize, w.clusterSize) {
            err := binary.Write(buf, binary.BigEndian, uint64(0))
            if err != nil {
                return err
            }
        } else {
            err := binary.Write(buf, binary.BigEndian, uint64(l2Offset)|(1<<63)) // OFLAG_COPIED
            if err != nil {
                return err
            }
        }
        l2++
        capacity += l2Capacity
    }

    _, err := io.Copy(w.w, bytes.NewReader(buf.Bytes()))
    if err != nil {
        return err
    }

    _, err = w.w.Seek(w.l2Offset, io.SeekStart)
    if err != nil {
        return err
    }

    return nil

}

func (w *Writer) writeL2Tables() error {

    buf := new(bytes.Buffer)
    for cluster := int64(0); cluster < w.totalDataClusters; cluster++ {
        offset := uint64(0)
        if w.clusterInUse[cluster] {
            offset = uint64(w.clusterOffsets[cluster]) | (1 << 63) // OFLAG_COPIED
        }
        err := binary.Write(buf, binary.BigEndian, offset)
        if err != nil {
            return err
        }
    }

    _, err := io.Copy(w.w, bytes.NewReader(buf.Bytes()))
    if err != nil {
        return err
    }

    _, err = w.w.Seek(w.clusterOffsets[0], io.SeekStart)
    if err != nil {
        return err
    }

    return nil

}

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

func (w *Writer) 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 cluster
}

func (w *Writer) 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")
    }

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