main
1// Package memfs provides a billy filesystem base on memory.
2package memfs // import "github.com/go-git/go-billy/v5/memfs"
3
4import (
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "sort"
11 "strings"
12 "syscall"
13 "time"
14
15 "github.com/go-git/go-billy/v5"
16 "github.com/go-git/go-billy/v5/helper/chroot"
17 "github.com/go-git/go-billy/v5/util"
18)
19
20const separator = filepath.Separator
21
22var errNotLink = errors.New("not a link")
23
24// Memory a very convenient filesystem based on memory files.
25type Memory struct {
26 s *storage
27
28 tempCount int
29}
30
31// New returns a new Memory filesystem.
32func New() billy.Filesystem {
33 fs := &Memory{s: newStorage()}
34 fs.s.New("/", 0755|os.ModeDir, 0)
35 return chroot.New(fs, string(separator))
36}
37
38func (fs *Memory) Create(filename string) (billy.File, error) {
39 return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
40}
41
42func (fs *Memory) Open(filename string) (billy.File, error) {
43 return fs.OpenFile(filename, os.O_RDONLY, 0)
44}
45
46func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
47 f, has := fs.s.Get(filename)
48 if !has {
49 if !isCreate(flag) {
50 return nil, os.ErrNotExist
51 }
52
53 var err error
54 f, err = fs.s.New(filename, perm, flag)
55 if err != nil {
56 return nil, err
57 }
58 } else {
59 if isExclusive(flag) {
60 return nil, os.ErrExist
61 }
62
63 if target, isLink := fs.resolveLink(filename, f); isLink {
64 if target != filename {
65 return fs.OpenFile(target, flag, perm)
66 }
67 }
68 }
69
70 if f.mode.IsDir() {
71 return nil, fmt.Errorf("cannot open directory: %s", filename)
72 }
73
74 return f.Duplicate(filename, perm, flag), nil
75}
76
77func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) {
78 if !isSymlink(f.mode) {
79 return fullpath, false
80 }
81
82 target = string(f.content.bytes)
83 if !isAbs(target) {
84 target = fs.Join(filepath.Dir(fullpath), target)
85 }
86
87 return target, true
88}
89
90// On Windows OS, IsAbs validates if a path is valid based on if stars with a
91// unit (eg.: `C:\`) to assert that is absolute, but in this mem implementation
92// any path starting by `separator` is also considered absolute.
93func isAbs(path string) bool {
94 return filepath.IsAbs(path) || strings.HasPrefix(path, string(separator))
95}
96
97func (fs *Memory) Stat(filename string) (os.FileInfo, error) {
98 f, has := fs.s.Get(filename)
99 if !has {
100 return nil, os.ErrNotExist
101 }
102
103 fi, _ := f.Stat()
104
105 var err error
106 if target, isLink := fs.resolveLink(filename, f); isLink {
107 fi, err = fs.Stat(target)
108 if err != nil {
109 return nil, err
110 }
111 }
112
113 // the name of the file should always the name of the stated file, so we
114 // overwrite the Stat returned from the storage with it, since the
115 // filename may belong to a link.
116 fi.(*fileInfo).name = filepath.Base(filename)
117 return fi, nil
118}
119
120func (fs *Memory) Lstat(filename string) (os.FileInfo, error) {
121 f, has := fs.s.Get(filename)
122 if !has {
123 return nil, os.ErrNotExist
124 }
125
126 return f.Stat()
127}
128
129type ByName []os.FileInfo
130
131func (a ByName) Len() int { return len(a) }
132func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() }
133func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
134
135func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) {
136 if f, has := fs.s.Get(path); has {
137 if target, isLink := fs.resolveLink(path, f); isLink {
138 if target != path {
139 return fs.ReadDir(target)
140 }
141 }
142 } else {
143 return nil, &os.PathError{Op: "open", Path: path, Err: syscall.ENOENT}
144 }
145
146 var entries []os.FileInfo
147 for _, f := range fs.s.Children(path) {
148 fi, _ := f.Stat()
149 entries = append(entries, fi)
150 }
151
152 sort.Sort(ByName(entries))
153
154 return entries, nil
155}
156
157func (fs *Memory) MkdirAll(path string, perm os.FileMode) error {
158 _, err := fs.s.New(path, perm|os.ModeDir, 0)
159 return err
160}
161
162func (fs *Memory) TempFile(dir, prefix string) (billy.File, error) {
163 return util.TempFile(fs, dir, prefix)
164}
165
166func (fs *Memory) getTempFilename(dir, prefix string) string {
167 fs.tempCount++
168 filename := fmt.Sprintf("%s_%d_%d", prefix, fs.tempCount, time.Now().UnixNano())
169 return fs.Join(dir, filename)
170}
171
172func (fs *Memory) Rename(from, to string) error {
173 return fs.s.Rename(from, to)
174}
175
176func (fs *Memory) Remove(filename string) error {
177 return fs.s.Remove(filename)
178}
179
180// Falls back to Go's filepath.Join, which works differently depending on the
181// OS where the code is being executed.
182func (fs *Memory) Join(elem ...string) string {
183 return filepath.Join(elem...)
184}
185
186func (fs *Memory) Symlink(target, link string) error {
187 _, err := fs.Lstat(link)
188 if err == nil {
189 return os.ErrExist
190 }
191
192 if !errors.Is(err, os.ErrNotExist) {
193 return err
194 }
195
196 return util.WriteFile(fs, link, []byte(target), 0777|os.ModeSymlink)
197}
198
199func (fs *Memory) Readlink(link string) (string, error) {
200 f, has := fs.s.Get(link)
201 if !has {
202 return "", os.ErrNotExist
203 }
204
205 if !isSymlink(f.mode) {
206 return "", &os.PathError{
207 Op: "readlink",
208 Path: link,
209 Err: fmt.Errorf("not a symlink"),
210 }
211 }
212
213 return string(f.content.bytes), nil
214}
215
216// Capabilities implements the Capable interface.
217func (fs *Memory) Capabilities() billy.Capability {
218 return billy.WriteCapability |
219 billy.ReadCapability |
220 billy.ReadAndWriteCapability |
221 billy.SeekCapability |
222 billy.TruncateCapability
223}
224
225type file struct {
226 name string
227 content *content
228 position int64
229 flag int
230 mode os.FileMode
231
232 isClosed bool
233}
234
235func (f *file) Name() string {
236 return f.name
237}
238
239func (f *file) Read(b []byte) (int, error) {
240 n, err := f.ReadAt(b, f.position)
241 f.position += int64(n)
242
243 if errors.Is(err, io.EOF) && n != 0 {
244 err = nil
245 }
246
247 return n, err
248}
249
250func (f *file) ReadAt(b []byte, off int64) (int, error) {
251 if f.isClosed {
252 return 0, os.ErrClosed
253 }
254
255 if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) {
256 return 0, errors.New("read not supported")
257 }
258
259 n, err := f.content.ReadAt(b, off)
260
261 return n, err
262}
263
264func (f *file) Seek(offset int64, whence int) (int64, error) {
265 if f.isClosed {
266 return 0, os.ErrClosed
267 }
268
269 switch whence {
270 case io.SeekCurrent:
271 f.position += offset
272 case io.SeekStart:
273 f.position = offset
274 case io.SeekEnd:
275 f.position = int64(f.content.Len()) + offset
276 }
277
278 return f.position, nil
279}
280
281func (f *file) Write(p []byte) (int, error) {
282 return f.WriteAt(p, f.position)
283}
284
285func (f *file) WriteAt(p []byte, off int64) (int, error) {
286 if f.isClosed {
287 return 0, os.ErrClosed
288 }
289
290 if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) {
291 return 0, errors.New("write not supported")
292 }
293
294 n, err := f.content.WriteAt(p, off)
295 f.position = off + int64(n)
296
297 return n, err
298}
299
300func (f *file) Close() error {
301 if f.isClosed {
302 return os.ErrClosed
303 }
304
305 f.isClosed = true
306 return nil
307}
308
309func (f *file) Truncate(size int64) error {
310 if size < int64(len(f.content.bytes)) {
311 f.content.bytes = f.content.bytes[:size]
312 } else if more := int(size) - len(f.content.bytes); more > 0 {
313 f.content.bytes = append(f.content.bytes, make([]byte, more)...)
314 }
315
316 return nil
317}
318
319func (f *file) Duplicate(filename string, mode os.FileMode, flag int) billy.File {
320 new := &file{
321 name: filename,
322 content: f.content,
323 mode: mode,
324 flag: flag,
325 }
326
327 if isTruncate(flag) {
328 new.content.Truncate()
329 }
330
331 if isAppend(flag) {
332 new.position = int64(new.content.Len())
333 }
334
335 return new
336}
337
338func (f *file) Stat() (os.FileInfo, error) {
339 return &fileInfo{
340 name: f.Name(),
341 mode: f.mode,
342 size: f.content.Len(),
343 }, nil
344}
345
346// Lock is a no-op in memfs.
347func (f *file) Lock() error {
348 return nil
349}
350
351// Unlock is a no-op in memfs.
352func (f *file) Unlock() error {
353 return nil
354}
355
356type fileInfo struct {
357 name string
358 size int
359 mode os.FileMode
360}
361
362func (fi *fileInfo) Name() string {
363 return fi.name
364}
365
366func (fi *fileInfo) Size() int64 {
367 return int64(fi.size)
368}
369
370func (fi *fileInfo) Mode() os.FileMode {
371 return fi.mode
372}
373
374func (*fileInfo) ModTime() time.Time {
375 return time.Now()
376}
377
378func (fi *fileInfo) IsDir() bool {
379 return fi.mode.IsDir()
380}
381
382func (*fileInfo) Sys() interface{} {
383 return nil
384}
385
386func (c *content) Truncate() {
387 c.bytes = make([]byte, 0)
388}
389
390func (c *content) Len() int {
391 return len(c.bytes)
392}
393
394func isCreate(flag int) bool {
395 return flag&os.O_CREATE != 0
396}
397
398func isExclusive(flag int) bool {
399 return flag&os.O_EXCL != 0
400}
401
402func isAppend(flag int) bool {
403 return flag&os.O_APPEND != 0
404}
405
406func isTruncate(flag int) bool {
407 return flag&os.O_TRUNC != 0
408}
409
410func isReadAndWrite(flag int) bool {
411 return flag&os.O_RDWR != 0
412}
413
414func isReadOnly(flag int) bool {
415 return flag == os.O_RDONLY
416}
417
418func isWriteOnly(flag int) bool {
419 return flag&os.O_WRONLY != 0
420}
421
422func isSymlink(m os.FileMode) bool {
423 return m&os.ModeSymlink != 0
424}