main
Raw Download raw file
  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}