Skip to content

Commit 3f0a4b1

Browse files
committed
guestfs: support filesystem images via libguestfs
Signed-off-by: Hank Donnay <hdonnay@redhat.com>
1 parent e393379 commit 3f0a4b1

File tree

6 files changed

+903
-0
lines changed

6 files changed

+903
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ toolchain go1.24.3
77
require (
88
github.com/Masterminds/semver v1.5.0
99
github.com/doug-martin/goqu/v8 v8.6.0
10+
github.com/ebitengine/purego v0.9.0
1011
github.com/google/go-cmp v0.7.0
1112
github.com/google/uuid v1.6.0
1213
github.com/jackc/pgconn v1.14.3

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ github.com/doug-martin/goqu/v8 v8.6.0 h1:KWuDGL135poBgY+SceArvOtIIEpieNKgIZCvger
1616
github.com/doug-martin/goqu/v8 v8.6.0/go.mod h1:wiiYWkiguNXK5d4kGIkYmOxBScEL37d9Cfv9tXhPsTk=
1717
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
1818
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
19+
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
20+
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
1921
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
2022
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
2123
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=

internal/guestfs/fs.go

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
package guestfs
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"errors"
7+
"io"
8+
"io/fs"
9+
"log/slog"
10+
"path"
11+
"runtime"
12+
"sync"
13+
"sync/atomic"
14+
"time"
15+
)
16+
17+
type fsCache struct {
18+
dirent sync.Map // map[string]*dirent
19+
fileinfo sync.Map // map[string]*fileinfo
20+
contents sync.Map // map[string]*[]byte
21+
}
22+
23+
func (c *fsCache) Clear() {
24+
c.dirent.Clear()
25+
c.fileinfo.Clear()
26+
c.contents.Clear()
27+
}
28+
29+
var (
30+
_ fs.FS = (*FS)(nil)
31+
_ fs.StatFS = (*FS)(nil)
32+
_ fs.ReadDirFS = (*FS)(nil)
33+
_ fs.ReadFileFS = (*FS)(nil)
34+
)
35+
36+
// FS implements [fs.FS].
37+
type FS struct {
38+
g guestfs
39+
closed *atomic.Bool
40+
cache fsCache
41+
}
42+
43+
// Open mounts the filesystem image (a file containing just a filesystem, i.e.
44+
// no partition table) and returns an [fs.FS] implementation for examining it.
45+
//
46+
// The returned [*FS] may panic if not closed.
47+
func Open(ctx context.Context, path string) (*FS, error) {
48+
sys := new(FS)
49+
if err := errors.Join(loadLibc(), loadLib()); err != nil {
50+
slog.DebugContext(ctx, "unable to do setup", "reason", err)
51+
return nil, errors.ErrUnsupported
52+
}
53+
54+
g, err := newGuestfs()
55+
if err != nil {
56+
return nil, err
57+
}
58+
closed := new(atomic.Bool)
59+
60+
// The cleanup closure holds an extra pointer to the "closed" bool, so it
61+
// will outlive the "sys" pointer. An atomic probably isn't strictly
62+
// necessary (there should only ever be two live pointers, and this one is
63+
// only used after the one stored in "sys" is gone), but I didn't want to
64+
// verify that.
65+
runtime.AddCleanup(sys, func(g guestfs) {
66+
if closed.CompareAndSwap(false, true) {
67+
lib.Close(g)
68+
}
69+
}, g)
70+
71+
sys.g = g
72+
sys.closed = closed
73+
74+
slog.DebugContext(ctx, "appliance launching")
75+
if err := addDrive(sys.g, path); err != nil {
76+
return nil, err
77+
}
78+
if err := launch(sys.g); err != nil {
79+
return nil, err
80+
}
81+
slog.DebugContext(ctx, "appliance launched")
82+
if err := mount(sys.g, "/dev/sda", "/"); err != nil {
83+
return nil, err
84+
}
85+
slog.DebugContext(ctx, "fs mounted")
86+
87+
return sys, nil
88+
}
89+
90+
// Close releases held resources.
91+
//
92+
// Any [fs.File]s returned by the receiver should not be used after this method
93+
// is called.
94+
func (sys *FS) Close() error {
95+
// Eagerly deref pointers in the caches.
96+
sys.cache.Clear()
97+
if sys.closed.CompareAndSwap(false, true) {
98+
lib.Close(sys.g)
99+
}
100+
return nil
101+
}
102+
103+
// ToAbs translates a name from [fs.FS] convention (always relative to the root)
104+
// to the guestfs convention (always absolute).
105+
func toAbs(name string) string {
106+
return "/" + path.Clean(name)
107+
}
108+
109+
// All the various fs method implementation are implemented as an exported
110+
// version that expects [fs.FS] paths and an unexported version that expects
111+
// guestfs paths.
112+
113+
// Open implements [fs.FS].
114+
func (sys *FS) Open(name string) (fs.File, error) {
115+
if !fs.ValidPath(name) {
116+
return nil, fs.ErrInvalid
117+
}
118+
119+
return sys.open(toAbs(name))
120+
}
121+
122+
func (sys *FS) open(name string) (fs.File, error) {
123+
stat, err := sys.stat(name)
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
return &file{
129+
sys: sys,
130+
stat: stat,
131+
path: name,
132+
}, nil
133+
}
134+
135+
var (
136+
_ fs.File = (*file)(nil)
137+
_ fs.ReadDirFile = (*file)(nil)
138+
_ io.Reader = (*file)(nil)
139+
_ io.ReaderAt = (*file)(nil)
140+
)
141+
142+
// File is the struct backing returned [fs.File]s.
143+
//
144+
// If [Read] is called, the file contents are pulled into memory in their
145+
// entirety.
146+
type file struct {
147+
sys *FS
148+
stat fs.FileInfo
149+
path string
150+
contents *guestfsFile
151+
reader *bytes.Reader
152+
}
153+
154+
// Close implements [fs.File].
155+
func (f *file) Close() error {
156+
*f = file{}
157+
return nil
158+
}
159+
160+
// Stat implements [fs.File].
161+
func (f *file) Stat() (fs.FileInfo, error) { return f.stat, nil }
162+
163+
// ReadDir implements [fs.ReadDirFile].
164+
//
165+
// BUG(hank) ReadDir currently does not respect the "n" argument and always
166+
// returns the entire directory contents.
167+
func (f *file) ReadDir(n int) ([]fs.DirEntry, error) {
168+
_ = n
169+
return f.sys.readDir(f.path)
170+
}
171+
172+
// Read implements [io.Reader].
173+
//
174+
// Calling Read pulls the entire file contents into memory.
175+
func (f *file) Read(b []byte) (int, error) {
176+
if f.reader == nil {
177+
name := f.path
178+
cache := &f.sys.cache.contents
179+
v, loaded := cache.Load(name)
180+
if !loaded {
181+
rd, err := readFile(f.sys.g, name)
182+
if err != nil {
183+
return 0, err
184+
}
185+
v, _ = cache.LoadOrStore(name, rd)
186+
}
187+
f.contents = v.(*guestfsFile)
188+
f.reader = bytes.NewReader(f.contents.data)
189+
}
190+
return f.reader.Read(b)
191+
}
192+
193+
// ReadAt implements [io.ReaderAt].
194+
//
195+
// BUG(hank) The underlying [guestfs_pread(3)] call used for the [io.ReaderAt]
196+
// implementation is only more efficient (due to calling convention switch and
197+
// buffer copies) if the data is actually being processed piece-wise and large
198+
// buffers (e.g. 2 MiB) are used.
199+
//
200+
// [guestfs_pread(3)]: https://libguestfs.org/guestfs.3.html#guestfs_pread
201+
func (f *file) ReadAt(b []byte, offset int64) (int, error) {
202+
if f.reader == nil {
203+
return pread(f.sys.g, f.path, b, offset)
204+
}
205+
return f.reader.ReadAt(b, offset)
206+
}
207+
208+
// Stat implements [fs.StatFS].
209+
func (sys *FS) Stat(name string) (fs.FileInfo, error) {
210+
if !fs.ValidPath(name) {
211+
return nil, fs.ErrInvalid
212+
}
213+
return sys.stat(toAbs(name))
214+
}
215+
216+
func (sys *FS) stat(name string) (fs.FileInfo, error) {
217+
v, loaded := sys.cache.fileinfo.Load(name)
218+
if !loaded {
219+
fi, err := statns(sys.g, name)
220+
if err != nil {
221+
return nil, err
222+
}
223+
v, _ = sys.cache.fileinfo.LoadOrStore(name, fi)
224+
}
225+
return v.(*fileinfo), nil
226+
}
227+
228+
type fileinfo struct {
229+
sys *FS
230+
name string
231+
statns *guestfsStatns
232+
}
233+
234+
// IsDir implements [fs.FileInfo].
235+
func (f *fileinfo) IsDir() bool { return f.Mode().IsDir() }
236+
237+
// ModTime implements [fs.FileInfo].
238+
func (f *fileinfo) ModTime() time.Time {
239+
return time.Unix(f.statns.mtime_sec, f.statns.mtime_nsec)
240+
}
241+
242+
// Mode implements [fs.FileInfo].
243+
func (f *fileinfo) Mode() fs.FileMode {
244+
return fs.FileMode(f.statns.mode)
245+
}
246+
247+
// Name implements [fs.FileInfo].
248+
func (f *fileinfo) Name() string { return path.Base(f.name) }
249+
250+
// Size implements [fs.FileInfo].
251+
func (f *fileinfo) Size() int64 { return f.statns.size }
252+
253+
// Sys implements [fs.FileInfo].
254+
func (f *fileinfo) Sys() any { return f.statns }
255+
256+
// ReadDir implements [fs.ReadDirFS].
257+
func (sys *FS) ReadDir(name string) ([]fs.DirEntry, error) {
258+
if !fs.ValidPath(name) {
259+
return nil, fs.ErrInvalid
260+
}
261+
return sys.readDir(toAbs(name))
262+
}
263+
264+
func (sys *FS) readDir(name string) ([]fs.DirEntry, error) {
265+
seq, err := readdir(sys.g, name)
266+
if err != nil {
267+
return nil, err
268+
}
269+
// TODO(hank): Cache ReadDir calls.
270+
var ret []fs.DirEntry
271+
for ent := range seq {
272+
ent.sys = sys
273+
ret = append(ret, &ent)
274+
}
275+
return ret, nil
276+
}
277+
278+
var _ fs.DirEntry = (*dirent)(nil)
279+
280+
type dirent struct {
281+
sys *FS
282+
dir string
283+
name string
284+
typ fs.FileMode
285+
}
286+
287+
// Info implements [fs.DirEntry].
288+
func (d *dirent) Info() (fs.FileInfo, error) {
289+
return d.sys.stat(path.Join(d.dir, d.name))
290+
}
291+
292+
// IsDir implements [fs.DirEntry].
293+
func (d *dirent) IsDir() bool { return d.typ == fs.ModeDir }
294+
295+
// Name implements [fs.DirEntry].
296+
func (d *dirent) Name() string { return d.name }
297+
298+
// Type implements [fs.DirEntry].
299+
func (d *dirent) Type() fs.FileMode { return d.typ }
300+
301+
// ReadFile implements [fs.ReadFileFS].
302+
func (sys *FS) ReadFile(name string) ([]byte, error) {
303+
if !fs.ValidPath(name) {
304+
return nil, fs.ErrInvalid
305+
}
306+
return sys.readFile(toAbs(name))
307+
}
308+
309+
func (sys *FS) readFile(name string) ([]byte, error) {
310+
// If the [foreign pointer tracking proposal] makes it, then this method
311+
// could avoid a copy and just hand out the foreign-backed slice.
312+
//
313+
// [foreign pointer tracking proposal]: https://github.com/golang/go/issues/70224
314+
v, loaded := sys.cache.contents.Load(name)
315+
if !loaded {
316+
rd, err := readFile(sys.g, name)
317+
if err != nil {
318+
return nil, err
319+
}
320+
v, _ = sys.cache.contents.LoadOrStore(name, rd)
321+
}
322+
f := v.(*guestfsFile)
323+
b := make([]byte, len(f.data))
324+
copy(b, f.data)
325+
return b, nil
326+
}

0 commit comments

Comments
 (0)