Skip to content

Commit 0824f16

Browse files
authored
Merge pull request #10 from AkihiroSuda/dev
Experimental support for Extended L2
2 parents 57fccac + c9606c9 commit 0824f16

File tree

3 files changed

+129
-24
lines changed

3 files changed

+129
-24
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
uses: actions/cache@v3
3434
with:
3535
path: test-images-ro
36-
key: ${{ runner.os }}-integration
36+
key: ${{ runner.os }}-${{ hashFiles('.github/workflows/test.yml') }}
3737
- name: Prepare test-images-ro
3838
if: steps.cache-test-images-ro.outputs.cache-hit != 'true'
3939
run: |
@@ -47,6 +47,8 @@ jobs:
4747
# TODO: write something to the child image (with qemu-nbd?)
4848
# Convert to zstd
4949
qemu-img convert -f qcow2 -O qcow2 -o compression_type=zstd debian-11-genericcloud-amd64-20230501-1367.qcow2 debian-11-genericcloud-amd64-20230501-1367.zstd.qcow2
50+
# Convert to ext_l2
51+
qemu-img convert -f qcow2 -O qcow2 -o extended_l2=on debian-11-genericcloud-amd64-20230501-1367.qcow2 debian-11-genericcloud-amd64-20230501-1367.ext_l2.qcow2
5052
- name: Prepare test-images
5153
run: cp -a test-images-ro test-images
5254
- name: "Test debian-11-genericcloud-amd64-20230501-1367.qcow2"
@@ -55,3 +57,5 @@ jobs:
5557
run: hack/compare-with-qemu-img.sh test-images/debian-11-genericcloud-amd64-20230501-1367.child_4G.qcow2
5658
- name: "Test debian-11-genericcloud-amd64-20230501-1367.zstd.qcow2"
5759
run: hack/compare-with-qemu-img.sh test-images/debian-11-genericcloud-amd64-20230501-1367.zstd.qcow2
60+
- name: "Test debian-11-genericcloud-amd64-20230501-1367.ext_l2.qcow2"
61+
run: hack/compare-with-qemu-img.sh test-images/debian-11-genericcloud-amd64-20230501-1367.ext_l2.qcow2

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ Use [`io.NewSectionReader`](https://pkg.go.dev/io#NewSectionReader) to wrap [`io
99
f, _ := os.Open("a.qcow2")
1010
defer f.Close()
1111
img, _ := qcow2reader.Open(f)
12-
r, _ := io.NewSectionReader(img, 0, int64(img.Size))
12+
r, _ := io.NewSectionReader(img, 0, img.Size()))
1313
```
1414

1515
The following features are not supported yet:
16-
- AES
17-
- LUKS
18-
- External data
19-
- Extended L2 Entries
16+
- [AES](https://gitlab.com/qemu-project/qemu/-/blob/v8.0.0/docs/interop/qcow2.txt#L411-L421)
17+
- [LUKS](https://gitlab.com/qemu-project/qemu/-/blob/v8.0.0/docs/interop/qcow2.txt#L423-L429)
18+
- [External data](https://gitlab.com/qemu-project/qemu/-/blob/v8.0.0/docs/interop/qcow2.txt#L106-L116)
19+
20+
The following features are experimentally supported:
21+
- [Extended L2 Entries](https://gitlab.com/qemu-project/qemu/-/blob/v8.0.0/docs/interop/qcow2.txt#L122-L126)

image/qcow2/qcow2.go

Lines changed: 117 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,11 @@ func (header *Header) Readable() error {
325325
switch i {
326326
case IncompatibleFeaturesDirtyBit, IncompatibleFeaturesCorruptBit:
327327
log.Warnf("unexpected incompatible feature bit: %q", IncompatibleFeaturesNames[i])
328+
case IncompatibleFeaturesExtendedL2EntriesBit:
329+
log.Warnf("Support for %q is experimental", IncompatibleFeaturesNames[i])
328330
case IncompatibleFeaturesCompressionTypeBit:
329331
// NOP
330-
case IncompatibleFeaturesExternalDataFileBit,
331-
IncompatibleFeaturesExtendedL2EntriesBit:
332+
case IncompatibleFeaturesExternalDataFileBit:
332333
return fmt.Errorf("%w: incompatible feature: %q", ErrUnsupportedFeature, IncompatibleFeaturesNames[i])
333334
default:
334335
return fmt.Errorf("%w: incompatible feature bit %d", ErrUnsupportedFeature, i)
@@ -466,13 +467,13 @@ func (x l2TableEntry) compressed() bool {
466467
return (x>>62)&0b1 == 0b1
467468
}
468469

469-
/*
470470
// extendedL2TableEntry is not supported yet
471471
type extendedL2TableEntry struct {
472-
l2TableEntry
473-
ext uint64
472+
L2TableEntry l2TableEntry
473+
// the following bitmaps are meaningless for compressed clusters
474+
ZeroStatusBitmap uint32 // 1: reads as zeros, 0: no effect
475+
AllocStatusBitmap uint32 // 1: allocated, 0: not allocated
474476
}
475-
*/
476477

477478
func readL2Table(ra io.ReaderAt, offset uint64, clusterSize int) ([]l2TableEntry, error) {
478479
if offset == 0 {
@@ -487,6 +488,19 @@ func readL2Table(ra io.ReaderAt, offset uint64, clusterSize int) ([]l2TableEntry
487488
return l2Table, nil
488489
}
489490

491+
func readExtendedL2Table(ra io.ReaderAt, offset uint64, clusterSize int) ([]extendedL2TableEntry, error) {
492+
if offset == 0 {
493+
return nil, errors.New("invalid extended L2 table offset: 0")
494+
}
495+
r := io.NewSectionReader(ra, int64(offset), int64(clusterSize))
496+
entries := clusterSize / 16
497+
extL2Table := make([]extendedL2TableEntry, entries)
498+
if err := binary.Read(r, binary.BigEndian, &extL2Table); err != nil {
499+
return nil, err
500+
}
501+
return extL2Table, nil
502+
}
503+
490504
type standardClusterDescriptor uint64
491505

492506
func (desc standardClusterDescriptor) allZero() bool {
@@ -662,9 +676,16 @@ func (img *Qcow2) Readable() error {
662676
return img.errUnreadable
663677
}
664678

679+
func (img *Qcow2) extendedL2() bool {
680+
return img.Header.HeaderFieldsV3 != nil && img.Header.HeaderFieldsV3.IncompatibleFeatures&(1<<IncompatibleFeaturesExtendedL2EntriesBit) != 0
681+
}
682+
665683
// readAtAligned requires that off and off+len(p)-1 belong to the same cluster.
666684
func (img *Qcow2) readAtAligned(p []byte, off int64) (int, error) {
667685
l2Entries := img.clusterSize / 8
686+
if img.extendedL2() {
687+
l2Entries = img.clusterSize / 16
688+
}
668689
l1Index := int((off / int64(img.clusterSize)) / int64(l2Entries))
669690
if l1Index >= len(img.l1Table) {
670691
return 0, fmt.Errorf("index %d exceeds the L1 table length %d", l1Index, img.l1Table)
@@ -674,20 +695,40 @@ func (img *Qcow2) readAtAligned(p []byte, off int64) (int, error) {
674695
if l2TableOffset == 0 {
675696
return img.readAtAlignedUnallocated(p, off)
676697
}
677-
l2Table, err := readL2Table(img.ra, l2TableOffset, img.clusterSize)
678-
if err != nil {
679-
return 0, fmt.Errorf("failed to read L2 table for L1 entry %v (index %d): %w", l1Entry, l1Index, err)
680-
}
681698
l2Index := int((off / int64(img.clusterSize)) % int64(l2Entries))
682-
if l2Index >= len(l2Table) {
683-
return 0, fmt.Errorf("index %d exceeds the L2 table length %d", l2Index, l2Table)
699+
var (
700+
extL2Entry *extendedL2TableEntry
701+
l2Entry l2TableEntry
702+
)
703+
if img.extendedL2() {
704+
// TODO
705+
extL2Table, err := readExtendedL2Table(img.ra, l2TableOffset, img.clusterSize)
706+
if err != nil {
707+
return 0, fmt.Errorf("failed to read extended L2 table for L1 entry %v (index %d): %w", l1Entry, l1Index, err)
708+
}
709+
if l2Index >= len(extL2Table) {
710+
return 0, fmt.Errorf("index %d exceeds the extended L2 table length %d", l2Index, extL2Table)
711+
}
712+
extL2Entry = &extL2Table[l2Index]
713+
l2Entry = extL2Entry.L2TableEntry
714+
} else {
715+
l2Table, err := readL2Table(img.ra, l2TableOffset, img.clusterSize)
716+
if err != nil {
717+
return 0, fmt.Errorf("failed to read L2 table for L1 entry %v (index %d): %w", l1Entry, l1Index, err)
718+
}
719+
if l2Index >= len(l2Table) {
720+
return 0, fmt.Errorf("index %d exceeds the L2 table length %d", l2Index, l2Table)
721+
}
722+
l2Entry = l2Table[l2Index]
684723
}
685-
l2Entry := l2Table[l2Index]
686724
desc := l2Entry.clusterDescriptor()
687-
if desc == 0 {
725+
if desc == 0 && !img.extendedL2() {
688726
return img.readAtAlignedUnallocated(p, off)
689727
}
690-
var n int
728+
var (
729+
n int
730+
err error
731+
)
691732
if l2Entry.compressed() {
692733
compressedDesc := compressedClusterDescriptor(desc)
693734
n, err = img.readAtAlignedCompressed(p, off, compressedDesc)
@@ -696,9 +737,16 @@ func (img *Qcow2) readAtAligned(p []byte, off int64) (int, error) {
696737
}
697738
} else {
698739
standardDesc := standardClusterDescriptor(desc)
699-
n, err = img.readAtAlignedStandard(p, off, standardDesc)
700-
if err != nil {
701-
err = fmt.Errorf("failed to read standard cluster (len=%d, off-%d, desc=0x%X): %w", len(p), off, desc, err)
740+
if img.extendedL2() {
741+
n, err = img.readAtAlignedStandardExtendedL2(p, off, standardDesc, *extL2Entry)
742+
if err != nil {
743+
err = fmt.Errorf("failed to read standard cluster with Extended L2 (len=%d, off=%d, desc=0x%X): %w", len(p), off, desc, err)
744+
}
745+
} else {
746+
n, err = img.readAtAlignedStandard(p, off, standardDesc)
747+
if err != nil {
748+
err = fmt.Errorf("failed to read standard cluster (len=%d, off=%d, desc=0x%X): %w", len(p), off, desc, err)
749+
}
702750
}
703751
}
704752
return n, err
@@ -744,6 +792,57 @@ func (img *Qcow2) readAtAlignedStandard(p []byte, off int64, desc standardCluste
744792
return n, err
745793
}
746794

795+
// readAtAlignedStandardExtendedL2 is experimental
796+
//
797+
// TODO: read multiple subclusters at once
798+
//
799+
// clusterNo = LBA / clusterSize
800+
// subclusterNo = (LBA % clusterSize) / subclusterSize
801+
func (img *Qcow2) readAtAlignedStandardExtendedL2(p []byte, off int64, desc standardClusterDescriptor, extL2Entry extendedL2TableEntry) (int, error) {
802+
var n int
803+
subclusterSize := img.clusterSize / 32
804+
hostClusterOffset := desc.hostClusterOffset()
805+
subclusterNo := (int(off) % img.clusterSize) / subclusterSize
806+
for i := subclusterNo; i < 32; i++ {
807+
pIdxBegin := n
808+
pIdxEnd := n + subclusterSize
809+
if pIdxEnd > len(p) {
810+
pIdxEnd = len(p)
811+
}
812+
if pIdxEnd <= pIdxBegin {
813+
break
814+
}
815+
var (
816+
currentN int
817+
err error
818+
)
819+
if ((extL2Entry.AllocStatusBitmap >> i) & 0b1) == 0b1 {
820+
currentRawOff := int64(hostClusterOffset) + (off % int64(img.clusterSize)) + int64(n)
821+
currentN, err = img.ra.ReadAt(p[pIdxBegin:pIdxEnd], currentRawOff)
822+
if err != nil {
823+
return n, fmt.Errorf("failed to read from the raw offset %d: %w", currentRawOff, err)
824+
}
825+
} else {
826+
currentOff := off + int64(n)
827+
if ((extL2Entry.ZeroStatusBitmap >> i) & 0b1) == 0b1 {
828+
currentN, err = img.readZero(p[pIdxBegin:pIdxEnd], currentOff)
829+
if err != nil {
830+
return n, fmt.Errorf("failed to read zero: %w", err)
831+
}
832+
} else {
833+
currentN, err = img.readAtAlignedUnallocated(p[pIdxBegin:pIdxEnd], currentOff)
834+
if err != nil && !errors.Is(err, io.EOF) {
835+
return n, fmt.Errorf("failed to read unallocated: %w", err)
836+
}
837+
}
838+
}
839+
if currentN > 0 {
840+
n += currentN
841+
}
842+
}
843+
return n, nil
844+
}
845+
747846
func (img *Qcow2) readAtAlignedCompressed(p []byte, off int64, desc compressedClusterDescriptor) (int, error) {
748847
hostClusterOffset := desc.hostClusterOffset(int(img.Header.ClusterBits))
749848
if hostClusterOffset == 0 {

0 commit comments

Comments
 (0)