Skip to content

Commit b121229

Browse files
authored
Merge pull request #2255 from vasileknik76/fix-copySparse
fix copySparse for empty qcow2 disk
2 parents 4609eb4 + bf6e770 commit b121229

File tree

3 files changed

+100
-2
lines changed

3 files changed

+100
-2
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ jobs:
7272
# For non-Homebrew we have to support an old release of Go
7373
go-version: ["1.21.x", "1.22.x"]
7474
steps:
75+
- name: Install test dependencies
76+
run: |
77+
sudo apt-get update
78+
sudo apt-get install -y --no-install-recommends qemu-utils
7579
- uses: actions/checkout@v4
7680
with:
7781
fetch-depth: 1

pkg/nativeimgutil/nativeimgutil.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,13 @@ func convertRawToRaw(source, dest string, size *int64) error {
123123
}
124124

125125
func copySparse(w *os.File, r io.Reader, bufSize int64) (int64, error) {
126-
var n int64
126+
var (
127+
n int64
128+
eof, hasWrites bool
129+
)
130+
127131
zeroBuf := make([]byte, bufSize)
128132
buf := make([]byte, bufSize)
129-
var eof bool
130133
for !eof {
131134
rN, rErr := r.Read(buf)
132135
if rErr != nil {
@@ -143,6 +146,7 @@ func copySparse(w *os.File, r io.Reader, bufSize int64) (int64, error) {
143146
// no need to ftruncate here
144147
n += int64(rN)
145148
} else {
149+
hasWrites = true
146150
wN, wErr := w.Write(buf)
147151
if wN > 0 {
148152
n += int64(wN)
@@ -155,6 +159,11 @@ func copySparse(w *os.File, r io.Reader, bufSize int64) (int64, error) {
155159
}
156160
}
157161
}
162+
163+
// Ftruncate must be run if the file contains only zeros
164+
if !hasWrites {
165+
return n, MakeSparse(w, n)
166+
}
158167
return n, nil
159168
}
160169

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package nativeimgutil
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
10+
"gotest.tools/v3/assert"
11+
)
12+
13+
func createImg(name, format, size string) error {
14+
return exec.Command("qemu-img", "create", name, "-f", format, size).Run()
15+
}
16+
17+
func TestConvertToRaw(t *testing.T) {
18+
_, err := exec.LookPath("qemu-img")
19+
if err != nil {
20+
t.Skipf("qemu-img does not seem installed: %v", err)
21+
}
22+
tmpDir := t.TempDir()
23+
24+
qcowImage, err := os.Create(filepath.Join(tmpDir, "qcow.img"))
25+
assert.NilError(t, err)
26+
defer qcowImage.Close()
27+
err = createImg(qcowImage.Name(), "qcow2", "1M")
28+
assert.NilError(t, err)
29+
30+
rawImage, err := os.Create(filepath.Join(tmpDir, "raw.img"))
31+
assert.NilError(t, err)
32+
defer rawImage.Close()
33+
err = createImg(rawImage.Name(), "raw", "1M")
34+
assert.NilError(t, err)
35+
36+
rawImageExtended, err := os.Create(filepath.Join(tmpDir, "raw_extended.img"))
37+
assert.NilError(t, err)
38+
defer rawImageExtended.Close()
39+
err = createImg(rawImageExtended.Name(), "raw", "2M")
40+
assert.NilError(t, err)
41+
42+
t.Run("qcow without backing file", func(t *testing.T) {
43+
resultImage := filepath.Join(tmpDir, strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
44+
assert.NilError(t, err)
45+
46+
err = ConvertToRaw(qcowImage.Name(), resultImage, nil, false)
47+
assert.NilError(t, err)
48+
assertFileEquals(t, rawImage.Name(), resultImage)
49+
})
50+
51+
t.Run("qcow with backing file", func(t *testing.T) {
52+
resultImage := filepath.Join(tmpDir, strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
53+
assert.NilError(t, err)
54+
55+
err = ConvertToRaw(qcowImage.Name(), resultImage, nil, true)
56+
assert.NilError(t, err)
57+
assertFileEquals(t, rawImage.Name(), resultImage)
58+
})
59+
60+
t.Run("qcow with extra size", func(t *testing.T) {
61+
resultImage := filepath.Join(tmpDir, strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
62+
assert.NilError(t, err)
63+
size := int64(2_097_152) // 2mb
64+
err = ConvertToRaw(qcowImage.Name(), resultImage, &size, false)
65+
assert.NilError(t, err)
66+
assertFileEquals(t, rawImageExtended.Name(), resultImage)
67+
})
68+
69+
t.Run("raw", func(t *testing.T) {
70+
resultImage := filepath.Join(tmpDir, strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"))
71+
assert.NilError(t, err)
72+
73+
err = ConvertToRaw(rawImage.Name(), resultImage, nil, false)
74+
assert.NilError(t, err)
75+
assertFileEquals(t, rawImage.Name(), resultImage)
76+
})
77+
}
78+
79+
func assertFileEquals(t *testing.T, expected, actual string) {
80+
expectedContent, err := os.ReadFile(expected)
81+
assert.NilError(t, err)
82+
actualContent, err := os.ReadFile(actual)
83+
assert.NilError(t, err)
84+
assert.DeepEqual(t, expectedContent, actualContent)
85+
}

0 commit comments

Comments
 (0)