diff --git a/fs.go b/fs.go index 00bb7a1..a9efccd 100644 --- a/fs.go +++ b/fs.go @@ -13,6 +13,39 @@ var ( ErrCrossedBoundary = errors.New("chroot boundary crossed") ) +// Capability holds the supported features of a billy filesystem. This does +// not mean that the capability has to be supported by the underlying storage. +// For example, a billy filesystem may support WriteCapability but the +// storage be mounted in read only mode. +type Capability uint64 + +const ( + // WriteCapability means that the fs is writable. + WriteCapability Capability = 1 << iota + // ReadCapability means that the fs is readable. + ReadCapability + // ReadAndWriteCapability is the ability to open a file in read and write mode. + ReadAndWriteCapability + // SeekCapability means it is able to move position inside the file. + SeekCapability + // TruncateCapability means that a file can be truncated. + TruncateCapability + // LockCapability is the ability to lock a file. + LockCapability + + // DefaultCapabilities lists all capable features supported by filesystems + // without Capability interface. This list should not be changed until a + // major version is released. + DefaultCapabilities Capability = WriteCapability | ReadCapability | + ReadAndWriteCapability | SeekCapability | TruncateCapability | + LockCapability + + // AllCapabilities lists all capable features. + AllCapabilities Capability = WriteCapability | ReadCapability | + ReadAndWriteCapability | SeekCapability | TruncateCapability | + LockCapability +) + // Filesystem abstract the operations in a storage-agnostic interface. // Each method implementation mimics the behavior of the equivalent functions // at the os package from the standard library. @@ -143,3 +176,27 @@ type File interface { // Truncate the file. Truncate(size int64) error } + +// Capable interface can return the available features of a filesystem. +type Capable interface { + // Capabilities returns the capabilities of a filesystem in bit flags. + Capabilities() Capability +} + +// Capabilities returns the features supported by a filesystem. If the FS +// does not implement Capable interface it returns all features. +func Capabilities(fs Basic) Capability { + capable, ok := fs.(Capable) + if !ok { + return DefaultCapabilities + } + + return capable.Capabilities() +} + +// CapabilityCheck tests the filesystem for the provided capabilities and +// returns true in case it supports all of them. +func CapabilityCheck(fs Basic, capabilities Capability) bool { + fsCaps := Capabilities(fs) + return fsCaps&capabilities == capabilities +} diff --git a/fs_test.go b/fs_test.go new file mode 100644 index 0000000..368eb95 --- /dev/null +++ b/fs_test.go @@ -0,0 +1,40 @@ +package billy_test + +import ( + "testing" + + . "gopkg.in/src-d/go-billy.v4" + "gopkg.in/src-d/go-billy.v4/test" + + . "gopkg.in/check.v1" +) + +type FSSuite struct{} + +func Test(t *testing.T) { TestingT(t) } + +var _ = Suite(&FSSuite{}) + +func (s *FSSuite) TestCapabilities(c *C) { + cases := []struct { + caps Capability + expected bool + }{ + {LockCapability, false}, + {ReadCapability, true}, + {ReadCapability | WriteCapability, true}, + {ReadCapability | WriteCapability | ReadAndWriteCapability | TruncateCapability, true}, + {ReadCapability | WriteCapability | ReadAndWriteCapability | TruncateCapability | LockCapability, false}, + {TruncateCapability | LockCapability, false}, + } + + // This filesystem supports all capabilities except for LockCapability + fs := new(test.NoLockCapFs) + + for _, e := range cases { + c.Assert(CapabilityCheck(fs, e.caps), Equals, e.expected) + } + + dummy := new(test.BasicMock) + c.Assert(Capabilities(dummy), Equals, DefaultCapabilities) +} diff --git a/helper/chroot/chroot.go b/helper/chroot/chroot.go index e3e5a6d..44ddb3d 100644 --- a/helper/chroot/chroot.go +++ b/helper/chroot/chroot.go @@ -217,6 +217,11 @@ func (fs *ChrootHelper) Underlying() billy.Basic { return fs.underlying } +// Capabilities implements the Capable interface. +func (fs *ChrootHelper) Capabilities() billy.Capability { + return billy.Capabilities(fs.underlying) +} + type file struct { billy.File name string diff --git a/helper/chroot/chroot_test.go b/helper/chroot/chroot_test.go index 580347a..819b000 100644 --- a/helper/chroot/chroot_test.go +++ b/helper/chroot/chroot_test.go @@ -351,3 +351,18 @@ func (s *ChrootSuite) TestReadlinkWithBasic(c *C) { _, err := fs.Readlink("") c.Assert(err, Equals, billy.ErrNotSupported) } + +func (s *ChrootSuite) TestCapabilities(c *C) { + testCapabilities(c, new(test.BasicMock)) + testCapabilities(c, new(test.OnlyReadCapFs)) + testCapabilities(c, new(test.NoLockCapFs)) +} + +func testCapabilities(c *C, basic billy.Basic) { + baseCapabilities := billy.Capabilities(basic) + + fs := New(basic, "/foo") + capabilities := billy.Capabilities(fs) + + c.Assert(capabilities, Equals, baseCapabilities) +} diff --git a/helper/mount/mount.go b/helper/mount/mount.go index bb13da5..83f7dd5 100644 --- a/helper/mount/mount.go +++ b/helper/mount/mount.go @@ -167,6 +167,11 @@ func (h *Mount) Underlying() billy.Basic { return h.underlying } +// Capabilities implements the Capable interface. +func (fs *Mount) Capabilities() billy.Capability { + return billy.Capabilities(fs.underlying) & billy.Capabilities(fs.source) +} + func (fs *Mount) getBasicAndPath(path string) (billy.Basic, string) { path = cleanPath(path) if !fs.isMountpoint(path) { diff --git a/helper/mount/mount_test.go b/helper/mount/mount_test.go index 8903dc9..ba558f3 100644 --- a/helper/mount/mount_test.go +++ b/helper/mount/mount_test.go @@ -337,3 +337,34 @@ func (s *MountSuite) TestSourceNotSupported(c *C) { _, err = h.Readlink("foo") c.Assert(err, Equals, billy.ErrNotSupported) } + +func (s *MountSuite) TestCapabilities(c *C) { + testCapabilities(c, new(test.BasicMock), new(test.BasicMock)) + testCapabilities(c, new(test.BasicMock), new(test.OnlyReadCapFs)) + testCapabilities(c, new(test.BasicMock), new(test.NoLockCapFs)) + testCapabilities(c, new(test.OnlyReadCapFs), new(test.BasicMock)) + testCapabilities(c, new(test.OnlyReadCapFs), new(test.OnlyReadCapFs)) + testCapabilities(c, new(test.OnlyReadCapFs), new(test.NoLockCapFs)) + testCapabilities(c, new(test.NoLockCapFs), new(test.BasicMock)) + testCapabilities(c, new(test.NoLockCapFs), new(test.OnlyReadCapFs)) + testCapabilities(c, new(test.NoLockCapFs), new(test.NoLockCapFs)) +} + +func testCapabilities(c *C, a, b billy.Basic) { + aCapabilities := billy.Capabilities(a) + bCapabilities := billy.Capabilities(b) + + fs := New(a, "/foo", b) + capabilities := billy.Capabilities(fs) + + unionCapabilities := aCapabilities & bCapabilities + + c.Assert(capabilities, Equals, unionCapabilities) + + fs = New(b, "/foo", a) + capabilities = billy.Capabilities(fs) + + unionCapabilities = aCapabilities & bCapabilities + + c.Assert(capabilities, Equals, unionCapabilities) +} diff --git a/helper/polyfill/polyfill.go b/helper/polyfill/polyfill.go index a3d85c1..f613c25 100644 --- a/helper/polyfill/polyfill.go +++ b/helper/polyfill/polyfill.go @@ -98,3 +98,8 @@ func (h *Polyfill) Root() string { func (h *Polyfill) Underlying() billy.Basic { return h.Basic } + +// Capabilities implements the Capable interface. +func (h *Polyfill) Capabilities() billy.Capability { + return billy.Capabilities(h.Basic) +} diff --git a/helper/polyfill/polyfill_test.go b/helper/polyfill/polyfill_test.go index b58d19d..2579ed9 100644 --- a/helper/polyfill/polyfill_test.go +++ b/helper/polyfill/polyfill_test.go @@ -61,3 +61,18 @@ func (s *PolyfillSuite) TestChroot(c *C) { func (s *PolyfillSuite) TestRoot(c *C) { c.Assert(s.Helper.Root(), Equals, string(filepath.Separator)) } + +func (s *PolyfillSuite) TestCapabilities(c *C) { + testCapabilities(c, new(test.BasicMock)) + testCapabilities(c, new(test.OnlyReadCapFs)) + testCapabilities(c, new(test.NoLockCapFs)) +} + +func testCapabilities(c *C, basic billy.Basic) { + baseCapabilities := billy.Capabilities(basic) + + fs := New(basic) + capabilities := billy.Capabilities(fs) + + c.Assert(capabilities, Equals, baseCapabilities) +} diff --git a/memfs/memory.go b/memfs/memory.go index 2f8dcae..7eab699 100644 --- a/memfs/memory.go +++ b/memfs/memory.go @@ -190,6 +190,15 @@ func (fs *Memory) Readlink(link string) (string, error) { return string(f.content.bytes), nil } +// Capabilities implements the Capable interface. +func (fs *Memory) Capabilities() billy.Capability { + return billy.WriteCapability | + billy.ReadCapability | + billy.ReadAndWriteCapability | + billy.SeekCapability | + billy.TruncateCapability +} + type file struct { name string content *content @@ -273,7 +282,7 @@ func (f *file) Close() error { func (f *file) Truncate(size int64) error { if size < int64(len(f.content.bytes)) { f.content.bytes = f.content.bytes[:size] - } else if more := int(size)-len(f.content.bytes); more > 0 { + } else if more := int(size) - len(f.content.bytes); more > 0 { f.content.bytes = append(f.content.bytes, make([]byte, more)...) } diff --git a/memfs/memory_test.go b/memfs/memory_test.go index 4de996e..4f15909 100644 --- a/memfs/memory_test.go +++ b/memfs/memory_test.go @@ -3,6 +3,7 @@ package memfs import ( "testing" + "gopkg.in/src-d/go-billy.v4" "gopkg.in/src-d/go-billy.v4/test" . "gopkg.in/check.v1" @@ -20,3 +21,11 @@ var _ = Suite(&MemorySuite{}) func (s *MemorySuite) SetUpTest(c *C) { s.FilesystemSuite = test.NewFilesystemSuite(New()) } + +func (s *MemorySuite) TestCapabilities(c *C) { + _, ok := s.FS.(billy.Capable) + c.Assert(ok, Equals, true) + + caps := billy.Capabilities(s.FS) + c.Assert(caps, Equals, billy.DefaultCapabilities&^billy.LockCapability) +} diff --git a/osfs/os.go b/osfs/os.go index 348cea9..ff35a3b 100644 --- a/osfs/os.go +++ b/osfs/os.go @@ -127,6 +127,11 @@ func (fs *OS) Readlink(link string) (string, error) { return os.Readlink(link) } +// Capabilities implements the Capable interface. +func (fs *OS) Capabilities() billy.Capability { + return billy.DefaultCapabilities +} + // file is a wrapper for an os.File which adds support for file locking. type file struct { *os.File diff --git a/osfs/os_test.go b/osfs/os_test.go index b81943f..3a0258b 100644 --- a/osfs/os_test.go +++ b/osfs/os_test.go @@ -6,8 +6,10 @@ import ( "path/filepath" "testing" - . "gopkg.in/check.v1" + "gopkg.in/src-d/go-billy.v4" "gopkg.in/src-d/go-billy.v4/test" + + . "gopkg.in/check.v1" ) func Test(t *testing.T) { TestingT(t) } @@ -36,3 +38,11 @@ func (s *OSSuite) TestOpenDoesNotCreateDir(c *C) { _, err = os.Stat(filepath.Join(s.path, "dir")) c.Assert(os.IsNotExist(err), Equals, true) } + +func (s *OSSuite) TestCapabilities(c *C) { + _, ok := s.FS.(billy.Capable) + c.Assert(ok, Equals, true) + + caps := billy.Capabilities(s.FS) + c.Assert(caps, Equals, billy.AllCapabilities) +} diff --git a/test/mock.go b/test/mock.go index 2cf7397..519bba6 100644 --- a/test/mock.go +++ b/test/mock.go @@ -134,3 +134,23 @@ func (*FileMock) Unlock() error { func (*FileMock) Truncate(size int64) error { return nil } + +type OnlyReadCapFs struct { + BasicMock +} + +func (o *OnlyReadCapFs) Capabilities() billy.Capability { + return billy.ReadCapability +} + +type NoLockCapFs struct { + BasicMock +} + +func (o *NoLockCapFs) Capabilities() billy.Capability { + return billy.WriteCapability | + billy.ReadCapability | + billy.ReadAndWriteCapability | + billy.SeekCapability | + billy.TruncateCapability +}