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
180func (fs *Memory) Chmod(path string, mode os.FileMode) error {
181	return fs.s.Chmod(path, mode)
182}
183
184// Falls back to Go's filepath.Join, which works differently depending on the
185// OS where the code is being executed.
186func (fs *Memory) Join(elem ...string) string {
187	return filepath.Join(elem...)
188}
189
190func (fs *Memory) Symlink(target, link string) error {
191	_, err := fs.Lstat(link)
192	if err == nil {
193		return os.ErrExist
194	}
195
196	if !errors.Is(err, os.ErrNotExist) {
197		return err
198	}
199
200	return util.WriteFile(fs, link, []byte(target), 0777|os.ModeSymlink)
201}
202
203func (fs *Memory) Readlink(link string) (string, error) {
204	f, has := fs.s.Get(link)
205	if !has {
206		return "", os.ErrNotExist
207	}
208
209	if !isSymlink(f.mode) {
210		return "", &os.PathError{
211			Op:   "readlink",
212			Path: link,
213			Err:  fmt.Errorf("not a symlink"),
214		}
215	}
216
217	return string(f.content.bytes), nil
218}
219
220// Capabilities implements the Capable interface.
221func (fs *Memory) Capabilities() billy.Capability {
222	return billy.WriteCapability |
223		billy.ReadCapability |
224		billy.ReadAndWriteCapability |
225		billy.SeekCapability |
226		billy.TruncateCapability
227}
228
229type file struct {
230	name     string
231	content  *content
232	position int64
233	flag     int
234	mode     os.FileMode
235
236	isClosed bool
237}
238
239func (f *file) Name() string {
240	return f.name
241}
242
243func (f *file) Read(b []byte) (int, error) {
244	n, err := f.ReadAt(b, f.position)
245	f.position += int64(n)
246
247	if errors.Is(err, io.EOF) && n != 0 {
248		err = nil
249	}
250
251	return n, err
252}
253
254func (f *file) ReadAt(b []byte, off int64) (int, error) {
255	if f.isClosed {
256		return 0, os.ErrClosed
257	}
258
259	if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) {
260		return 0, errors.New("read not supported")
261	}
262
263	n, err := f.content.ReadAt(b, off)
264
265	return n, err
266}
267
268func (f *file) Seek(offset int64, whence int) (int64, error) {
269	if f.isClosed {
270		return 0, os.ErrClosed
271	}
272
273	switch whence {
274	case io.SeekCurrent:
275		f.position += offset
276	case io.SeekStart:
277		f.position = offset
278	case io.SeekEnd:
279		f.position = int64(f.content.Len()) + offset
280	}
281
282	return f.position, nil
283}
284
285func (f *file) Write(p []byte) (int, error) {
286	return f.WriteAt(p, f.position)
287}
288
289func (f *file) WriteAt(p []byte, off int64) (int, error) {
290	if f.isClosed {
291		return 0, os.ErrClosed
292	}
293
294	if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) {
295		return 0, errors.New("write not supported")
296	}
297
298	n, err := f.content.WriteAt(p, off)
299	f.position = off + int64(n)
300
301	return n, err
302}
303
304func (f *file) Close() error {
305	if f.isClosed {
306		return os.ErrClosed
307	}
308
309	f.isClosed = true
310	return nil
311}
312
313func (f *file) Truncate(size int64) error {
314	if size < int64(len(f.content.bytes)) {
315		f.content.bytes = f.content.bytes[:size]
316	} else if more := int(size) - len(f.content.bytes); more > 0 {
317		f.content.bytes = append(f.content.bytes, make([]byte, more)...)
318	}
319
320	return nil
321}
322
323func (f *file) Duplicate(filename string, mode os.FileMode, flag int) billy.File {
324	new := &file{
325		name:    filename,
326		content: f.content,
327		mode:    mode,
328		flag:    flag,
329	}
330
331	if isTruncate(flag) {
332		new.content.Truncate()
333	}
334
335	if isAppend(flag) {
336		new.position = int64(new.content.Len())
337	}
338
339	return new
340}
341
342func (f *file) Stat() (os.FileInfo, error) {
343	return &fileInfo{
344		name: f.Name(),
345		mode: f.mode,
346		size: f.content.Len(),
347	}, nil
348}
349
350// Lock is a no-op in memfs.
351func (f *file) Lock() error {
352	return nil
353}
354
355// Unlock is a no-op in memfs.
356func (f *file) Unlock() error {
357	return nil
358}
359
360type fileInfo struct {
361	name string
362	size int
363	mode os.FileMode
364}
365
366func (fi *fileInfo) Name() string {
367	return fi.name
368}
369
370func (fi *fileInfo) Size() int64 {
371	return int64(fi.size)
372}
373
374func (fi *fileInfo) Mode() os.FileMode {
375	return fi.mode
376}
377
378func (*fileInfo) ModTime() time.Time {
379	return time.Now()
380}
381
382func (fi *fileInfo) IsDir() bool {
383	return fi.mode.IsDir()
384}
385
386func (*fileInfo) Sys() interface{} {
387	return nil
388}
389
390func (c *content) Truncate() {
391	c.bytes = make([]byte, 0)
392}
393
394func (c *content) Len() int {
395	return len(c.bytes)
396}
397
398func isCreate(flag int) bool {
399	return flag&os.O_CREATE != 0
400}
401
402func isExclusive(flag int) bool {
403	return flag&os.O_EXCL != 0
404}
405
406func isAppend(flag int) bool {
407	return flag&os.O_APPEND != 0
408}
409
410func isTruncate(flag int) bool {
411	return flag&os.O_TRUNC != 0
412}
413
414func isReadAndWrite(flag int) bool {
415	return flag&os.O_RDWR != 0
416}
417
418func isReadOnly(flag int) bool {
419	return flag == os.O_RDONLY
420}
421
422func isWriteOnly(flag int) bool {
423	return flag&os.O_WRONLY != 0
424}
425
426func isSymlink(m os.FileMode) bool {
427	return m&os.ModeSymlink != 0
428}