Merge pull request #34 from mcuadros/v3

V3 reorganization 
diff --git a/.travis.yml b/.travis.yml
index 01b51bb..5839b61 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,7 +4,7 @@
     - 1.8
     - tip
 
-go_import_path: gopkg.in/src-d/go-billy.v2
+go_import_path: gopkg.in/src-d/go-billy.v3
 
 matrix:
     allow_failures:
diff --git a/README.md b/README.md
index f389b53..d7409a8 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# go-billy [![GoDoc](https://godoc.org/gopkg.in/src-d/go-billy.v2?status.svg)](https://godoc.org/gopkg.in/src-d/go-billy.v2) [![Build Status](https://travis-ci.org/src-d/go-billy.svg)](https://travis-ci.org/src-d/go-billy) [![Build status](https://ci.appveyor.com/api/projects/status/vx2qn6vlakbi724t?svg=true)](https://ci.appveyor.com/project/mcuadros/go-billy) [![codecov](https://codecov.io/gh/src-d/go-billy/branch/master/graph/badge.svg)](https://codecov.io/gh/src-d/go-billy) [![codebeat badge](https://codebeat.co/badges/03bdec03-b477-4472-bbe3-b552e3799174)](https://codebeat.co/projects/github-com-src-d-go-billy)
+# go-billy [![GoDoc](https://godoc.org/gopkg.in/src-d/go-billy.v3?status.svg)](https://godoc.org/gopkg.in/src-d/go-billy.v3) [![Build Status](https://travis-ci.org/src-d/go-billy.svg)](https://travis-ci.org/src-d/go-billy) [![Build status](https://ci.appveyor.com/api/projects/status/vx2qn6vlakbi724t?svg=true)](https://ci.appveyor.com/project/mcuadros/go-billy) [![codecov](https://codecov.io/gh/src-d/go-billy/branch/master/graph/badge.svg)](https://codecov.io/gh/src-d/go-billy) [![codebeat badge](https://codebeat.co/badges/03bdec03-b477-4472-bbe3-b552e3799174)](https://codebeat.co/projects/github-com-src-d-go-billy)
 
 The missing interface filesystem abstraction for Go. 
 Billy implements an interface based on the `os` standard library, allowing to develop applications without dependency on the underlying storage. Make virtually free implement an mocks and testing over filesystem operations.
@@ -8,7 +8,7 @@
 ## Installation
 
 ```go
-go get -u gopkg.in/src-d/go-billy.v2/...
+go get -u gopkg.in/src-d/go-billy.v3/...
 ```
 
 ## Why billy?
diff --git a/appveyor.yml b/appveyor.yml
index 845cfad..4814a61 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,7 +1,7 @@
 version: "{build}"
 platform: x64
 
-clone_folder: c:\gopath\src\gopkg.in\src-d\go-billy.v2
+clone_folder: c:\gopath\src\gopkg.in\src-d\go-billy.v3
 
 environment:
   GOPATH: c:\gopath
diff --git a/fs.go b/fs.go
index 3433eaa..eb5f7ae 100644
--- a/fs.go
+++ b/fs.go
@@ -4,82 +4,135 @@
 	"errors"
 	"io"
 	"os"
+	"time"
 )
 
 var (
-	ErrClosed       = errors.New("file: Writing on closed file.")
-	ErrReadOnly     = errors.New("this is a read-only filesystem")
-	ErrNotSupported = errors.New("feature not supported")
+	ErrReadOnly        = errors.New("read-only filesystem")
+	ErrNotSupported    = errors.New("feature not supported")
+	ErrCrossedBoundary = errors.New("chroot boundary crossed")
 )
 
 // Filesystem abstract the operations in a storage-agnostic interface.
-// It allows you to:
-// * Create files.
-// * Open existing files.
-// * Get info about files.
-// * List files in a directory.
-// * Get a temporal file.
-// * Rename files.
-// * Remove files.
-// * Create directories.
-// * Join parts of path.
-// * Obtain a filesystem starting on a subdirectory in the current filesystem.
-// * Get the base path for the filesystem.
-// Each method implementation varies from implementation to implementation. Refer to
-// the specific documentation for more info.
+// Each method implementation mimics the behavior of the equivalent functions
+// at the os package from the standard library.
 type Filesystem interface {
+	Basic
+	TempFile
+	Dir
+	Symlink
+	Chroot
+}
+
+// Basic abstract the basic operations in a storage-agnostic interface as
+// an extension to the Basic interface.
+type Basic interface {
+	// Create creates the named file with mode 0666 (before umask), truncating
+	// it if it already exists. If successful, methods on the returned File can
+	// be used for I/O; the associated file descriptor has mode O_RDWR.
 	Create(filename string) (File, error)
+	// Open opens the named file for reading. If successful, methods on the
+	// returned file can be used for reading; the associated file descriptor has
+	// mode O_RDONLY.
 	Open(filename string) (File, error)
+	// OpenFile is the generalized open call; most users will use Open or Create
+	// instead. It opens the named file with specified flag (O_RDONLY etc.) and
+	// perm, (0666 etc.) if applicable. If successful, methods on the returned
+	// File can be used for I/O.
 	OpenFile(filename string, flag int, perm os.FileMode) (File, error)
 	// Stat returns a FileInfo describing the named file.
-	Stat(filename string) (FileInfo, error)
+	Stat(filename string) (os.FileInfo, error)
+	// Rename renames (moves) oldpath to newpath. If newpath already exists and
+	// is not a directory, Rename replaces it. OS-specific restrictions may
+	// apply when oldpath and newpath are in different directories.
+	Rename(oldpath, newpath string) error
+	// Remove removes the named file or directory.
+	Remove(filename string) error
+	// Join joins any number of path elements into a single path, adding a
+	// Separator if necessary. Join calls filepath.Clean on the result; in
+	// particular, all empty strings are ignored. On Windows, the result is a
+	// UNC path if and only if the first path element is a UNC path.
+	Join(elem ...string) string
+}
+
+type TempFile interface {
+	// TempFile creates a new temporary file in the directory dir with a name
+	// beginning with prefix, opens the file for reading and writing, and
+	// returns the resulting *os.File. If dir is the empty string, TempFile
+	// uses the default directory for temporary files (see os.TempDir).
+	// Multiple programs calling TempFile simultaneously will not choose the
+	// same file. The caller can use f.Name() to find the pathname of the file.
+	// It is the caller's responsibility to remove the file when no longer
+	// needed.
+	TempFile(dir, prefix string) (File, error)
+}
+
+// Dir abstract the dir related operations in a storage-agnostic interface as
+// an extension to the Basic interface.
+type Dir interface {
+	// ReadDir reads the directory named by dirname and returns a list of
+	// directory entries sorted by filename.
+	ReadDir(path string) ([]os.FileInfo, error)
+	// MkdirAll creates a directory named path, along with any necessary
+	// parents, and returns nil, or else returns an error. The permission bits
+	// perm are used for all directories that MkdirAll creates. If path is/
+	// already a directory, MkdirAll does nothing and returns nil.
+	MkdirAll(filename string, perm os.FileMode) error
+}
+
+// Symlink abstract the symlink related operations in a storage-agnostic
+// interface as an extension to the Basic interface.
+type Symlink interface {
 	// Lstat returns a FileInfo describing the named file. If the file is a
 	// symbolic link, the returned FileInfo describes the symbolic link. Lstat
 	// makes no attempt to follow the link.
-	Lstat(filename string) (FileInfo, error)
-	ReadDir(path string) ([]FileInfo, error)
-	TempFile(dir, prefix string) (File, error)
-	Rename(from, to string) error
-	Remove(filename string) error
-	MkdirAll(filename string, perm os.FileMode) error
-	Join(elem ...string) string
-	Dir(path string) Filesystem
+	Lstat(filename string) (os.FileInfo, error)
 	// Symlink creates a symbolic-link from link to target. target may be an
 	// absolute or relative path, and need not refer to an existing node.
 	// Parent directories of link are created as necessary.
 	Symlink(target, link string) error
-	// Readlink returns the target path of link. An error is returned if link is
-	// not a symbolic-link.
+	// Readlink returns the target path of link.
 	Readlink(link string) (string, error)
-	Base() string
 }
 
-// File implements io.Closer, io.Reader, io.Seeker, and io.Writer>
-// Provides method to obtain the file name and the state of the file (open or closed).
+// Change abstract the FileInfo change related operations in a storage-agnostic
+// interface as an extension to the Basic interface
+type Change interface {
+	// Chmod changes the mode of the named file to mode. If the file is a
+	// symbolic link, it changes the mode of the link's target.
+	Chmod(name string, mode os.FileMode) error
+	// Lchown changes the numeric uid and gid of the named file. If the file is
+	// a symbolic link, it changes the uid and gid of the link itself.
+	Lchown(name string, uid, gid int) error
+	// Chown changes the numeric uid and gid of the named file. If the file is a
+	// symbolic link, it changes the uid and gid of the link's target.
+	Chown(name string, uid, gid int) error
+	// Chtimes changes the access and modification times of the named file,
+	// similar to the Unix utime() or utimes() functions.
+	//
+	// The underlying filesystem may truncate or round the values to a less
+	// precise time unit.
+	Chtimes(name string, atime time.Time, mtime time.Time) error
+}
+
+// Chroot abstract the chroot related operations in a storage-agnostic interface
+// as an extension to the Basic interface.
+type Chroot interface {
+	// Chroot returns a new filesystem from the same type where the new root is
+	// the given path. Files outside of the designated directory tree cannot be
+	// accessed.
+	Chroot(path string) (Basic, error)
+	// Root returns the root path of the filesystem.
+	Root() string
+}
+
+// File represent a file, being a subset of the os.File
 type File interface {
-	Filename() string
-	IsClosed() bool
+	// Name returns the name of the file as presented to Open.
+	Name() string
 	io.Writer
 	io.Reader
+	io.ReaderAt
 	io.Seeker
 	io.Closer
 }
-
-type FileInfo os.FileInfo
-
-type BaseFile struct {
-	BaseFilename string
-	Closed       bool
-}
-
-func (f *BaseFile) Filename() string {
-	return f.BaseFilename
-}
-
-func (f *BaseFile) IsClosed() bool {
-	return f.Closed
-}
-
-type removerAll interface {
-	RemoveAll(string) error
-}
diff --git a/helper/chroot/chroot.go b/helper/chroot/chroot.go
new file mode 100644
index 0000000..e3e8bb7
--- /dev/null
+++ b/helper/chroot/chroot.go
@@ -0,0 +1,281 @@
+package chroot
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+
+	"gopkg.in/src-d/go-billy.v3"
+)
+
+// ChrootHelper is a helper to implement billy.Chroot.
+type ChrootHelper struct {
+	underlying billy.Basic
+	base       string
+
+	dirSupport      bool
+	symlinkSupport  bool
+	tempFileSupport bool
+}
+
+// New creates a new filesystem wrapping up the given 'fs'.
+// The created filesystem has its base in the given ChrootHelperectory of the
+// underlying filesystem.
+func New(fs billy.Basic, base string) billy.Filesystem {
+	helper := &ChrootHelper{underlying: fs, base: base}
+	_, helper.dirSupport = fs.(billy.Dir)
+	_, helper.symlinkSupport = fs.(billy.Symlink)
+	_, helper.tempFileSupport = fs.(billy.TempFile)
+
+	return helper
+}
+
+func (fs *ChrootHelper) underlyingPath(filename string) (string, error) {
+	if isCrossBoundaries(filename) {
+		return "", billy.ErrCrossedBoundary
+	}
+
+	return fs.Join(fs.Root(), filename), nil
+}
+
+func isCrossBoundaries(path string) bool {
+	path = filepath.ToSlash(path)
+	path = filepath.Clean(path)
+
+	return strings.HasPrefix(path, "..")
+}
+
+func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
+	fullpath, err := fs.underlyingPath(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	f, err := fs.underlying.Create(fullpath)
+	if err != nil {
+		return nil, err
+	}
+
+	return newFile(fs, f, filename), nil
+}
+
+func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
+	fullpath, err := fs.underlyingPath(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	f, err := fs.underlying.Open(fullpath)
+	if err != nil {
+		return nil, err
+	}
+
+	return newFile(fs, f, filename), nil
+}
+
+func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) {
+	fullpath, err := fs.underlyingPath(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	f, err := fs.underlying.OpenFile(fullpath, flag, mode)
+	if err != nil {
+		return nil, err
+	}
+
+	return newFile(fs, f, filename), nil
+}
+
+func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) {
+	fullpath, err := fs.underlyingPath(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	return fs.underlying.Stat(fullpath)
+}
+
+func (fs *ChrootHelper) Rename(from, to string) error {
+	var err error
+	from, err = fs.underlyingPath(from)
+	if err != nil {
+		return err
+	}
+
+	to, err = fs.underlyingPath(to)
+	if err != nil {
+		return err
+	}
+
+	return fs.underlying.Rename(from, to)
+}
+
+func (fs *ChrootHelper) Remove(path string) error {
+	fullpath, err := fs.underlyingPath(path)
+	if err != nil {
+		return err
+	}
+
+	return fs.underlying.Remove(fullpath)
+}
+
+func (fs *ChrootHelper) Join(elem ...string) string {
+	return fs.underlying.Join(elem...)
+}
+
+func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) {
+	if !fs.tempFileSupport {
+		return nil, billy.ErrNotSupported
+	}
+
+	fullpath, err := fs.underlyingPath(dir)
+	if err != nil {
+		return nil, err
+	}
+
+	f, err := fs.underlying.(billy.TempFile).TempFile(fullpath, prefix)
+	if err != nil {
+		return nil, err
+	}
+
+	return newFile(fs, f, fs.Join(dir, filepath.Base(f.Name()))), nil
+}
+
+func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) {
+	if !fs.dirSupport {
+		return nil, billy.ErrNotSupported
+	}
+
+	fullpath, err := fs.underlyingPath(path)
+	if err != nil {
+		return nil, err
+	}
+
+	return fs.underlying.(billy.Dir).ReadDir(fullpath)
+}
+
+func (fs *ChrootHelper) MkdirAll(filename string, perm os.FileMode) error {
+	if !fs.dirSupport {
+		return billy.ErrNotSupported
+	}
+
+	fullpath, err := fs.underlyingPath(filename)
+	if err != nil {
+		return err
+	}
+
+	return fs.underlying.(billy.Dir).MkdirAll(fullpath, perm)
+}
+
+func (fs *ChrootHelper) Lstat(filename string) (os.FileInfo, error) {
+	if !fs.symlinkSupport {
+		return nil, billy.ErrNotSupported
+	}
+
+	fullpath, err := fs.underlyingPath(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	return fs.underlying.(billy.Symlink).Lstat(fullpath)
+}
+
+func (fs *ChrootHelper) Symlink(target, link string) error {
+	if !fs.symlinkSupport {
+		return billy.ErrNotSupported
+	}
+
+	target = filepath.FromSlash(target)
+
+	// only rewrite target if it's already absolute
+	if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
+		target = fs.Join(fs.Root(), target)
+		target = filepath.Clean(filepath.FromSlash(target))
+	}
+
+	if fs.isTargetOutBounders(link, target) {
+		return billy.ErrCrossedBoundary
+	}
+
+	link, err := fs.underlyingPath(link)
+	if err != nil {
+		return err
+	}
+
+	return fs.underlying.(billy.Symlink).Symlink(target, link)
+}
+
+func (fs *ChrootHelper) isTargetOutBounders(link, target string) bool {
+	fulllink := fs.Join(fs.base, link)
+	fullpath := fs.Join(filepath.Dir(fulllink), target)
+	target, err := filepath.Rel(fs.base, fullpath)
+	if err != nil {
+		return true
+	}
+
+	return isCrossBoundaries(target)
+}
+
+func (fs *ChrootHelper) Readlink(link string) (string, error) {
+	if !fs.symlinkSupport {
+		return "", billy.ErrNotSupported
+	}
+
+	fullpath, err := fs.underlyingPath(link)
+	if err != nil {
+		return "", err
+	}
+
+	target, err := fs.underlying.(billy.Symlink).Readlink(fullpath)
+	if err != nil {
+		return "", err
+	}
+
+	if !filepath.IsAbs(target) && !strings.HasPrefix(target, string(filepath.Separator)) {
+		return target, nil
+	}
+
+	target, err = filepath.Rel(fs.base, target)
+	if err != nil {
+		return "", err
+	}
+
+	return string(os.PathSeparator) + target, nil
+}
+
+func (fs *ChrootHelper) Chroot(path string) (billy.Basic, error) {
+	fullpath, err := fs.underlyingPath(path)
+	if err != nil {
+		return nil, err
+	}
+
+	return New(fs.underlying, fullpath), nil
+}
+
+func (fs *ChrootHelper) Root() string {
+	return fs.base
+}
+
+func (fs *ChrootHelper) Underlying() billy.Basic {
+	return fs.underlying
+}
+
+type file struct {
+	billy.File
+	name string
+}
+
+func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
+	filename = fs.Join(fs.Root(), filename)
+	filename, _ = filepath.Rel(fs.Root(), filename)
+
+	return &file{
+		File: f,
+		name: filename,
+	}
+}
+
+func (f *file) Name() string {
+	return f.name
+}
diff --git a/helper/chroot/chroot_test.go b/helper/chroot/chroot_test.go
new file mode 100644
index 0000000..27edd78
--- /dev/null
+++ b/helper/chroot/chroot_test.go
@@ -0,0 +1,344 @@
+package chroot
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"gopkg.in/src-d/go-billy.v3"
+	"gopkg.in/src-d/go-billy.v3/test"
+
+	. "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+var _ = Suite(&ChrootSuite{})
+
+type ChrootSuite struct{}
+
+func (s *ChrootSuite) TestCreate(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	f, err := fs.Create("bar/qux")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, filepath.Join("bar", "qux"))
+
+	c.Assert(m.CreateArgs, HasLen, 1)
+	c.Assert(m.CreateArgs[0], Equals, "/foo/bar/qux")
+}
+
+func (s *ChrootSuite) TestCreateErrCrossedBoundary(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.Create("../foo")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestOpen(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	f, err := fs.Open("bar/qux")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, filepath.Join("bar", "qux"))
+
+	c.Assert(m.OpenArgs, HasLen, 1)
+	c.Assert(m.OpenArgs[0], Equals, "/foo/bar/qux")
+}
+
+func (s *ChrootSuite) TestChroot(c *C) {
+	m := &test.BasicMock{}
+
+	fs, _ := New(m, "/foo").Chroot("baz")
+	f, err := fs.Open("bar/qux")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, filepath.Join("bar", "qux"))
+
+	c.Assert(m.OpenArgs, HasLen, 1)
+	c.Assert(m.OpenArgs[0], Equals, "/foo/baz/bar/qux")
+}
+
+func (s *ChrootSuite) TestChrootErrCrossedBoundary(c *C) {
+	m := &test.BasicMock{}
+
+	fs, err := New(m, "/foo").Chroot("../qux")
+	c.Assert(fs, IsNil)
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestOpenErrCrossedBoundary(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.Open("../foo")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestOpenFile(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	f, err := fs.OpenFile("bar/qux", 42, 0777)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, filepath.Join("bar", "qux"))
+
+	c.Assert(m.OpenFileArgs, HasLen, 1)
+	c.Assert(m.OpenFileArgs[0], Equals, [3]interface{}{"/foo/bar/qux", 42, os.FileMode(0777)})
+}
+
+func (s *ChrootSuite) TestOpenFileErrCrossedBoundary(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.OpenFile("../foo", 42, 0777)
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestStat(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.Stat("bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(m.StatArgs, HasLen, 1)
+	c.Assert(m.StatArgs[0], Equals, "/foo/bar/qux")
+}
+
+func (s *ChrootSuite) TestStatErrCrossedBoundary(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.Stat("../foo")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestRename(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	err := fs.Rename("bar/qux", "qux/bar")
+	c.Assert(err, IsNil)
+
+	c.Assert(m.RenameArgs, HasLen, 1)
+	c.Assert(m.RenameArgs[0], Equals, [2]string{"/foo/bar/qux", "/foo/qux/bar"})
+}
+
+func (s *ChrootSuite) TestRenameErrCrossedBoundary(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	err := fs.Rename("../foo", "bar")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+
+	err = fs.Rename("foo", "../bar")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestRemove(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	err := fs.Remove("bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(m.RemoveArgs, HasLen, 1)
+	c.Assert(m.RemoveArgs[0], Equals, "/foo/bar/qux")
+}
+
+func (s *ChrootSuite) TestRemoveErrCrossedBoundary(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	err := fs.Remove("../foo")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestTempFile(c *C) {
+	m := &test.TempFileMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.TempFile("bar", "qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(m.TempFileArgs, HasLen, 1)
+	c.Assert(m.TempFileArgs[0], Equals, [2]string{"/foo/bar", "qux"})
+}
+
+func (s *ChrootSuite) TestTempFileErrCrossedBoundary(c *C) {
+	m := &test.TempFileMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.TempFile("../foo", "qux")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestTempFileWithBasic(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.TempFile("", "")
+	c.Assert(err, Equals, billy.ErrNotSupported)
+}
+
+func (s *ChrootSuite) TestReadDir(c *C) {
+	m := &test.DirMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.ReadDir("bar")
+	c.Assert(err, IsNil)
+
+	c.Assert(m.ReadDirArgs, HasLen, 1)
+	c.Assert(m.ReadDirArgs[0], Equals, "/foo/bar")
+}
+
+func (s *ChrootSuite) TestReadDirErrCrossedBoundary(c *C) {
+	m := &test.DirMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.ReadDir("../foo")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestReadDirWithBasic(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.ReadDir("")
+	c.Assert(err, Equals, billy.ErrNotSupported)
+}
+
+func (s *ChrootSuite) TestMkDirAll(c *C) {
+	m := &test.DirMock{}
+
+	fs := New(m, "/foo")
+	err := fs.MkdirAll("bar", 0777)
+	c.Assert(err, IsNil)
+
+	c.Assert(m.MkdirAllArgs, HasLen, 1)
+	c.Assert(m.MkdirAllArgs[0], Equals, [2]interface{}{"/foo/bar", os.FileMode(0777)})
+}
+
+func (s *ChrootSuite) TestMkdirAllErrCrossedBoundary(c *C) {
+	m := &test.DirMock{}
+
+	fs := New(m, "/foo")
+	err := fs.MkdirAll("../foo", 0777)
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestMkdirAllWithBasic(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	err := fs.MkdirAll("", 0)
+	c.Assert(err, Equals, billy.ErrNotSupported)
+}
+
+func (s *ChrootSuite) TestLstat(c *C) {
+	m := &test.SymlinkMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.Lstat("qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(m.LstatArgs, HasLen, 1)
+	c.Assert(m.LstatArgs[0], Equals, "/foo/qux")
+}
+
+func (s *ChrootSuite) TestLstatErrCrossedBoundary(c *C) {
+	m := &test.SymlinkMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.Lstat("../qux")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestLstatWithBasic(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.Lstat("")
+	c.Assert(err, Equals, billy.ErrNotSupported)
+}
+
+func (s *ChrootSuite) TestSymlink(c *C) {
+	m := &test.SymlinkMock{}
+
+	fs := New(m, "/foo")
+	err := fs.Symlink("../baz", "qux/bar")
+	c.Assert(err, IsNil)
+
+	c.Assert(m.SymlinkArgs, HasLen, 1)
+	c.Assert(m.SymlinkArgs[0], Equals, [2]string{filepath.FromSlash("../baz"), "/foo/qux/bar"})
+}
+
+func (s *ChrootSuite) TestSymlinkWithAbsoluteTarget(c *C) {
+	m := &test.SymlinkMock{}
+
+	fs := New(m, "/foo")
+	err := fs.Symlink("/bar", "qux/baz")
+	c.Assert(err, IsNil)
+
+	c.Assert(m.SymlinkArgs, HasLen, 1)
+	c.Assert(m.SymlinkArgs[0], Equals, [2]string{filepath.FromSlash("/foo/bar"), "/foo/qux/baz"})
+}
+
+func (s *ChrootSuite) TestSymlinkErrCrossedBoundary(c *C) {
+	m := &test.SymlinkMock{}
+
+	fs := New(m, "/foo")
+	err := fs.Symlink("qux", "../foo")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestSymlinkWithBasic(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	err := fs.Symlink("", "")
+	c.Assert(err, Equals, billy.ErrNotSupported)
+}
+
+func (s *ChrootSuite) TestReadlink(c *C) {
+	m := &test.SymlinkMock{}
+
+	fs := New(m, "/foo")
+	link, err := fs.Readlink("/qux")
+	c.Assert(err, IsNil)
+	c.Assert(link, Equals, filepath.FromSlash("/qux"))
+
+	c.Assert(m.ReadlinkArgs, HasLen, 1)
+	c.Assert(m.ReadlinkArgs[0], Equals, "/foo/qux")
+}
+
+func (s *ChrootSuite) TestReadlinkWithRelative(c *C) {
+	m := &test.SymlinkMock{}
+
+	fs := New(m, "/foo")
+	link, err := fs.Readlink("qux/bar")
+	c.Assert(err, IsNil)
+	c.Assert(link, Equals, filepath.FromSlash("/qux/bar"))
+
+	c.Assert(m.ReadlinkArgs, HasLen, 1)
+	c.Assert(m.ReadlinkArgs[0], Equals, "/foo/qux/bar")
+}
+
+func (s *ChrootSuite) TestReadlinkErrCrossedBoundary(c *C) {
+	m := &test.SymlinkMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.Readlink("../qux")
+	c.Assert(err, Equals, billy.ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestReadlinkWithBasic(c *C) {
+	m := &test.BasicMock{}
+
+	fs := New(m, "/foo")
+	_, err := fs.Readlink("")
+	c.Assert(err, Equals, billy.ErrNotSupported)
+}
diff --git a/helper/mount/mount.go b/helper/mount/mount.go
new file mode 100644
index 0000000..74c1e88
--- /dev/null
+++ b/helper/mount/mount.go
@@ -0,0 +1,284 @@
+package mount
+
+import (
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"fmt"
+
+	"gopkg.in/src-d/go-billy.v3"
+)
+
+var separator = string(filepath.Separator)
+
+// Mount is a helper that allows to emulate the behavior of mount in memory.
+// Very usufull to create a temporal dir, on filesystem where is a performance
+// penalty in doing so.
+type Mount struct {
+	underlying billy.Basic
+	source     billy.Basic
+	mountpoint string
+
+	uc capabilities // underlying
+	sc capabilities // source
+}
+
+type capabilities struct{ dir, symlink bool }
+
+// New creates a new filesystem wrapping up 'fs' the intercepts all the calls
+// made to `mountpoint` path and redirecting it to `source` filesystem.
+func New(fs billy.Basic, mountpoint string, source billy.Basic) *Mount {
+	h := &Mount{
+		underlying: fs,
+		source:     source,
+		mountpoint: cleanPath(mountpoint),
+	}
+
+	_, h.sc.dir = h.source.(billy.Dir)
+	_, h.sc.symlink = h.source.(billy.Symlink)
+	_, h.uc.dir = h.underlying.(billy.Dir)
+	_, h.uc.symlink = h.underlying.(billy.Symlink)
+	return h
+}
+
+func (h *Mount) Create(path string) (billy.File, error) {
+	fs, fullpath := h.getBasicAndPath(path)
+	if fullpath == "." {
+		return nil, os.ErrInvalid
+	}
+
+	f, err := fs.Create(fullpath)
+	return wrapFile(f, path), err
+}
+
+func (h *Mount) Open(path string) (billy.File, error) {
+	fs, fullpath := h.getBasicAndPath(path)
+	if fullpath == "." {
+		return nil, os.ErrInvalid
+	}
+
+	f, err := fs.Open(fullpath)
+	return wrapFile(f, path), err
+}
+
+func (h *Mount) OpenFile(path string, flag int, mode os.FileMode) (billy.File, error) {
+	fs, fullpath := h.getBasicAndPath(path)
+	if fullpath == "." {
+		return nil, os.ErrInvalid
+	}
+
+	f, err := fs.OpenFile(fullpath, flag, mode)
+	return wrapFile(f, path), err
+}
+
+func (h *Mount) Rename(from, to string) error {
+	fromInSource := h.isMountpoint(from)
+	toInSource := h.isMountpoint(to)
+
+	var fromFS, toFS billy.Basic
+
+	switch {
+	case fromInSource && toInSource:
+		from = h.mustRelToMountpoint(from)
+		to = h.mustRelToMountpoint(to)
+		return h.source.Rename(from, to)
+	case !fromInSource && !toInSource:
+		return h.underlying.Rename(from, to)
+	case fromInSource && !toInSource:
+		fromFS = h.source
+		from = h.mustRelToMountpoint(from)
+		toFS = h.underlying
+		to = cleanPath(to)
+	case !fromInSource && toInSource:
+		fromFS = h.underlying
+		from = cleanPath(from)
+		toFS = h.source
+		to = h.mustRelToMountpoint(to)
+	}
+
+	if err := copyPath(fromFS, toFS, from, to); err != nil {
+		return err
+	}
+
+	return fromFS.Remove(from)
+}
+
+func (h *Mount) Stat(path string) (os.FileInfo, error) {
+	fs, fullpath := h.getBasicAndPath(path)
+	return fs.Stat(fullpath)
+}
+
+func (h *Mount) Remove(path string) error {
+	fs, fullpath := h.getBasicAndPath(path)
+	if fullpath == "." {
+		return os.ErrInvalid
+	}
+
+	return fs.Remove(fullpath)
+}
+
+func (h *Mount) ReadDir(path string) ([]os.FileInfo, error) {
+	fs, fullpath, err := h.getDirAndPath(path)
+	if err != nil {
+		return nil, err
+	}
+
+	return fs.ReadDir(fullpath)
+}
+
+func (h *Mount) MkdirAll(filename string, perm os.FileMode) error {
+	fs, fullpath, err := h.getDirAndPath(filename)
+	if err != nil {
+		return err
+	}
+
+	return fs.MkdirAll(fullpath, perm)
+}
+
+func (h *Mount) Symlink(target, link string) error {
+	fs, fullpath, err := h.getSymlinkAndPath(link)
+	if err != nil {
+		return err
+	}
+
+	resolved := filepath.Join(filepath.Dir(link), target)
+	if h.isMountpoint(resolved) != h.isMountpoint(link) {
+		return fmt.Errorf("invalid symlink, target is crossing filesystems")
+	}
+
+	return fs.Symlink(target, fullpath)
+}
+
+func (h *Mount) Readlink(link string) (string, error) {
+	fs, fullpath, err := h.getSymlinkAndPath(link)
+	if err != nil {
+		return "", err
+	}
+
+	return fs.Readlink(fullpath)
+}
+
+func (h *Mount) Lstat(path string) (os.FileInfo, error) {
+	fs, fullpath, err := h.getSymlinkAndPath(path)
+	if err != nil {
+		return nil, err
+	}
+
+	return fs.Lstat(fullpath)
+}
+
+func (fs *Mount) getBasicAndPath(path string) (billy.Basic, string) {
+	path = cleanPath(path)
+	if !fs.isMountpoint(path) {
+		return fs.underlying, path
+	}
+
+	return fs.source, fs.mustRelToMountpoint(path)
+}
+
+func (fs *Mount) getDirAndPath(path string) (billy.Dir, string, error) {
+	path = cleanPath(path)
+	if !fs.isMountpoint(path) {
+		if !fs.uc.dir {
+			return nil, "", billy.ErrNotSupported
+		}
+
+		return fs.underlying.(billy.Dir), path, nil
+	}
+
+	if !fs.sc.dir {
+		return nil, "", billy.ErrNotSupported
+	}
+
+	return fs.source.(billy.Dir), fs.mustRelToMountpoint(path), nil
+}
+
+func (fs *Mount) getSymlinkAndPath(path string) (billy.Symlink, string, error) {
+	path = cleanPath(path)
+	if !fs.isMountpoint(path) {
+		if !fs.uc.symlink {
+			return nil, "", billy.ErrNotSupported
+		}
+
+		return fs.underlying.(billy.Symlink), path, nil
+	}
+
+	if !fs.sc.symlink {
+		return nil, "", billy.ErrNotSupported
+	}
+
+	return fs.source.(billy.Symlink), fs.mustRelToMountpoint(path), nil
+}
+
+func (fs *Mount) mustRelToMountpoint(path string) string {
+	path = cleanPath(path)
+	fullpath, err := filepath.Rel(fs.mountpoint, path)
+	if err != nil {
+		panic(err)
+	}
+
+	return fullpath
+}
+
+func (fs *Mount) isMountpoint(path string) bool {
+	path = cleanPath(path)
+	return strings.HasPrefix(path, fs.mountpoint)
+}
+
+func cleanPath(path string) string {
+	path = filepath.FromSlash(path)
+	rel, err := filepath.Rel(separator, path)
+	if err == nil {
+		path = rel
+	}
+
+	return filepath.Clean(path)
+}
+
+// copyPath copies a file across filesystems.
+func copyPath(src, dst billy.Basic, srcPath, dstPath string) error {
+	dstFile, err := dst.Create(dstPath)
+	if err != nil {
+		return err
+	}
+
+	srcFile, err := src.Open(srcPath)
+	if err != nil {
+		return nil
+	}
+
+	_, err = io.Copy(dstFile, srcFile)
+	if err != nil {
+		return nil
+	}
+
+	err = dstFile.Close()
+	if err != nil {
+		_ = srcFile.Close()
+		return err
+	}
+
+	return srcFile.Close()
+}
+
+type file struct {
+	billy.File
+	name string
+}
+
+func wrapFile(f billy.File, filename string) billy.File {
+	if f == nil {
+		return nil
+	}
+
+	return &file{
+		File: f,
+		name: cleanPath(filename),
+	}
+}
+
+func (f *file) Name() string {
+	return f.name
+}
diff --git a/helper/mount/mount_test.go b/helper/mount/mount_test.go
new file mode 100644
index 0000000..20af8d0
--- /dev/null
+++ b/helper/mount/mount_test.go
@@ -0,0 +1,331 @@
+package mount
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"gopkg.in/src-d/go-billy.v3"
+	"gopkg.in/src-d/go-billy.v3/memfs"
+	"gopkg.in/src-d/go-billy.v3/test"
+	"gopkg.in/src-d/go-billy.v3/util"
+
+	. "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+var _ = Suite(&MountSuite{})
+
+type MountSuite struct {
+	Helper     *Mount
+	Underlying mock
+	Source     mock
+}
+
+type mock struct {
+	test.BasicMock
+	test.DirMock
+	test.SymlinkMock
+}
+
+func (s *MountSuite) SetUpTest(c *C) {
+	s.Underlying.BasicMock = test.BasicMock{}
+	s.Underlying.DirMock = test.DirMock{}
+	s.Underlying.SymlinkMock = test.SymlinkMock{}
+	s.Source.BasicMock = test.BasicMock{}
+	s.Source.DirMock = test.DirMock{}
+	s.Source.SymlinkMock = test.SymlinkMock{}
+
+	s.Helper = New(&s.Underlying, "/foo", &s.Source)
+}
+
+func (s *MountSuite) TestCreate(c *C) {
+	f, err := s.Helper.Create("bar/qux")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, filepath.Join("bar", "qux"))
+
+	c.Assert(s.Underlying.CreateArgs, HasLen, 1)
+	c.Assert(s.Underlying.CreateArgs[0], Equals, filepath.Join("bar", "qux"))
+	c.Assert(s.Source.CreateArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestCreateMountPoint(c *C) {
+	f, err := s.Helper.Create("foo")
+	c.Assert(f, IsNil)
+	c.Assert(err, Equals, os.ErrInvalid)
+}
+
+func (s *MountSuite) TestCreateInMount(c *C) {
+	f, err := s.Helper.Create("foo/bar/qux")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, filepath.Join("foo", "bar", "qux"))
+
+	c.Assert(s.Underlying.CreateArgs, HasLen, 0)
+	c.Assert(s.Source.CreateArgs, HasLen, 1)
+	c.Assert(s.Source.CreateArgs[0], Equals, filepath.Join("bar", "qux"))
+}
+
+func (s *MountSuite) TestOpen(c *C) {
+	f, err := s.Helper.Open("bar/qux")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, filepath.Join("bar", "qux"))
+
+	c.Assert(s.Underlying.OpenArgs, HasLen, 1)
+	c.Assert(s.Underlying.OpenArgs[0], Equals, filepath.Join("bar", "qux"))
+	c.Assert(s.Source.OpenArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestOpenMountPoint(c *C) {
+	f, err := s.Helper.Open("foo")
+	c.Assert(f, IsNil)
+	c.Assert(err, Equals, os.ErrInvalid)
+}
+
+func (s *MountSuite) TestOpenInMount(c *C) {
+	f, err := s.Helper.Open("foo/bar/qux")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, filepath.Join("foo", "bar", "qux"))
+
+	c.Assert(s.Underlying.OpenArgs, HasLen, 0)
+	c.Assert(s.Source.OpenArgs, HasLen, 1)
+	c.Assert(s.Source.OpenArgs[0], Equals, filepath.Join("bar", "qux"))
+}
+
+func (s *MountSuite) TestOpenFile(c *C) {
+	f, err := s.Helper.OpenFile("bar/qux", 42, 0777)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, filepath.Join("bar", "qux"))
+
+	c.Assert(s.Underlying.OpenFileArgs, HasLen, 1)
+	c.Assert(s.Underlying.OpenFileArgs[0], Equals,
+		[3]interface{}{filepath.Join("bar", "qux"), 42, os.FileMode(0777)})
+	c.Assert(s.Source.OpenFileArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestOpenFileMountPoint(c *C) {
+	f, err := s.Helper.OpenFile("foo", 42, 0777)
+	c.Assert(f, IsNil)
+	c.Assert(err, Equals, os.ErrInvalid)
+}
+
+func (s *MountSuite) TestOpenFileInMount(c *C) {
+	f, err := s.Helper.OpenFile("foo/bar/qux", 42, 0777)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, filepath.Join("foo", "bar", "qux"))
+
+	c.Assert(s.Underlying.OpenFileArgs, HasLen, 0)
+	c.Assert(s.Source.OpenFileArgs, HasLen, 1)
+	c.Assert(s.Source.OpenFileArgs[0], Equals,
+		[3]interface{}{filepath.Join("bar", "qux"), 42, os.FileMode(0777)})
+}
+
+func (s *MountSuite) TestStat(c *C) {
+	_, err := s.Helper.Stat("bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.StatArgs, HasLen, 1)
+	c.Assert(s.Underlying.StatArgs[0], Equals, filepath.Join("bar", "qux"))
+	c.Assert(s.Source.StatArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestStatInMount(c *C) {
+	_, err := s.Helper.Stat("foo/bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.StatArgs, HasLen, 0)
+	c.Assert(s.Source.StatArgs, HasLen, 1)
+	c.Assert(s.Source.StatArgs[0], Equals, filepath.Join("bar", "qux"))
+}
+
+func (s *MountSuite) TestRename(c *C) {
+	err := s.Helper.Rename("bar/qux", "qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.RenameArgs, HasLen, 1)
+	c.Assert(s.Underlying.RenameArgs[0], Equals, [2]string{"bar/qux", "qux"})
+	c.Assert(s.Source.RenameArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestRenameInMount(c *C) {
+	err := s.Helper.Rename("foo/bar/qux", "foo/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.RenameArgs, HasLen, 0)
+	c.Assert(s.Source.RenameArgs, HasLen, 1)
+	c.Assert(s.Source.RenameArgs[0], Equals,
+		[2]string{filepath.Join("bar", "qux"), "qux"})
+}
+
+func (s *MountSuite) TestRenameCross(c *C) {
+	underlying := memfs.New()
+	source := memfs.New()
+
+	util.WriteFile(underlying, "file", []byte("foo"), 0777)
+
+	fs := New(underlying, "/foo", source)
+	err := fs.Rename("file", "foo/file")
+	c.Assert(err, IsNil)
+
+	_, err = underlying.Stat("file")
+	c.Assert(err, Equals, os.ErrNotExist)
+
+	_, err = source.Stat("file")
+	c.Assert(err, IsNil)
+
+	err = fs.Rename("foo/file", "file")
+	c.Assert(err, IsNil)
+
+	_, err = underlying.Stat("file")
+	c.Assert(err, IsNil)
+
+	_, err = source.Stat("file")
+	c.Assert(err, Equals, os.ErrNotExist)
+}
+
+func (s *MountSuite) TestRemove(c *C) {
+	err := s.Helper.Remove("bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.RemoveArgs, HasLen, 1)
+	c.Assert(s.Underlying.RemoveArgs[0], Equals, filepath.Join("bar", "qux"))
+	c.Assert(s.Source.RemoveArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestRemoveMountPoint(c *C) {
+	err := s.Helper.Remove("foo")
+	c.Assert(err, Equals, os.ErrInvalid)
+}
+
+func (s *MountSuite) TestRemoveInMount(c *C) {
+	err := s.Helper.Remove("foo/bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.RemoveArgs, HasLen, 0)
+	c.Assert(s.Source.RemoveArgs, HasLen, 1)
+	c.Assert(s.Source.RemoveArgs[0], Equals, filepath.Join("bar", "qux"))
+}
+
+func (s *MountSuite) TestReadDir(c *C) {
+	_, err := s.Helper.ReadDir("bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.ReadDirArgs, HasLen, 1)
+	c.Assert(s.Underlying.ReadDirArgs[0], Equals, filepath.Join("bar", "qux"))
+	c.Assert(s.Source.ReadDirArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestReadDirInMount(c *C) {
+	_, err := s.Helper.ReadDir("foo/bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.ReadDirArgs, HasLen, 0)
+	c.Assert(s.Source.ReadDirArgs, HasLen, 1)
+	c.Assert(s.Source.ReadDirArgs[0], Equals, filepath.Join("bar", "qux"))
+}
+
+func (s *MountSuite) TestMkdirAll(c *C) {
+	err := s.Helper.MkdirAll("bar/qux", 0777)
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.MkdirAllArgs, HasLen, 1)
+	c.Assert(s.Underlying.MkdirAllArgs[0], Equals,
+		[2]interface{}{filepath.Join("bar", "qux"), os.FileMode(0777)})
+	c.Assert(s.Source.MkdirAllArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestMkdirAllInMount(c *C) {
+	err := s.Helper.MkdirAll("foo/bar/qux", 0777)
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.MkdirAllArgs, HasLen, 0)
+	c.Assert(s.Source.MkdirAllArgs, HasLen, 1)
+	c.Assert(s.Source.MkdirAllArgs[0], Equals,
+		[2]interface{}{filepath.Join("bar", "qux"), os.FileMode(0777)})
+}
+
+func (s *MountSuite) TestLstat(c *C) {
+	_, err := s.Helper.Lstat("bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.LstatArgs, HasLen, 1)
+	c.Assert(s.Underlying.LstatArgs[0], Equals, filepath.Join("bar", "qux"))
+	c.Assert(s.Source.LstatArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestLstatInMount(c *C) {
+	_, err := s.Helper.Lstat("foo/bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.LstatArgs, HasLen, 0)
+	c.Assert(s.Source.LstatArgs, HasLen, 1)
+	c.Assert(s.Source.LstatArgs[0], Equals, filepath.Join("bar", "qux"))
+}
+
+func (s *MountSuite) TestSymlink(c *C) {
+	err := s.Helper.Symlink("../baz", "bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.SymlinkArgs, HasLen, 1)
+	c.Assert(s.Underlying.SymlinkArgs[0], Equals,
+		[2]string{"../baz", filepath.Join("bar", "qux")})
+	c.Assert(s.Source.SymlinkArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestSymlinkCrossMount(c *C) {
+	err := s.Helper.Symlink("../foo", "bar/qux")
+	c.Assert(err, NotNil)
+
+	err = s.Helper.Symlink("../foo/qux", "bar/qux")
+	c.Assert(err, NotNil)
+
+	err = s.Helper.Symlink("../baz", "foo")
+	c.Assert(err, NotNil)
+
+	err = s.Helper.Symlink("../../../foo", "foo/bar/qux")
+	c.Assert(err, NotNil)
+}
+
+func (s *MountSuite) TestSymlinkInMount(c *C) {
+	err := s.Helper.Symlink("../baz", "foo/bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.SymlinkArgs, HasLen, 0)
+	c.Assert(s.Source.SymlinkArgs, HasLen, 1)
+	c.Assert(s.Source.SymlinkArgs[0], Equals,
+		[2]string{"../baz", filepath.Join("bar", "qux")})
+}
+
+func (s *MountSuite) TestRadlink(c *C) {
+	_, err := s.Helper.Readlink("bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.ReadlinkArgs, HasLen, 1)
+	c.Assert(s.Underlying.ReadlinkArgs[0], Equals, filepath.Join("bar", "qux"))
+	c.Assert(s.Source.ReadlinkArgs, HasLen, 0)
+}
+
+func (s *MountSuite) TestReadlinkInMount(c *C) {
+	_, err := s.Helper.Readlink("foo/bar/qux")
+	c.Assert(err, IsNil)
+
+	c.Assert(s.Underlying.ReadlinkArgs, HasLen, 0)
+	c.Assert(s.Source.ReadlinkArgs, HasLen, 1)
+	c.Assert(s.Source.ReadlinkArgs[0], Equals, filepath.Join("bar", "qux"))
+}
+
+func (s *MountSuite) TestUnderlyingNotSupported(c *C) {
+	h := New(&test.BasicMock{}, "/foo", &test.BasicMock{})
+	_, err := h.ReadDir("qux")
+	c.Assert(err, Equals, billy.ErrNotSupported)
+	_, err = h.Readlink("qux")
+	c.Assert(err, Equals, billy.ErrNotSupported)
+}
+
+func (s *MountSuite) TestSourceNotSupported(c *C) {
+	h := New(&s.Underlying, "/foo", &test.BasicMock{})
+	_, err := h.ReadDir("foo")
+	c.Assert(err, Equals, billy.ErrNotSupported)
+	_, err = h.Readlink("foo")
+	c.Assert(err, Equals, billy.ErrNotSupported)
+}
diff --git a/memfs/memory.go b/memfs/memory.go
index e31159c..60316dd 100644
--- a/memfs/memory.go
+++ b/memfs/memory.go
@@ -1,5 +1,5 @@
 // Package memfs provides a billy filesystem base on memory.
-package memfs // import "gopkg.in/src-d/go-billy.v2/memfs"
+package memfs // import "gopkg.in/src-d/go-billy.v3/memfs"
 
 import (
 	"errors"
@@ -10,53 +10,48 @@
 	"strings"
 	"time"
 
-	"gopkg.in/src-d/go-billy.v2"
+	"gopkg.in/src-d/go-billy.v3"
+	"gopkg.in/src-d/go-billy.v3/helper/chroot"
+	"gopkg.in/src-d/go-billy.v3/util"
 )
 
 const separator = filepath.Separator
 
 // Memory a very convenient filesystem based on memory files
 type Memory struct {
-	base string
-	s    *storage
+	s *storage
 
 	tempCount int
 }
 
-//New returns a new Memory filesystem
-func New() *Memory {
-	return &Memory{
-		base: string(separator),
-		s:    newStorage(),
-	}
+//New returns a new Memory filesystem.
+func New() billy.Filesystem {
+	fs := &Memory{s: newStorage()}
+	return chroot.New(fs, string(separator))
 }
 
-// Create returns a new file in memory from a given filename.
 func (fs *Memory) Create(filename string) (billy.File, error) {
 	return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
 }
 
-// Open returns a readonly file from a given name.
 func (fs *Memory) Open(filename string) (billy.File, error) {
 	return fs.OpenFile(filename, os.O_RDONLY, 0)
 }
 
-// OpenFile returns the file from a given name with given flag and permits.
 func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
-	fullpath := fs.fullpath(filename)
-	f, has := fs.s.Get(fullpath)
+	f, has := fs.s.Get(filename)
 	if !has {
 		if !isCreate(flag) {
 			return nil, os.ErrNotExist
 		}
 
 		var err error
-		f, err = fs.s.New(fullpath, perm, flag)
+		f, err = fs.s.New(filename, perm, flag)
 		if err != nil {
 			return nil, err
 		}
 	} else {
-		if target, isLink := fs.resolveIfLink(fullpath, f); isLink {
+		if target, isLink := fs.resolveLink(filename, f); isLink {
 			return fs.OpenFile(target, flag, perm)
 		}
 	}
@@ -65,19 +60,12 @@
 		return nil, fmt.Errorf("cannot open directory: %s", filename)
 	}
 
-	filename, err := filepath.Rel(fs.base, fullpath)
-	if err != nil {
-		return nil, err
-	}
-
 	return f.Duplicate(filename, perm, flag), nil
 }
 
-func (fs *Memory) fullpath(path string) string {
-	return clean(fs.Join(fs.base, path))
-}
+var errNotLink = errors.New("not a link")
 
-func (fs *Memory) resolveIfLink(fullpath string, f *file) (target string, isLink bool) {
+func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) {
 	if !isSymlink(f.mode) {
 		return fullpath, false
 	}
@@ -87,7 +75,7 @@
 		target = fs.Join(filepath.Dir(fullpath), target)
 	}
 
-	return clean(target), true
+	return target, true
 }
 
 // On Windows OS, IsAbs validates if a path is valid based on if stars with a
@@ -97,18 +85,16 @@
 	return filepath.IsAbs(path) || strings.HasPrefix(path, string(separator))
 }
 
-// Stat returns a billy.FileInfo with the information of the requested file.
-func (fs *Memory) Stat(filename string) (billy.FileInfo, error) {
-	fullpath := fs.fullpath(filename)
-	f, has := fs.s.Get(fullpath)
+func (fs *Memory) Stat(filename string) (os.FileInfo, error) {
+	f, has := fs.s.Get(filename)
 	if !has {
 		return nil, os.ErrNotExist
 	}
 
-	fi := f.Stat()
+	fi, _ := f.Stat()
 
 	var err error
-	if target, isLink := fs.resolveIfLink(fullpath, f); isLink {
+	if target, isLink := fs.resolveLink(filename, f); isLink {
 		fi, err = fs.Stat(target)
 		if err != nil {
 			return nil, err
@@ -122,98 +108,60 @@
 	return fi, nil
 }
 
-func (fs *Memory) Lstat(filename string) (billy.FileInfo, error) {
-	fullpath := fs.fullpath(filename)
-	f, has := fs.s.Get(fullpath)
+func (fs *Memory) Lstat(filename string) (os.FileInfo, error) {
+	f, has := fs.s.Get(filename)
 	if !has {
 		return nil, os.ErrNotExist
 	}
 
-	return f.Stat(), nil
+	return f.Stat()
 }
 
-// ReadDir returns a list of billy.FileInfo in the given directory.
-func (fs *Memory) ReadDir(path string) ([]billy.FileInfo, error) {
-	fullpath := fs.fullpath(path)
-	if f, has := fs.s.Get(fullpath); has {
-		if target, isLink := fs.resolveIfLink(fullpath, f); isLink {
+func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) {
+	if f, has := fs.s.Get(path); has {
+		if target, isLink := fs.resolveLink(path, f); isLink {
 			return fs.ReadDir(target)
 		}
 	}
 
-	var entries []billy.FileInfo
-	for _, f := range fs.s.Children(fullpath) {
-		entries = append(entries, f.Stat())
+	var entries []os.FileInfo
+	for _, f := range fs.s.Children(path) {
+		fi, _ := f.Stat()
+		entries = append(entries, fi)
 	}
 
 	return entries, nil
 }
 
-// MkdirAll creates a directory.
 func (fs *Memory) MkdirAll(path string, perm os.FileMode) error {
-	fullpath := fs.Join(fs.base, path)
-
-	_, err := fs.s.New(fullpath, perm|os.ModeDir, 0)
+	_, err := fs.s.New(path, perm|os.ModeDir, 0)
 	return err
 }
 
-var maxTempFiles = 1024 * 4
-
-// TempFile creates a new temporary file.
 func (fs *Memory) TempFile(dir, prefix string) (billy.File, error) {
-	var fullpath string
-	for {
-		if fs.tempCount >= maxTempFiles {
-			return nil, errors.New("max. number of tempfiles reached")
-		}
-
-		fullpath = fs.getTempFilename(dir, prefix)
-		if _, ok := fs.s.files[fullpath]; !ok {
-			break
-		}
-	}
-
-	return fs.Create(fullpath)
+	return util.TempFile(fs, dir, prefix)
 }
 
 func (fs *Memory) getTempFilename(dir, prefix string) string {
 	fs.tempCount++
 	filename := fmt.Sprintf("%s_%d_%d", prefix, fs.tempCount, time.Now().UnixNano())
-	return fs.Join(fs.base, dir, filename)
+	return fs.Join(dir, filename)
 }
 
-// Rename moves a the `from` file to the `to` file.
 func (fs *Memory) Rename(from, to string) error {
-	from = fs.Join(fs.base, from)
-	to = fs.Join(fs.base, to)
-
 	return fs.s.Rename(from, to)
 }
 
-// Remove deletes a given file from storage.
 func (fs *Memory) Remove(filename string) error {
-	fullpath := fs.Join(fs.base, filename)
-	return fs.s.Remove(fullpath)
+	return fs.s.Remove(filename)
 }
 
-// Join joins any number of path elements into a single path, adding a Separator if necessary.
 func (fs *Memory) Join(elem ...string) string {
 	return filepath.Join(elem...)
 }
 
-// Dir creates a new memory filesystem whose root is the given path inside the current
-// filesystem.
-func (fs *Memory) Dir(path string) billy.Filesystem {
-	return &Memory{
-		base: fs.Join(fs.base, path),
-		s:    fs.s,
-	}
-}
-
 func (fs *Memory) Symlink(target, link string) error {
-	fullpath := clean(fs.Join(fs.base, link))
-
-	_, err := fs.Stat(fullpath)
+	_, err := fs.Stat(link)
 	if err == nil {
 		return os.ErrExist
 	}
@@ -222,13 +170,11 @@
 		return err
 	}
 
-	target = clean(target)
-	return billy.WriteFile(fs, fullpath, []byte(target), 0777|os.ModeSymlink)
+	return util.WriteFile(fs, link, []byte(target), 0777|os.ModeSymlink)
 }
 
 func (fs *Memory) Readlink(link string) (string, error) {
-	fullpath := fs.fullpath(link)
-	f, has := fs.s.Get(fullpath)
+	f, has := fs.s.Get(link)
 	if !has {
 		return "", os.ErrNotExist
 	}
@@ -236,7 +182,7 @@
 	if !isSymlink(f.mode) {
 		return "", &os.PathError{
 			Op:   "readlink",
-			Path: fullpath,
+			Path: link,
 			Err:  fmt.Errorf("not a symlink"),
 		}
 	}
@@ -244,18 +190,18 @@
 	return string(f.content.bytes), nil
 }
 
-// Base returns the base path for the filesystem.
-func (fs *Memory) Base() string {
-	return fs.base
-}
-
 type file struct {
-	billy.BaseFile
-
+	name     string
 	content  *content
 	position int64
 	flag     int
 	mode     os.FileMode
+
+	isClosed bool
+}
+
+func (f *file) Name() string {
+	return f.name
 }
 
 func (f *file) Read(b []byte) (int, error) {
@@ -270,8 +216,8 @@
 }
 
 func (f *file) ReadAt(b []byte, off int64) (int, error) {
-	if f.IsClosed() {
-		return 0, billy.ErrClosed
+	if f.isClosed {
+		return 0, os.ErrClosed
 	}
 
 	if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) {
@@ -284,8 +230,8 @@
 }
 
 func (f *file) Seek(offset int64, whence int) (int64, error) {
-	if f.IsClosed() {
-		return 0, billy.ErrClosed
+	if f.isClosed {
+		return 0, os.ErrClosed
 	}
 
 	switch whence {
@@ -301,8 +247,8 @@
 }
 
 func (f *file) Write(p []byte) (int, error) {
-	if f.IsClosed() {
-		return 0, billy.ErrClosed
+	if f.isClosed {
+		return 0, os.ErrClosed
 	}
 
 	if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) {
@@ -316,20 +262,20 @@
 }
 
 func (f *file) Close() error {
-	if f.IsClosed() {
-		return errors.New("file already closed")
+	if f.isClosed {
+		return os.ErrClosed
 	}
 
-	f.Closed = true
+	f.isClosed = true
 	return nil
 }
 
 func (f *file) Duplicate(filename string, mode os.FileMode, flag int) billy.File {
 	new := &file{
-		BaseFile: billy.BaseFile{BaseFilename: filename},
-		content:  f.content,
-		mode:     mode,
-		flag:     flag,
+		name:    filename,
+		content: f.content,
+		mode:    mode,
+		flag:    flag,
 	}
 
 	if isAppend(flag) {
@@ -343,12 +289,12 @@
 	return new
 }
 
-func (f *file) Stat() billy.FileInfo {
+func (f *file) Stat() (os.FileInfo, error) {
 	return &fileInfo{
-		name: f.Filename(),
+		name: f.Name(),
 		mode: f.mode,
 		size: f.content.Len(),
-	}
+	}, nil
 }
 
 type fileInfo struct {
diff --git a/memfs/memory_test.go b/memfs/memory_test.go
index d527a41..6687459 100644
--- a/memfs/memory_test.go
+++ b/memfs/memory_test.go
@@ -3,8 +3,9 @@
 import (
 	"testing"
 
+	"gopkg.in/src-d/go-billy.v3/test"
+
 	. "gopkg.in/check.v1"
-	"gopkg.in/src-d/go-billy.v2/test"
 )
 
 func Test(t *testing.T) { TestingT(t) }
@@ -17,17 +18,5 @@
 var _ = Suite(&MemorySuite{})
 
 func (s *MemorySuite) SetUpTest(c *C) {
-	s.FilesystemSuite.FS = New()
-}
-
-func (s *MemorySuite) TestTempFileMaxTempFiles(c *C) {
-	for i := 0; i < maxTempFiles; i++ {
-		f, err := s.FilesystemSuite.FS.TempFile("", "")
-		c.Assert(err, IsNil)
-		c.Assert(f, NotNil)
-	}
-
-	f, err := s.FilesystemSuite.FS.TempFile("", "")
-	c.Assert(err, NotNil)
-	c.Assert(f, IsNil)
+	s.FilesystemSuite = test.NewFilesystemSuite(New())
 }
diff --git a/memfs/storage.go b/memfs/storage.go
index 916be11..354c512 100644
--- a/memfs/storage.go
+++ b/memfs/storage.go
@@ -5,8 +5,6 @@
 	"io"
 	"os"
 	"path/filepath"
-
-	"gopkg.in/src-d/go-billy.v2"
 )
 
 type storage struct {
@@ -39,9 +37,7 @@
 	}
 
 	f := &file{
-		BaseFile: billy.BaseFile{
-			BaseFilename: filepath.Base(path),
-		},
+		name:    filepath.Base(path),
 		content: &content{},
 		mode:    mode,
 		flag:    flag,
@@ -55,7 +51,7 @@
 func (s *storage) createParent(path string, mode os.FileMode, f *file) error {
 	base := filepath.Dir(path)
 	base = clean(base)
-	if f.Filename() == string(separator) {
+	if f.Name() == string(separator) {
 		return nil
 	}
 
@@ -67,7 +63,7 @@
 		s.children[base] = make(map[string]*file, 0)
 	}
 
-	s.children[base][f.Filename()] = f
+	s.children[base][f.Name()] = f
 	return nil
 }
 
@@ -136,7 +132,7 @@
 
 func (s *storage) move(from, to string) error {
 	s.files[to] = s.files[from]
-	s.files[to].BaseFilename = filepath.Base(to)
+	s.files[to].name = filepath.Base(to)
 	s.children[to] = s.children[from]
 
 	defer func() {
diff --git a/osfs/os.go b/osfs/os.go
index 9bc9656..62d90a5 100644
--- a/osfs/os.go
+++ b/osfs/os.go
@@ -1,14 +1,13 @@
 // Package osfs provides a billy filesystem for the OS.
-package osfs // import "gopkg.in/src-d/go-billy.v2/osfs"
+package osfs // import "gopkg.in/src-d/go-billy.v3/osfs"
 
 import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
 
-	"strings"
-
-	"gopkg.in/src-d/go-billy.v2"
+	"gopkg.in/src-d/go-billy.v3"
+	"gopkg.in/src-d/go-billy.v3/helper/chroot"
 )
 
 const (
@@ -16,46 +15,26 @@
 	defaultCreateMode    = 0666
 )
 
-// OS is a filesystem based on the os filesystem
-type OS struct {
-	base string
+// OS is a filesystem based on the os filesystem.
+type OS struct{}
+
+// New returns a new OS filesystem.
+func New(baseDir string) billy.Filesystem {
+	return chroot.New(&OS{}, baseDir)
 }
 
-// New returns a new OS filesystem
-func New(baseDir string) *OS {
-	return &OS{
-		base: baseDir,
-	}
-}
-
-// Create creates a file and opens it with standard permissions
-// and modes O_RDWR, O_CREATE and O_TRUNC.
 func (fs *OS) Create(filename string) (billy.File, error) {
 	return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode)
 }
 
-// OpenFile is equivalent to standard os.OpenFile.
-// If flag os.O_CREATE is set, all parent directories will be created.
 func (fs *OS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
-	fullpath := fs.absolutize(filename)
-
 	if flag&os.O_CREATE != 0 {
-		if err := fs.createDir(fullpath); err != nil {
+		if err := fs.createDir(filename); err != nil {
 			return nil, err
 		}
 	}
 
-	f, err := os.OpenFile(fullpath, flag, perm)
-	if err != nil {
-		return nil, err
-	}
-
-	filename, err = filepath.Rel(fs.base, fullpath)
-	if err != nil {
-		return nil, err
-	}
-
-	return newOSFile(filename, f), nil
+	return os.OpenFile(filename, flag, perm)
 }
 
 func (fs *OS) createDir(fullpath string) error {
@@ -69,17 +48,13 @@
 	return nil
 }
 
-// ReadDir returns the filesystem info for all the archives under the specified
-// path.
-func (fs *OS) ReadDir(path string) ([]billy.FileInfo, error) {
-	fullpath := fs.absolutize(path)
-
-	l, err := ioutil.ReadDir(fullpath)
+func (fs *OS) ReadDir(path string) ([]os.FileInfo, error) {
+	l, err := ioutil.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
 
-	var s = make([]billy.FileInfo, len(l))
+	var s = make([]os.FileInfo, len(l))
 	for i, f := range l {
 		s[i] = f
 	}
@@ -87,11 +62,7 @@
 	return s, nil
 }
 
-// Rename moves a file in disk from _from_ to _to_.
 func (fs *OS) Rename(from, to string) error {
-	from = fs.absolutize(from)
-	to = fs.absolutize(to)
-
 	if err := fs.createDir(to); err != nil {
 		return err
 	}
@@ -99,86 +70,39 @@
 	return os.Rename(from, to)
 }
 
-// MkdirAll creates a directory.
 func (fs *OS) MkdirAll(path string, perm os.FileMode) error {
-	fullpath := fs.absolutize(path)
-	return os.MkdirAll(fullpath, defaultDirectoryMode)
+	return os.MkdirAll(path, defaultDirectoryMode)
 }
 
-// Open opens a file in read-only mode.
 func (fs *OS) Open(filename string) (billy.File, error) {
 	return fs.OpenFile(filename, os.O_RDONLY, 0)
 }
 
-// Remove deletes a file in disk.
 func (fs *OS) Remove(filename string) error {
-	fullpath := fs.absolutize(filename)
-	return os.Remove(fullpath)
+	return os.Remove(filename)
 }
 
-// TempFile creates a new temporal file.
 func (fs *OS) TempFile(dir, prefix string) (billy.File, error) {
-	fullpath := fs.absolutize(dir)
-	if err := fs.createDir(fullpath + string(os.PathSeparator)); err != nil {
+	if err := fs.createDir(dir + string(os.PathSeparator)); err != nil {
 		return nil, err
 	}
 
-	f, err := ioutil.TempFile(fullpath, prefix)
-	if err != nil {
-		return nil, err
-	}
-
-	s, err := f.Stat()
-	if err != nil {
-		return nil, err
-	}
-
-	filename, err := filepath.Rel(fs.base, fs.Join(fullpath, s.Name()))
-	if err != nil {
-		return nil, err
-	}
-
-	return newOSFile(filename, f), nil
+	return ioutil.TempFile(dir, prefix)
 }
 
-// Join joins the specified elements using the filesystem separator.
 func (fs *OS) Join(elem ...string) string {
 	return filepath.Join(elem...)
 }
 
-// Dir returns a new Filesystem from the same type of fs using as baseDir the
-// given path
-func (fs *OS) Dir(path string) billy.Filesystem {
-	return New(fs.absolutize(path))
-}
-
-// Base returns the base path of the filesytem
-func (fs *OS) Base() string {
-	return fs.base
-}
-
-// RemoveAll removes a file or directory recursively. Removes everything it can,
-// but returns the first error.
 func (fs *OS) RemoveAll(path string) error {
-	fullpath := fs.Join(fs.base, path)
-	return os.RemoveAll(fullpath)
+	return os.RemoveAll(filepath.Clean(path))
 }
 
-func (fs *OS) Lstat(filename string) (billy.FileInfo, error) {
-	fullpath := fs.Join(fs.base, filename)
-	return os.Lstat(fullpath)
+func (fs *OS) Lstat(filename string) (os.FileInfo, error) {
+	return os.Lstat(filepath.Clean(filename))
 }
 
-// Symlink imlements billy.Symlinker.Symlink.
 func (fs *OS) Symlink(target, link string) error {
-	target = filepath.FromSlash(target)
-
-	// only rewrite target if it's already absolute
-	if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
-		target = fs.absolutize(target)
-	}
-
-	link = fs.absolutize(link)
 	if err := fs.createDir(link); err != nil {
 		return err
 	}
@@ -186,64 +110,6 @@
 	return os.Symlink(target, link)
 }
 
-// Readlink implements billy.Symlinker.Readlink.
 func (fs *OS) Readlink(link string) (string, error) {
-	fullpath := fs.Join(fs.base, link)
-	target, err := os.Readlink(fullpath)
-	if err != nil {
-		return "", err
-	}
-
-	if !filepath.IsAbs(target) && !strings.HasPrefix(target, string(filepath.Separator)) {
-		return target, nil
-	}
-
-	target, err = filepath.Rel(fs.base, target)
-	if err != nil {
-		return "", err
-	}
-
-	return string(os.PathSeparator) + target, nil
-}
-
-// osFile represents a file in the os filesystem
-type osFile struct {
-	billy.BaseFile
-	file *os.File
-}
-
-func newOSFile(filename string, file *os.File) billy.File {
-	return &osFile{
-		BaseFile: billy.BaseFile{BaseFilename: filename},
-		file:     file,
-	}
-}
-
-func (f *osFile) Read(p []byte) (int, error) {
-	return f.file.Read(p)
-}
-
-func (f *osFile) Seek(offset int64, whence int) (int64, error) {
-	return f.file.Seek(offset, whence)
-}
-
-func (f *osFile) Write(p []byte) (int, error) {
-	return f.file.Write(p)
-}
-
-func (f *osFile) Close() error {
-	f.BaseFile.Closed = true
-
-	return f.file.Close()
-}
-
-func (f *osFile) ReadAt(p []byte, off int64) (int, error) {
-	return f.file.ReadAt(p, off)
-}
-
-func (fs *OS) absolutize(relpath string) string {
-	fullpath := filepath.FromSlash(filepath.ToSlash(relpath))
-
-	fullpath = fs.Join(fs.base, fullpath)
-	return filepath.Clean(fullpath)
+	return os.Readlink(link)
 }
diff --git a/osfs/os_posix.go b/osfs/os_posix.go
index 9f23b02..2cee95e 100644
--- a/osfs/os_posix.go
+++ b/osfs/os_posix.go
@@ -4,12 +4,9 @@
 
 import (
 	"os"
-
-	"gopkg.in/src-d/go-billy.v2"
 )
 
 // Stat returns the FileInfo structure describing file.
-func (fs *OS) Stat(filename string) (billy.FileInfo, error) {
-	fullpath := fs.absolutize(filename)
-	return os.Stat(fullpath)
+func (fs *OS) Stat(filename string) (os.FileInfo, error) {
+	return os.Stat(filename)
 }
diff --git a/osfs/os_test.go b/osfs/os_test.go
index 767dc74..ad3d445 100644
--- a/osfs/os_test.go
+++ b/osfs/os_test.go
@@ -7,7 +7,7 @@
 	"testing"
 
 	. "gopkg.in/check.v1"
-	"gopkg.in/src-d/go-billy.v2/test"
+	"gopkg.in/src-d/go-billy.v3/test"
 )
 
 func Test(t *testing.T) { TestingT(t) }
@@ -21,7 +21,7 @@
 
 func (s *OSSuite) SetUpTest(c *C) {
 	s.path, _ = ioutil.TempDir(os.TempDir(), "go-billy-osfs-test")
-	s.FilesystemSuite.FS = New(s.path)
+	s.FilesystemSuite = test.NewFilesystemSuite(New(s.path))
 }
 
 func (s *OSSuite) TearDownTest(c *C) {
diff --git a/osfs/os_windows.go b/osfs/os_windows.go
index 18beabe..333f72e 100644
--- a/osfs/os_windows.go
+++ b/osfs/os_windows.go
@@ -6,23 +6,18 @@
 	"os"
 	"path/filepath"
 	"strings"
-
-	"gopkg.in/src-d/go-billy.v2"
 )
 
 // Stat returns the FileInfo structure describing file.
-func (fs *OS) Stat(filename string) (billy.FileInfo, error) {
+func (fs *OS) Stat(filename string) (os.FileInfo, error) {
 	// TODO: remove this in Go 1.9
-
-	fullpath := fs.absolutize(filename)
-
 	target, err := fs.Readlink(filename)
 	if err != nil {
-		return os.Stat(fullpath)
+		return os.Stat(filename)
 	}
 
 	if !filepath.IsAbs(target) && !strings.HasPrefix(target, string(filepath.Separator)) {
-		target, _ = filepath.Rel(fs.base, fs.Join(filepath.Dir(fullpath), target))
+		target = fs.Join(filepath.Dir(filename), target)
 	}
 
 	fi, err := fs.Stat(target)
@@ -32,7 +27,7 @@
 
 	return &fileInfo{
 		FileInfo: fi,
-		name:     filepath.Base(fullpath),
+		name:     filepath.Base(filename),
 	}, nil
 }
 
diff --git a/subdirfs/file.go b/subdirfs/file.go
deleted file mode 100644
index cd6f38d..0000000
--- a/subdirfs/file.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package subdirfs
-
-import (
-	"io"
-	"path/filepath"
-
-	"gopkg.in/src-d/go-billy.v2"
-)
-
-type file struct {
-	billy.BaseFile
-
-	f billy.File
-}
-
-func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
-	filename = fs.Join(fs.Base(), filename)
-	filename, _ = filepath.Rel(fs.Base(), filename)
-
-	return &file{
-		BaseFile: billy.BaseFile{BaseFilename: filename},
-		f:        f,
-	}
-}
-
-func (f *file) Read(p []byte) (int, error) {
-	return f.f.Read(p)
-}
-
-func (f *file) ReadAt(b []byte, off int64) (int, error) {
-	rf, ok := f.f.(io.ReaderAt)
-	if !ok {
-		return 0, billy.ErrNotSupported
-	}
-
-	return rf.ReadAt(b, off)
-}
-
-func (f *file) Seek(offset int64, whence int) (int64, error) {
-	return f.f.Seek(offset, whence)
-}
-
-func (f *file) Write(p []byte) (int, error) {
-	return f.f.Write(p)
-}
-
-func (f *file) Close() error {
-	defer func() { f.Closed = true }()
-	return f.f.Close()
-}
diff --git a/subdirfs/fileinfo.go b/subdirfs/fileinfo.go
deleted file mode 100644
index 4ede637..0000000
--- a/subdirfs/fileinfo.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package subdirfs
-
-import (
-	"os"
-	"time"
-
-	"gopkg.in/src-d/go-billy.v2"
-)
-
-type fileInfo struct {
-	filename string
-	fi       billy.FileInfo
-}
-
-func newFileInfo(filename string, fi billy.FileInfo) billy.FileInfo {
-	return &fileInfo{filename, fi}
-}
-
-func (fi *fileInfo) Name() string {
-	return fi.filename
-}
-func (fi *fileInfo) Size() int64 {
-	return fi.fi.Size()
-}
-
-func (fi *fileInfo) Mode() os.FileMode {
-	return fi.fi.Mode()
-}
-
-func (fi *fileInfo) ModTime() time.Time {
-	return fi.fi.ModTime()
-}
-
-func (fi *fileInfo) IsDir() bool {
-	return fi.fi.IsDir()
-}
-
-func (fi *fileInfo) Sys() interface{} {
-	return fi.fi.Sys()
-}
diff --git a/subdirfs/subdir.go b/subdirfs/subdir.go
deleted file mode 100644
index 9bcdfcb..0000000
--- a/subdirfs/subdir.go
+++ /dev/null
@@ -1,164 +0,0 @@
-package subdirfs
-
-import (
-	"errors"
-	"os"
-	"path/filepath"
-	"strings"
-
-	"gopkg.in/src-d/go-billy.v2"
-)
-
-// ErrSymlinkNotSupported is returned by Symlink() and Readfile() if the
-// underlying filesystem does not support symlinking.
-var ErrSymlinkNotSupported = errors.New("symlink not supported")
-
-type subdirFs struct {
-	underlying billy.Filesystem
-	base       string
-}
-
-// New creates a new filesystem wrapping up the given 'fs'.
-// The created filesystem has its base in the given subdirectory
-// of the underlying filesystem.
-//
-// This is particularly useful to implement the Dir method for
-// other filesystems.
-func New(fs billy.Filesystem, base string) billy.Filesystem {
-	return &subdirFs{fs, base}
-}
-
-func (s *subdirFs) underlyingPath(filename string) string {
-	return s.Join(s.Base(), filename)
-}
-
-func (s *subdirFs) Create(filename string) (billy.File, error) {
-	f, err := s.underlying.Create(s.underlyingPath(filename))
-	if err != nil {
-		return nil, err
-	}
-
-	return newFile(s, f, filename), nil
-}
-
-func (s *subdirFs) Open(filename string) (billy.File, error) {
-	f, err := s.underlying.Open(s.underlyingPath(filename))
-	if err != nil {
-		return nil, err
-	}
-
-	return newFile(s, f, filename), nil
-}
-
-func (s *subdirFs) OpenFile(filename string, flag int, mode os.FileMode) (
-	billy.File, error) {
-
-	f, err := s.underlying.OpenFile(s.underlyingPath(filename), flag, mode)
-	if err != nil {
-		return nil, err
-	}
-
-	return newFile(s, f, filename), nil
-}
-
-func (s *subdirFs) TempFile(dir, prefix string) (billy.File, error) {
-	f, err := s.underlying.TempFile(s.underlyingPath(dir), prefix)
-	if err != nil {
-		return nil, err
-	}
-
-	return newFile(s, f, s.Join(dir, filepath.Base(f.Filename()))), nil
-}
-
-func (s *subdirFs) Rename(from, to string) error {
-	return s.underlying.Rename(s.underlyingPath(from), s.underlyingPath(to))
-}
-
-func (s *subdirFs) Remove(path string) error {
-	return s.underlying.Remove(s.underlyingPath(path))
-}
-
-func (s *subdirFs) MkdirAll(filename string, perm os.FileMode) error {
-	fullpath := s.Join(s.base, filename)
-	return s.underlying.MkdirAll(fullpath, perm)
-}
-
-func (s *subdirFs) Stat(filename string) (billy.FileInfo, error) {
-	fullpath := s.underlyingPath(filename)
-	fi, err := s.underlying.Stat(fullpath)
-	if err != nil {
-		return nil, err
-	}
-
-	return newFileInfo(filepath.Base(fullpath), fi), nil
-}
-
-func (s *subdirFs) Lstat(filename string) (billy.FileInfo, error) {
-	fullpath := s.underlyingPath(filename)
-	fi, err := s.underlying.Lstat(fullpath)
-	if err != nil {
-		return nil, err
-	}
-
-	return newFileInfo(filepath.Base(fullpath), fi), nil
-}
-
-func (s *subdirFs) ReadDir(path string) ([]billy.FileInfo, error) {
-	prefix := s.underlyingPath(path)
-	fis, err := s.underlying.ReadDir(prefix)
-	if err != nil {
-		return nil, err
-	}
-	for i := 0; i < len(fis); i++ {
-		rn := strings.Replace(fis[i].Name(), prefix, "", 1)
-		fis[i] = newFileInfo(rn, fis[i])
-	}
-
-	return fis, nil
-}
-
-func (s *subdirFs) Join(elem ...string) string {
-	return s.underlying.Join(elem...)
-}
-
-func (s *subdirFs) Dir(path string) billy.Filesystem {
-	return New(s.underlying, s.underlyingPath(path))
-}
-
-func (s *subdirFs) Base() string {
-	return s.base
-}
-
-// Symlink implements billy.Symlinker.Symlink.
-func (s *subdirFs) Symlink(target, link string) error {
-	target = filepath.FromSlash(target)
-
-	// only rewrite target if it's already absolute
-	if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
-		target = string(os.PathSeparator) + s.underlyingPath(target)
-	}
-
-	link = s.underlyingPath(link)
-	return s.underlying.Symlink(target, link)
-}
-
-// Readlink implements billy.Symlinker.Readlink.
-func (s *subdirFs) Readlink(link string) (string, error) {
-	fullpath := s.underlyingPath(link)
-	target, err := s.underlying.Readlink(fullpath)
-	if err != nil {
-		return "", err
-	}
-
-	if !filepath.IsAbs(target) && !strings.HasPrefix(target, string(filepath.Separator)) {
-		return target, nil
-	}
-
-	base := string(os.PathSeparator) + s.base
-	target, err = filepath.Rel(base, target)
-	if err != nil {
-		return "", err
-	}
-
-	return string(os.PathSeparator) + target, nil
-}
diff --git a/subdirfs/subdir_test.go b/subdirfs/subdir_test.go
deleted file mode 100644
index 6158973..0000000
--- a/subdirfs/subdir_test.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package subdirfs
-
-import (
-	"io/ioutil"
-	stdos "os"
-	"testing"
-
-	"gopkg.in/src-d/go-billy.v2"
-	"gopkg.in/src-d/go-billy.v2/osfs"
-	"gopkg.in/src-d/go-billy.v2/test"
-
-	. "gopkg.in/check.v1"
-)
-
-func Test(t *testing.T) { TestingT(t) }
-
-type FilesystemSuite struct {
-	test.FilesystemSuite
-	cfs  billy.Filesystem
-	path string
-}
-
-var _ = Suite(&FilesystemSuite{})
-
-func (s *FilesystemSuite) SetUpTest(c *C) {
-	s.path, _ = ioutil.TempDir(stdos.TempDir(), "go-billy-subdirfs-test")
-	fs := osfs.New(s.path)
-
-	s.cfs = New(fs, "test-subdir")
-	s.FilesystemSuite.FS = s.cfs
-}
-
-func (s *FilesystemSuite) TearDownTest(c *C) {
-	fi, err := ioutil.ReadDir(s.path)
-	c.Assert(err, IsNil)
-	c.Assert(len(fi) <= 1, Equals, true)
-
-	err = stdos.RemoveAll(s.path)
-	c.Assert(err, IsNil)
-}
diff --git a/test/basic.go b/test/basic.go
new file mode 100644
index 0000000..1a0b264
--- /dev/null
+++ b/test/basic.go
@@ -0,0 +1,564 @@
+package test
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	. "gopkg.in/check.v1"
+	. "gopkg.in/src-d/go-billy.v3"
+	"gopkg.in/src-d/go-billy.v3/util"
+)
+
+// BasicSuite is a convenient test suite to validate any implementation of
+// billy.Basic
+type BasicSuite struct {
+	FS Basic
+}
+
+func (s *BasicSuite) TestCreate(c *C) {
+	f, err := s.FS.Create("foo")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestCreateDepth(c *C) {
+	f, err := s.FS.Create("bar/foo")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, s.FS.Join("bar", "foo"))
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestCreateDepthAbsolute(c *C) {
+	f, err := s.FS.Create("/bar/foo")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, s.FS.Join("bar", "foo"))
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestCreateOverwrite(c *C) {
+	for i := 0; i < 3; i++ {
+		f, err := s.FS.Create("foo")
+		c.Assert(err, IsNil)
+
+		l, err := f.Write([]byte(fmt.Sprintf("foo%d", i)))
+		c.Assert(err, IsNil)
+		c.Assert(l, Equals, 4)
+
+		err = f.Close()
+		c.Assert(err, IsNil)
+	}
+
+	f, err := s.FS.Open("foo")
+	c.Assert(err, IsNil)
+
+	wrote, err := ioutil.ReadAll(f)
+	c.Assert(err, IsNil)
+	c.Assert(string(wrote), DeepEquals, "foo2")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestCreateAndClose(c *C) {
+	f, err := s.FS.Create("foo")
+	c.Assert(err, IsNil)
+
+	_, err = f.Write([]byte("foo"))
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+
+	f, err = s.FS.Open(f.Name())
+	c.Assert(err, IsNil)
+
+	wrote, err := ioutil.ReadAll(f)
+	c.Assert(err, IsNil)
+	c.Assert(string(wrote), DeepEquals, "foo")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestOpen(c *C) {
+	f, err := s.FS.Create("foo")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo")
+	c.Assert(f.Close(), IsNil)
+
+	f, err = s.FS.Open("foo")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestOpenNotExists(c *C) {
+	f, err := s.FS.Open("not-exists")
+	c.Assert(err, NotNil)
+	c.Assert(f, IsNil)
+}
+
+func (s *BasicSuite) TestOpenFile(c *C) {
+	defaultMode := os.FileMode(0666)
+
+	f, err := s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, defaultMode)
+	c.Assert(err, IsNil)
+	s.testWriteClose(c, f, "foo1")
+
+	// Truncate if it exists
+	f, err = s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, defaultMode)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo1")
+	s.testWriteClose(c, f, "foo1overwritten")
+
+	// Read-only if it exists
+	f, err = s.FS.OpenFile("foo1", os.O_RDONLY, defaultMode)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo1")
+	s.testReadClose(c, f, "foo1overwritten")
+
+	// Create when it does exist
+	f, err = s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, defaultMode)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo1")
+	s.testWriteClose(c, f, "bar")
+
+	f, err = s.FS.OpenFile("foo1", os.O_RDONLY, defaultMode)
+	c.Assert(err, IsNil)
+	s.testReadClose(c, f, "bar")
+}
+
+func (s *BasicSuite) TestOpenFileNoTruncate(c *C) {
+	defaultMode := os.FileMode(0666)
+
+	// Create when it does not exist
+	f, err := s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY, defaultMode)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo1")
+	s.testWriteClose(c, f, "foo1")
+
+	f, err = s.FS.OpenFile("foo1", os.O_RDONLY, defaultMode)
+	c.Assert(err, IsNil)
+	s.testReadClose(c, f, "foo1")
+
+	// Create when it does exist
+	f, err = s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY, defaultMode)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo1")
+	s.testWriteClose(c, f, "bar")
+
+	f, err = s.FS.OpenFile("foo1", os.O_RDONLY, defaultMode)
+	c.Assert(err, IsNil)
+	s.testReadClose(c, f, "bar1")
+}
+
+func (s *BasicSuite) TestOpenFileAppend(c *C) {
+	defaultMode := os.FileMode(0666)
+
+	f, err := s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY|os.O_APPEND, defaultMode)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo1")
+	s.testWriteClose(c, f, "foo1")
+
+	f, err = s.FS.OpenFile("foo1", os.O_WRONLY|os.O_APPEND, defaultMode)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo1")
+	s.testWriteClose(c, f, "bar1")
+
+	f, err = s.FS.OpenFile("foo1", os.O_RDONLY, defaultMode)
+	c.Assert(err, IsNil)
+	s.testReadClose(c, f, "foo1bar1")
+}
+
+func (s *BasicSuite) TestOpenFileReadWrite(c *C) {
+	defaultMode := os.FileMode(0666)
+
+	f, err := s.FS.OpenFile("foo1", os.O_CREATE|os.O_TRUNC|os.O_RDWR, defaultMode)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo1")
+
+	written, err := f.Write([]byte("foobar"))
+	c.Assert(written, Equals, 6)
+	c.Assert(err, IsNil)
+
+	_, err = f.Seek(0, os.SEEK_SET)
+	c.Assert(err, IsNil)
+
+	written, err = f.Write([]byte("qux"))
+	c.Assert(written, Equals, 3)
+	c.Assert(err, IsNil)
+
+	_, err = f.Seek(0, os.SEEK_SET)
+	c.Assert(err, IsNil)
+
+	s.testReadClose(c, f, "quxbar")
+}
+
+func (s *BasicSuite) TestOpenFileWithModes(c *C) {
+	f, err := s.FS.OpenFile("foo", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, customMode)
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+
+	fi, err := s.FS.Stat("foo")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Mode(), Equals, os.FileMode(customMode))
+}
+
+func (s *BasicSuite) testWriteClose(c *C, f File, content string) {
+	written, err := f.Write([]byte(content))
+	c.Assert(written, Equals, len(content))
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) testReadClose(c *C, f File, content string) {
+	read, err := ioutil.ReadAll(f)
+	c.Assert(err, IsNil)
+	c.Assert(string(read), Equals, content)
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestFileWrite(c *C) {
+	f, err := s.FS.Create("foo")
+	c.Assert(err, IsNil)
+
+	n, err := f.Write([]byte("foo"))
+	c.Assert(err, IsNil)
+	c.Assert(n, Equals, 3)
+
+	f.Seek(0, io.SeekStart)
+	all, err := ioutil.ReadAll(f)
+	c.Assert(err, IsNil)
+	c.Assert(string(all), Equals, "foo")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestFileWriteClose(c *C) {
+	f, err := s.FS.Create("foo")
+	c.Assert(err, IsNil)
+
+	c.Assert(f.Close(), IsNil)
+
+	_, err = f.Write([]byte("foo"))
+	c.Assert(err, NotNil)
+}
+
+func (s *BasicSuite) TestFileRead(c *C) {
+	err := util.WriteFile(s.FS, "foo", []byte("foo"), 0644)
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Open("foo")
+	c.Assert(err, IsNil)
+
+	all, err := ioutil.ReadAll(f)
+	c.Assert(err, IsNil)
+	c.Assert(string(all), Equals, "foo")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestFileClosed(c *C) {
+	err := util.WriteFile(s.FS, "foo", []byte("foo"), 0644)
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Open("foo")
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+
+	_, err = ioutil.ReadAll(f)
+	c.Assert(err, NotNil)
+}
+
+func (s *BasicSuite) TestFileNonRead(c *C) {
+	err := util.WriteFile(s.FS, "foo", []byte("foo"), 0644)
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.OpenFile("foo", os.O_WRONLY, 0)
+	c.Assert(err, IsNil)
+
+	_, err = ioutil.ReadAll(f)
+	c.Assert(err, NotNil)
+
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestFileSeekstart(c *C) {
+	s.testFileSeek(c, 10, io.SeekStart)
+}
+
+func (s *BasicSuite) TestFileSeekCurrent(c *C) {
+	s.testFileSeek(c, 5, io.SeekCurrent)
+}
+
+func (s *BasicSuite) TestFileSeekEnd(c *C) {
+	s.testFileSeek(c, -26, io.SeekEnd)
+}
+
+func (s *BasicSuite) testFileSeek(c *C, offset int64, whence int) {
+	err := util.WriteFile(s.FS, "foo", []byte("0123456789abcdefghijklmnopqrstuvwxyz"), 0644)
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Open("foo")
+	c.Assert(err, IsNil)
+
+	some := make([]byte, 5)
+	_, err = f.Read(some)
+	c.Assert(err, IsNil)
+	c.Assert(string(some), Equals, "01234")
+
+	p, err := f.Seek(offset, whence)
+	c.Assert(err, IsNil)
+	c.Assert(int(p), Equals, 10)
+
+	all, err := ioutil.ReadAll(f)
+	c.Assert(err, IsNil)
+	c.Assert(all, HasLen, 26)
+	c.Assert(string(all), Equals, "abcdefghijklmnopqrstuvwxyz")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestSeekToEndAndWrite(c *C) {
+	defaultMode := os.FileMode(0666)
+
+	f, err := s.FS.OpenFile("foo1", os.O_CREATE|os.O_TRUNC|os.O_RDWR, defaultMode)
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "foo1")
+
+	_, err = f.Seek(10, io.SeekEnd)
+	c.Assert(err, IsNil)
+
+	n, err := f.Write([]byte(`TEST`))
+	c.Assert(err, IsNil)
+	c.Assert(n, Equals, 4)
+
+	_, err = f.Seek(0, io.SeekStart)
+	c.Assert(err, IsNil)
+
+	s.testReadClose(c, f, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TEST")
+}
+
+func (s *BasicSuite) TestFileSeekClosed(c *C) {
+	err := util.WriteFile(s.FS, "foo", []byte("foo"), 0644)
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Open("foo")
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+
+	_, err = f.Seek(0, 0)
+	c.Assert(err, NotNil)
+}
+
+func (s *BasicSuite) TestFileCloseTwice(c *C) {
+	f, err := s.FS.Create("foo")
+	c.Assert(err, IsNil)
+
+	c.Assert(f.Close(), IsNil)
+	c.Assert(f.Close(), NotNil)
+}
+
+func (s *BasicSuite) TestStat(c *C) {
+	util.WriteFile(s.FS, "foo/bar", []byte("foo"), customMode)
+
+	fi, err := s.FS.Stat("foo/bar")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "bar")
+	c.Assert(fi.Size(), Equals, int64(3))
+	c.Assert(fi.Mode(), Equals, customMode)
+	c.Assert(fi.ModTime().IsZero(), Equals, false)
+	c.Assert(fi.IsDir(), Equals, false)
+}
+
+func (s *BasicSuite) TestStatNonExistent(c *C) {
+	fi, err := s.FS.Stat("non-existent")
+	comment := Commentf("error: %s", err)
+	c.Assert(os.IsNotExist(err), Equals, true, comment)
+	c.Assert(fi, IsNil)
+}
+
+func (s *BasicSuite) TestRename(c *C) {
+	err := util.WriteFile(s.FS, "foo", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Rename("foo", "bar")
+	c.Assert(err, IsNil)
+
+	foo, err := s.FS.Stat("foo")
+	c.Assert(foo, IsNil)
+	c.Assert(os.IsNotExist(err), Equals, true)
+
+	bar, err := s.FS.Stat("bar")
+	c.Assert(err, IsNil)
+	c.Assert(bar, NotNil)
+}
+
+func (s *BasicSuite) TestOpenAndWrite(c *C) {
+	err := util.WriteFile(s.FS, "foo", nil, 0644)
+	c.Assert(err, IsNil)
+
+	foo, err := s.FS.Open("foo")
+	c.Assert(foo, NotNil)
+	c.Assert(err, IsNil)
+
+	n, err := foo.Write([]byte("foo"))
+	c.Assert(err, NotNil)
+	c.Assert(n, Equals, 0)
+
+	c.Assert(foo.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestOpenAndStat(c *C) {
+	err := util.WriteFile(s.FS, "foo", []byte("foo"), 0644)
+	c.Assert(err, IsNil)
+
+	foo, err := s.FS.Open("foo")
+	c.Assert(foo, NotNil)
+	c.Assert(foo.Name(), Equals, "foo")
+	c.Assert(err, IsNil)
+	c.Assert(foo.Close(), IsNil)
+
+	stat, err := s.FS.Stat("foo")
+	c.Assert(stat, NotNil)
+	c.Assert(err, IsNil)
+	c.Assert(stat.Name(), Equals, "foo")
+	c.Assert(stat.Size(), Equals, int64(3))
+}
+
+func (s *BasicSuite) TestRemove(c *C) {
+	f, err := s.FS.Create("foo")
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+
+	err = s.FS.Remove("foo")
+	c.Assert(err, IsNil)
+}
+
+func (s *BasicSuite) TestRemoveNonExisting(c *C) {
+	err := s.FS.Remove("NON-EXISTING")
+	c.Assert(err, NotNil)
+	c.Assert(os.IsNotExist(err), Equals, true)
+}
+
+func (s *BasicSuite) TestRemoveNotEmptyDir(c *C) {
+	err := util.WriteFile(s.FS, "foo", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Remove("no-exists")
+	c.Assert(err, NotNil)
+}
+
+func (s *BasicSuite) TestJoin(c *C) {
+	c.Assert(s.FS.Join("foo", "bar"), Equals, fmt.Sprintf("foo%cbar", filepath.Separator))
+}
+
+func (s *BasicSuite) TestReadAtOnReadWrite(c *C) {
+	f, err := s.FS.Create("foo")
+	c.Assert(err, IsNil)
+	_, err = f.Write([]byte("abcdefg"))
+	c.Assert(err, IsNil)
+
+	rf, ok := f.(io.ReaderAt)
+	c.Assert(ok, Equals, true)
+
+	b := make([]byte, 3)
+	n, err := rf.ReadAt(b, 2)
+	c.Assert(err, IsNil)
+	c.Assert(n, Equals, 3)
+	c.Assert(string(b), Equals, "cde")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestReadAtOnReadOnly(c *C) {
+	err := util.WriteFile(s.FS, "foo", []byte("abcdefg"), 0644)
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Open("foo")
+	c.Assert(err, IsNil)
+
+	rf, ok := f.(io.ReaderAt)
+	c.Assert(ok, Equals, true)
+
+	b := make([]byte, 3)
+	n, err := rf.ReadAt(b, 2)
+	c.Assert(err, IsNil)
+	c.Assert(n, Equals, 3)
+	c.Assert(string(b), Equals, "cde")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestReadAtEOF(c *C) {
+	err := util.WriteFile(s.FS, "foo", []byte("TEST"), 0644)
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Open("foo")
+	c.Assert(err, IsNil)
+
+	b := make([]byte, 5)
+	n, err := f.ReadAt(b, 0)
+	c.Assert(err, Equals, io.EOF)
+	c.Assert(n, Equals, 4)
+	c.Assert(string(b), Equals, "TEST\x00")
+
+	err = f.Close()
+	c.Assert(err, IsNil)
+}
+
+func (s *BasicSuite) TestReadAtOffset(c *C) {
+	err := util.WriteFile(s.FS, "foo", []byte("TEST"), 0644)
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Open("foo")
+	c.Assert(err, IsNil)
+
+	rf, ok := f.(io.ReaderAt)
+	c.Assert(ok, Equals, true)
+
+	o, err := f.Seek(0, io.SeekCurrent)
+	c.Assert(err, IsNil)
+	c.Assert(o, Equals, int64(0))
+
+	b := make([]byte, 4)
+	n, err := rf.ReadAt(b, 0)
+	c.Assert(err, IsNil)
+	c.Assert(n, Equals, 4)
+	c.Assert(string(b), Equals, "TEST")
+
+	o, err = f.Seek(0, io.SeekCurrent)
+	c.Assert(err, IsNil)
+	c.Assert(o, Equals, int64(0))
+
+	err = f.Close()
+	c.Assert(err, IsNil)
+}
+
+func (s *BasicSuite) TestReadWriteLargeFile(c *C) {
+	f, err := s.FS.Create("foo")
+	c.Assert(err, IsNil)
+
+	size := 1 << 20
+
+	n, err := f.Write(bytes.Repeat([]byte("F"), size))
+	c.Assert(err, IsNil)
+	c.Assert(n, Equals, size)
+
+	c.Assert(f.Close(), IsNil)
+
+	f, err = s.FS.Open("foo")
+	c.Assert(err, IsNil)
+	b, err := ioutil.ReadAll(f)
+	c.Assert(err, IsNil)
+	c.Assert(len(b), Equals, size)
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *BasicSuite) TestWriteFile(c *C) {
+	err := util.WriteFile(s.FS, "foo", []byte("bar"), 0777)
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Open("foo")
+	c.Assert(err, IsNil)
+
+	wrote, err := ioutil.ReadAll(f)
+	c.Assert(err, IsNil)
+	c.Assert(string(wrote), DeepEquals, "bar")
+
+	c.Assert(f.Close(), IsNil)
+}
diff --git a/test/chroot.go b/test/chroot.go
new file mode 100644
index 0000000..70bfac6
--- /dev/null
+++ b/test/chroot.go
@@ -0,0 +1,123 @@
+package test
+
+import (
+	"os"
+
+	. "gopkg.in/check.v1"
+	. "gopkg.in/src-d/go-billy.v3"
+	"gopkg.in/src-d/go-billy.v3/util"
+)
+
+// ChrootSuite is a convenient test suite to validate any implementation of
+// billy.Chroot
+type ChrootSuite struct {
+	FS interface {
+		Basic
+		Chroot
+	}
+}
+
+func (s *ChrootSuite) TestCreateWithChroot(c *C) {
+	fs, _ := s.FS.Chroot("foo")
+	f, err := fs.Create("bar")
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+	c.Assert(f.Name(), Equals, "bar")
+
+	f, err = s.FS.Open("foo/bar")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, s.FS.Join("foo", "bar"))
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *ChrootSuite) TestOpenWithChroot(c *C) {
+	fs, _ := s.FS.Chroot("foo")
+	f, err := fs.Create("bar")
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+	c.Assert(f.Name(), Equals, "bar")
+
+	f, err = fs.Open("bar")
+	c.Assert(err, IsNil)
+	c.Assert(f.Name(), Equals, "bar")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *ChrootSuite) TestOpenOutOffBoundary(c *C) {
+	err := util.WriteFile(s.FS, "bar", nil, 0644)
+	c.Assert(err, IsNil)
+
+	fs, _ := s.FS.Chroot("foo")
+	f, err := fs.Open("../bar")
+	c.Assert(err, Equals, ErrCrossedBoundary)
+	c.Assert(f, IsNil)
+}
+
+func (s *ChrootSuite) TestStatOutOffBoundary(c *C) {
+	err := util.WriteFile(s.FS, "bar", nil, 0644)
+	c.Assert(err, IsNil)
+
+	fs, _ := s.FS.Chroot("foo")
+	f, err := fs.Stat("../bar")
+	c.Assert(err, Equals, ErrCrossedBoundary)
+	c.Assert(f, IsNil)
+}
+
+func (s *ChrootSuite) TestStatWithChroot(c *C) {
+	files := []string{"foo", "bar", "qux/baz", "qux/qux"}
+	for _, name := range files {
+		err := util.WriteFile(s.FS, name, nil, 0644)
+		c.Assert(err, IsNil)
+	}
+
+	// Some implementations detect directories based on a prefix
+	// for all files; it's easy to miss path separator handling there.
+	fi, err := s.FS.Stat("qu")
+	c.Assert(os.IsNotExist(err), Equals, true, Commentf("error: %s", err))
+	c.Assert(fi, IsNil)
+
+	fi, err = s.FS.Stat("qux")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "qux")
+	c.Assert(fi.IsDir(), Equals, true)
+
+	qux, _ := s.FS.Chroot("qux")
+
+	fi, err = qux.Stat("baz")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "baz")
+	c.Assert(fi.IsDir(), Equals, false)
+
+	fi, err = qux.Stat("/baz")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "baz")
+	c.Assert(fi.IsDir(), Equals, false)
+}
+
+func (s *ChrootSuite) TestRenameOutOffBoundary(c *C) {
+	err := util.WriteFile(s.FS, "foo/foo", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = util.WriteFile(s.FS, "bar", nil, 0644)
+	c.Assert(err, IsNil)
+
+	fs, _ := s.FS.Chroot("foo")
+	err = fs.Rename("../bar", "foo")
+	c.Assert(err, Equals, ErrCrossedBoundary)
+
+	err = fs.Rename("foo", "../bar")
+	c.Assert(err, Equals, ErrCrossedBoundary)
+}
+
+func (s *ChrootSuite) TestRemoveOutOffBoundary(c *C) {
+	err := util.WriteFile(s.FS, "bar", nil, 0644)
+	c.Assert(err, IsNil)
+
+	fs, _ := s.FS.Chroot("foo")
+	err = fs.Remove("../bar")
+	c.Assert(err, Equals, ErrCrossedBoundary)
+}
+
+func (s *FilesystemSuite) TestRoot(c *C) {
+	c.Assert(s.FS.Root(), Not(Equals), "")
+}
diff --git a/test/fs_suite_linux.go b/test/common_posix.go
similarity index 100%
rename from test/fs_suite_linux.go
rename to test/common_posix.go
diff --git a/test/fs_suite_windows.go b/test/common_windows.go
similarity index 100%
rename from test/fs_suite_windows.go
rename to test/common_windows.go
diff --git a/test/dir.go b/test/dir.go
new file mode 100644
index 0000000..c7ca1ad
--- /dev/null
+++ b/test/dir.go
@@ -0,0 +1,238 @@
+package test
+
+import (
+	"os"
+
+	. "gopkg.in/check.v1"
+	. "gopkg.in/src-d/go-billy.v3"
+	"gopkg.in/src-d/go-billy.v3/util"
+)
+
+// DirSuite is a convenient test suite to validate any implementation of
+// billy.Dir
+type DirSuite struct {
+	FS interface {
+		Basic
+		Dir
+	}
+}
+
+func (s *DirSuite) TestMkdirAll(c *C) {
+	err := s.FS.MkdirAll("empty", os.FileMode(0755))
+	c.Assert(err, IsNil)
+
+	fi, err := s.FS.Stat("empty")
+	c.Assert(err, IsNil)
+	c.Assert(fi.IsDir(), Equals, true)
+}
+
+func (s *DirSuite) TestMkdirAllNested(c *C) {
+	err := s.FS.MkdirAll("foo/bar/baz", os.FileMode(0755))
+	c.Assert(err, IsNil)
+
+	fi, err := s.FS.Stat("foo/bar/baz")
+	c.Assert(err, IsNil)
+	c.Assert(fi.IsDir(), Equals, true)
+
+	fi, err = s.FS.Stat("foo/bar")
+	c.Assert(err, IsNil)
+	c.Assert(fi.IsDir(), Equals, true)
+
+	fi, err = s.FS.Stat("foo")
+	c.Assert(err, IsNil)
+	c.Assert(fi.IsDir(), Equals, true)
+}
+
+func (s *DirSuite) TestMkdirAllIdempotent(c *C) {
+	err := s.FS.MkdirAll("empty", 0755)
+	c.Assert(err, IsNil)
+	fi, err := s.FS.Stat("empty")
+	c.Assert(err, IsNil)
+	c.Assert(fi.IsDir(), Equals, true)
+
+	// idempotent
+	err = s.FS.MkdirAll("empty", 0755)
+	c.Assert(err, IsNil)
+	fi, err = s.FS.Stat("empty")
+	c.Assert(err, IsNil)
+	c.Assert(fi.IsDir(), Equals, true)
+}
+
+func (s *DirSuite) TestMkdirAllAndCreate(c *C) {
+	err := s.FS.MkdirAll("dir", os.FileMode(0755))
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Create("dir/bar/foo")
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+
+	fi, err := s.FS.Stat("dir/bar/foo")
+	c.Assert(err, IsNil)
+	c.Assert(fi.IsDir(), Equals, false)
+}
+
+func (s *DirSuite) TestMkdirAllWithExistingFile(c *C) {
+	f, err := s.FS.Create("dir/foo")
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+
+	err = s.FS.MkdirAll("dir/foo", os.FileMode(0755))
+	c.Assert(err, NotNil)
+
+	fi, err := s.FS.Stat("dir/foo")
+	c.Assert(err, IsNil)
+	c.Assert(fi.IsDir(), Equals, false)
+}
+
+func (s *DirSuite) TestStatDir(c *C) {
+	s.FS.MkdirAll("foo/bar", 0644)
+
+	fi, err := s.FS.Stat("foo/bar")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "bar")
+	c.Assert(fi.Mode().IsDir(), Equals, true)
+	c.Assert(fi.ModTime().IsZero(), Equals, false)
+	c.Assert(fi.IsDir(), Equals, true)
+}
+
+func (s *BasicSuite) TestStatDeep(c *C) {
+	files := []string{"foo", "bar", "qux/baz", "qux/qux"}
+	for _, name := range files {
+		err := util.WriteFile(s.FS, name, nil, 0644)
+		c.Assert(err, IsNil)
+	}
+
+	// Some implementations detect directories based on a prefix
+	// for all files; it's easy to miss path separator handling there.
+	fi, err := s.FS.Stat("qu")
+	c.Assert(os.IsNotExist(err), Equals, true, Commentf("error: %s", err))
+	c.Assert(fi, IsNil)
+
+	fi, err = s.FS.Stat("qux")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "qux")
+	c.Assert(fi.IsDir(), Equals, true)
+
+	fi, err = s.FS.Stat("qux/baz")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "baz")
+	c.Assert(fi.IsDir(), Equals, false)
+}
+
+func (s *DirSuite) TestReadDir(c *C) {
+	files := []string{"foo", "bar", "qux/baz", "qux/qux"}
+	for _, name := range files {
+		err := util.WriteFile(s.FS, name, nil, 0644)
+		c.Assert(err, IsNil)
+	}
+
+	info, err := s.FS.ReadDir("/")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 3)
+
+	info, err = s.FS.ReadDir("/qux")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 2)
+}
+
+func (s *DirSuite) TestReadDirWithMkDirAll(c *C) {
+	err := s.FS.MkdirAll("qux", 0644)
+	c.Assert(err, IsNil)
+
+	files := []string{"qux/baz", "qux/qux"}
+	for _, name := range files {
+		err := util.WriteFile(s.FS, name, nil, 0644)
+		c.Assert(err, IsNil)
+	}
+
+	info, err := s.FS.ReadDir("/")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 1)
+	c.Assert(info[0].IsDir(), Equals, true)
+
+	info, err = s.FS.ReadDir("/qux")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 2)
+}
+
+func (s *DirSuite) TestReadDirFileInfo(c *C) {
+	err := util.WriteFile(s.FS, "foo", []byte{'F', 'O', 'O'}, 0644)
+	c.Assert(err, IsNil)
+
+	info, err := s.FS.ReadDir("/")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 1)
+
+	c.Assert(info[0].Size(), Equals, int64(3))
+	c.Assert(info[0].IsDir(), Equals, false)
+	c.Assert(info[0].Name(), Equals, "foo")
+}
+
+func (s *DirSuite) TestReadDirFileInfoDirs(c *C) {
+	files := []string{"qux/baz/foo"}
+	for _, name := range files {
+		err := util.WriteFile(s.FS, name, []byte{'F', 'O', 'O'}, 0644)
+		c.Assert(err, IsNil)
+	}
+
+	info, err := s.FS.ReadDir("qux")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 1)
+	c.Assert(info[0].IsDir(), Equals, true)
+	c.Assert(info[0].Name(), Equals, "baz")
+
+	info, err = s.FS.ReadDir("qux/baz")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 1)
+	c.Assert(info[0].Size(), Equals, int64(3))
+	c.Assert(info[0].IsDir(), Equals, false)
+	c.Assert(info[0].Name(), Equals, "foo")
+	c.Assert(info[0].Mode(), Not(Equals), 0)
+}
+
+func (s *DirSuite) TestRenameToDir(c *C) {
+	err := util.WriteFile(s.FS, "foo", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Rename("foo", "bar/qux")
+	c.Assert(err, IsNil)
+
+	old, err := s.FS.Stat("foo")
+	c.Assert(old, IsNil)
+	c.Assert(os.IsNotExist(err), Equals, true)
+
+	dir, err := s.FS.Stat("bar")
+	c.Assert(dir, NotNil)
+	c.Assert(err, IsNil)
+
+	file, err := s.FS.Stat("bar/qux")
+	c.Assert(file.Name(), Equals, "qux")
+	c.Assert(err, IsNil)
+}
+
+func (s *DirSuite) TestRenameDir(c *C) {
+	err := s.FS.MkdirAll("foo", 0644)
+	c.Assert(err, IsNil)
+
+	err = util.WriteFile(s.FS, "foo/bar", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Rename("foo", "bar")
+	c.Assert(err, IsNil)
+
+	dirfoo, err := s.FS.Stat("foo")
+	c.Assert(dirfoo, IsNil)
+	c.Assert(os.IsNotExist(err), Equals, true)
+
+	dirbar, err := s.FS.Stat("bar")
+	c.Assert(err, IsNil)
+	c.Assert(dirbar, NotNil)
+
+	foo, err := s.FS.Stat("foo/bar")
+	c.Assert(os.IsNotExist(err), Equals, true)
+	c.Assert(foo, IsNil)
+
+	bar, err := s.FS.Stat("bar/bar")
+	c.Assert(err, IsNil)
+	c.Assert(bar, NotNil)
+}
diff --git a/test/fs.go b/test/fs.go
new file mode 100644
index 0000000..e135e1c
--- /dev/null
+++ b/test/fs.go
@@ -0,0 +1,198 @@
+package test
+
+import (
+	"os"
+
+	. "gopkg.in/check.v1"
+	. "gopkg.in/src-d/go-billy.v3"
+	"gopkg.in/src-d/go-billy.v3/util"
+)
+
+// FilesystemSuite is a convenient test suite to validate any implementation of
+// billy.Filesystem
+type FilesystemSuite struct {
+	FS Filesystem
+
+	BasicSuite
+	DirSuite
+	SymlinkSuite
+	TempFileSuite
+	ChrootSuite
+}
+
+// NewFilesystemSuite returns a new FilesystemSuite based on the given fs.
+func NewFilesystemSuite(fs Filesystem) FilesystemSuite {
+	s := FilesystemSuite{FS: fs}
+	s.BasicSuite.FS = s.FS
+	s.DirSuite.FS = s.FS
+	s.SymlinkSuite.FS = s.FS
+	s.TempFileSuite.FS = s.FS
+	s.ChrootSuite.FS = s.FS
+
+	return s
+}
+
+func (s *FilesystemSuite) TestSymlinkToDir(c *C) {
+	err := s.FS.MkdirAll("dir", 0755)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("dir", "link")
+	c.Assert(err, IsNil)
+
+	fi, err := s.FS.Stat("link")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "link")
+	c.Assert(fi.IsDir(), Equals, true)
+}
+
+func (s *FilesystemSuite) TestSymlinkReadDir(c *C) {
+	err := util.WriteFile(s.FS, "dir/file", []byte("foo"), 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("dir", "link")
+	c.Assert(err, IsNil)
+
+	info, err := s.FS.ReadDir("link")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 1)
+
+	c.Assert(info[0].Size(), Equals, int64(3))
+	c.Assert(info[0].IsDir(), Equals, false)
+	c.Assert(info[0].Name(), Equals, "file")
+}
+
+func (s *FilesystemSuite) TestCreateWithExistantDir(c *C) {
+	err := s.FS.MkdirAll("foo", 0644)
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Create("foo")
+	c.Assert(err, NotNil)
+	c.Assert(f, IsNil)
+}
+
+func (s *ChrootSuite) TestReadDirWithChroot(c *C) {
+	files := []string{"foo", "bar", "qux/baz", "qux/qux"}
+	for _, name := range files {
+		err := util.WriteFile(s.FS, name, nil, 0644)
+		c.Assert(err, IsNil)
+	}
+
+	qux, _ := s.FS.Chroot("/qux")
+
+	info, err := qux.(Filesystem).ReadDir("/")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 2)
+}
+
+func (s *FilesystemSuite) TestSymlinkWithChrootBasic(c *C) {
+	qux, _ := s.FS.Chroot("/qux")
+
+	err := util.WriteFile(qux, "file", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = qux.(Filesystem).Symlink("file", "link")
+	c.Assert(err, IsNil)
+
+	fi, err := qux.Stat("link")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "link")
+
+	fi, err = s.FS.Stat("qux/link")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "link")
+}
+
+func (s *FilesystemSuite) TestSymlinkWithChrootCrossBounders(c *C) {
+	qux, _ := s.FS.Chroot("/qux")
+	err := qux.(Filesystem).Symlink("../../file", "qux/link")
+	c.Assert(err, Equals, ErrCrossedBoundary)
+}
+
+func (s *FilesystemSuite) TestReadDirWithLink(c *C) {
+	util.WriteFile(s.FS, "foo/bar", []byte("foo"), customMode)
+	s.FS.Symlink("bar", "foo/qux")
+
+	info, err := s.FS.ReadDir("/foo")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 2)
+}
+
+func (s *FilesystemSuite) TestRemoveAllNonExistent(c *C) {
+	c.Assert(util.RemoveAll(s.FS, "non-existent"), IsNil)
+}
+
+func (s *FilesystemSuite) TestRemoveAllEmptyDir(c *C) {
+	c.Assert(s.FS.MkdirAll("empty", os.FileMode(0755)), IsNil)
+	c.Assert(util.RemoveAll(s.FS, "empty"), IsNil)
+	_, err := s.FS.Stat("empty")
+	c.Assert(err, NotNil)
+	c.Assert(os.IsNotExist(err), Equals, true)
+}
+
+func (s *FilesystemSuite) TestRemoveAll(c *C) {
+	fnames := []string{
+		"foo/1",
+		"foo/2",
+		"foo/bar/1",
+		"foo/bar/2",
+		"foo/bar/baz/1",
+		"foo/bar/baz/qux/1",
+		"foo/bar/baz/qux/2",
+		"foo/bar/baz/qux/3",
+	}
+
+	for _, fname := range fnames {
+		err := util.WriteFile(s.FS, fname, nil, 0644)
+		c.Assert(err, IsNil)
+	}
+
+	c.Assert(util.RemoveAll(s.FS, "foo"), IsNil)
+
+	for _, fname := range fnames {
+		_, err := s.FS.Stat(fname)
+		comment := Commentf("not removed: %s %s", fname, err)
+		c.Assert(os.IsNotExist(err), Equals, true, comment)
+	}
+}
+
+func (s *FilesystemSuite) TestRemoveAllRelative(c *C) {
+	fnames := []string{
+		"foo/1",
+		"foo/2",
+		"foo/bar/1",
+		"foo/bar/2",
+		"foo/bar/baz/1",
+		"foo/bar/baz/qux/1",
+		"foo/bar/baz/qux/2",
+		"foo/bar/baz/qux/3",
+	}
+
+	for _, fname := range fnames {
+		err := util.WriteFile(s.FS, fname, nil, 0644)
+		c.Assert(err, IsNil)
+	}
+
+	c.Assert(util.RemoveAll(s.FS, "foo/bar/.."), IsNil)
+
+	for _, fname := range fnames {
+		_, err := s.FS.Stat(fname)
+		comment := Commentf("not removed: %s %s", fname, err)
+		c.Assert(os.IsNotExist(err), Equals, true, comment)
+	}
+}
+
+func (s *FilesystemSuite) TestReadDir(c *C) {
+	files := []string{"foo", "bar", "qux/baz", "qux/qux"}
+	for _, name := range files {
+		err := util.WriteFile(s.FS, name, nil, 0644)
+		c.Assert(err, IsNil)
+	}
+
+	info, err := s.FS.ReadDir("/")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 3)
+
+	info, err = s.FS.ReadDir("/qux")
+	c.Assert(err, IsNil)
+	c.Assert(info, HasLen, 2)
+}
diff --git a/test/fs_suite.go b/test/fs_suite.go
deleted file mode 100644
index 9983f78..0000000
--- a/test/fs_suite.go
+++ /dev/null
@@ -1,1150 +0,0 @@
-package test
-
-import (
-	"bytes"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-	"testing"
-
-	. "gopkg.in/check.v1"
-	. "gopkg.in/src-d/go-billy.v2"
-)
-
-func Test(t *testing.T) { TestingT(t) }
-
-type FilesystemSuite struct {
-	FS Filesystem
-}
-
-func (s *FilesystemSuite) TestCreate(c *C) {
-	f, err := s.FS.Create("foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestOpen(c *C) {
-	f, err := s.FS.Create("foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo")
-	c.Assert(f.Close(), IsNil)
-
-	f, err = s.FS.Open("foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestOpenNotExists(c *C) {
-	f, err := s.FS.Open("not-exists")
-	c.Assert(err, NotNil)
-	c.Assert(f, IsNil)
-}
-
-func (s *FilesystemSuite) TestCreateDir(c *C) {
-	err := s.FS.MkdirAll("foo", 0644)
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Create("foo")
-	c.Assert(err, NotNil)
-	c.Assert(f, IsNil)
-}
-
-func (s *FilesystemSuite) TestCreateDepth(c *C) {
-	f, err := s.FS.Create("bar/foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, s.FS.Join("bar", "foo"))
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestCreateDepthAbsolute(c *C) {
-	f, err := s.FS.Create("/bar/foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, s.FS.Join("bar", "foo"))
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestCreateOverwrite(c *C) {
-	for i := 0; i < 3; i++ {
-		f, err := s.FS.Create("foo")
-		c.Assert(err, IsNil)
-
-		l, err := f.Write([]byte(fmt.Sprintf("foo%d", i)))
-		c.Assert(err, IsNil)
-		c.Assert(l, Equals, 4)
-
-		err = f.Close()
-		c.Assert(err, IsNil)
-	}
-
-	f, err := s.FS.Open("foo")
-	c.Assert(err, IsNil)
-
-	wrote, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(string(wrote), DeepEquals, "foo2")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestCreateClose(c *C) {
-	f, err := s.FS.Create("foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.IsClosed(), Equals, false)
-
-	_, err = f.Write([]byte("foo"))
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	f, err = s.FS.Open(f.Filename())
-	c.Assert(err, IsNil)
-
-	wrote, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(string(wrote), DeepEquals, "foo")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestOpenFile(c *C) {
-	defaultMode := os.FileMode(0666)
-
-	f, err := s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, defaultMode)
-	c.Assert(err, IsNil)
-	s.testWriteClose(c, f, "foo1")
-
-	// Truncate if it exists
-	f, err = s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, defaultMode)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo1")
-	s.testWriteClose(c, f, "foo1overwritten")
-
-	// Read-only if it exists
-	f, err = s.FS.OpenFile("foo1", os.O_RDONLY, defaultMode)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo1")
-	s.testReadClose(c, f, "foo1overwritten")
-
-	// Create when it does exist
-	f, err = s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, defaultMode)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo1")
-	s.testWriteClose(c, f, "bar")
-
-	f, err = s.FS.OpenFile("foo1", os.O_RDONLY, defaultMode)
-	c.Assert(err, IsNil)
-	s.testReadClose(c, f, "bar")
-}
-
-func (s *FilesystemSuite) TestOpenFileNoTruncate(c *C) {
-	defaultMode := os.FileMode(0666)
-
-	// Create when it does not exist
-	f, err := s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY, defaultMode)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo1")
-	s.testWriteClose(c, f, "foo1")
-
-	f, err = s.FS.OpenFile("foo1", os.O_RDONLY, defaultMode)
-	c.Assert(err, IsNil)
-	s.testReadClose(c, f, "foo1")
-
-	// Create when it does exist
-	f, err = s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY, defaultMode)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo1")
-	s.testWriteClose(c, f, "bar")
-
-	f, err = s.FS.OpenFile("foo1", os.O_RDONLY, defaultMode)
-	c.Assert(err, IsNil)
-	s.testReadClose(c, f, "bar1")
-}
-
-func (s *FilesystemSuite) TestOpenFileAppend(c *C) {
-	defaultMode := os.FileMode(0666)
-
-	f, err := s.FS.OpenFile("foo1", os.O_CREATE|os.O_WRONLY|os.O_APPEND, defaultMode)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo1")
-	s.testWriteClose(c, f, "foo1")
-
-	f, err = s.FS.OpenFile("foo1", os.O_WRONLY|os.O_APPEND, defaultMode)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo1")
-	s.testWriteClose(c, f, "bar1")
-
-	f, err = s.FS.OpenFile("foo1", os.O_RDONLY, defaultMode)
-	c.Assert(err, IsNil)
-	s.testReadClose(c, f, "foo1bar1")
-}
-
-func (s *FilesystemSuite) TestOpenFileReadWrite(c *C) {
-	defaultMode := os.FileMode(0666)
-
-	f, err := s.FS.OpenFile("foo1", os.O_CREATE|os.O_TRUNC|os.O_RDWR, defaultMode)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo1")
-
-	written, err := f.Write([]byte("foobar"))
-	c.Assert(written, Equals, 6)
-	c.Assert(err, IsNil)
-
-	_, err = f.Seek(0, os.SEEK_SET)
-	c.Assert(err, IsNil)
-
-	written, err = f.Write([]byte("qux"))
-	c.Assert(written, Equals, 3)
-	c.Assert(err, IsNil)
-
-	_, err = f.Seek(0, os.SEEK_SET)
-	c.Assert(err, IsNil)
-
-	s.testReadClose(c, f, "quxbar")
-}
-
-func (s *FilesystemSuite) TestOpenFileWithModes(c *C) {
-	f, err := s.FS.OpenFile("foo", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, customMode)
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	fi, err := s.FS.Stat("foo")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Mode(), Equals, os.FileMode(customMode))
-}
-
-func (s *FilesystemSuite) testWriteClose(c *C, f File, content string) {
-	written, err := f.Write([]byte(content))
-	c.Assert(written, Equals, len(content))
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) testReadClose(c *C, f File, content string) {
-	read, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(string(read), Equals, content)
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestFileWrite(c *C) {
-	f, err := s.FS.Create("foo")
-	c.Assert(err, IsNil)
-
-	n, err := f.Write([]byte("foo"))
-	c.Assert(err, IsNil)
-	c.Assert(n, Equals, 3)
-
-	f.Seek(0, io.SeekStart)
-	all, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(string(all), Equals, "foo")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestFileWriteClose(c *C) {
-	f, err := s.FS.Create("foo")
-	c.Assert(err, IsNil)
-
-	c.Assert(f.Close(), IsNil)
-
-	_, err = f.Write([]byte("foo"))
-	c.Assert(err, NotNil)
-}
-
-func (s *FilesystemSuite) TestFileRead(c *C) {
-	err := WriteFile(s.FS, "foo", []byte("foo"), 0644)
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Open("foo")
-	c.Assert(err, IsNil)
-
-	all, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(string(all), Equals, "foo")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestFileClosed(c *C) {
-	err := WriteFile(s.FS, "foo", []byte("foo"), 0644)
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Open("foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	_, err = ioutil.ReadAll(f)
-	c.Assert(err, NotNil)
-}
-
-func (s *FilesystemSuite) TestFileNonRead(c *C) {
-	err := WriteFile(s.FS, "foo", []byte("foo"), 0644)
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.OpenFile("foo", os.O_WRONLY, 0)
-	c.Assert(err, IsNil)
-
-	_, err = ioutil.ReadAll(f)
-	c.Assert(err, NotNil)
-
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestFileSeekstart(c *C) {
-	s.testFileSeek(c, 10, io.SeekStart)
-}
-
-func (s *FilesystemSuite) TestFileSeekCurrent(c *C) {
-	s.testFileSeek(c, 5, io.SeekCurrent)
-}
-
-func (s *FilesystemSuite) TestFileSeekEnd(c *C) {
-	s.testFileSeek(c, -26, io.SeekEnd)
-}
-
-func (s *FilesystemSuite) testFileSeek(c *C, offset int64, whence int) {
-	err := WriteFile(s.FS, "foo", []byte("0123456789abcdefghijklmnopqrstuvwxyz"), 0644)
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Open("foo")
-	c.Assert(err, IsNil)
-
-	some := make([]byte, 5)
-	_, err = f.Read(some)
-	c.Assert(err, IsNil)
-	c.Assert(string(some), Equals, "01234")
-
-	p, err := f.Seek(offset, whence)
-	c.Assert(err, IsNil)
-	c.Assert(int(p), Equals, 10)
-
-	all, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(all, HasLen, 26)
-	c.Assert(string(all), Equals, "abcdefghijklmnopqrstuvwxyz")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestSeekToEndAndWrite(c *C) {
-	defaultMode := os.FileMode(0666)
-
-	f, err := s.FS.OpenFile("foo1", os.O_CREATE|os.O_TRUNC|os.O_RDWR, defaultMode)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, "foo1")
-
-	_, err = f.Seek(10, io.SeekEnd)
-	c.Assert(err, IsNil)
-
-	n, err := f.Write([]byte(`TEST`))
-	c.Assert(err, IsNil)
-	c.Assert(n, Equals, 4)
-
-	_, err = f.Seek(0, io.SeekStart)
-	c.Assert(err, IsNil)
-
-	s.testReadClose(c, f, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TEST")
-}
-
-func (s *FilesystemSuite) TestFileSeekClosed(c *C) {
-	err := WriteFile(s.FS, "foo", []byte("foo"), 0644)
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Open("foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	_, err = f.Seek(0, 0)
-	c.Assert(err, NotNil)
-}
-
-func (s *FilesystemSuite) TestFileCloseTwice(c *C) {
-	f, err := s.FS.Create("foo")
-	c.Assert(err, IsNil)
-
-	c.Assert(f.Close(), IsNil)
-	c.Assert(f.Close(), NotNil)
-}
-
-func (s *FilesystemSuite) TestReadDirAndDir(c *C) {
-	files := []string{"foo", "bar", "qux/baz", "qux/qux"}
-	for _, name := range files {
-		err := WriteFile(s.FS, name, nil, 0644)
-		c.Assert(err, IsNil)
-	}
-
-	info, err := s.FS.ReadDir("/")
-	c.Assert(err, IsNil)
-	c.Assert(info, HasLen, 3)
-
-	info, err = s.FS.ReadDir("/qux")
-	c.Assert(err, IsNil)
-	c.Assert(info, HasLen, 2)
-
-	qux := s.FS.Dir("/qux")
-	info, err = qux.ReadDir("/")
-	c.Assert(err, IsNil)
-	c.Assert(info, HasLen, 2)
-}
-
-func (s *FilesystemSuite) TestReadDirAndWithMkDirAll(c *C) {
-	err := s.FS.MkdirAll("qux", 0644)
-	c.Assert(err, IsNil)
-
-	files := []string{"qux/baz", "qux/qux"}
-	for _, name := range files {
-		err := WriteFile(s.FS, name, nil, 0644)
-		c.Assert(err, IsNil)
-	}
-
-	info, err := s.FS.ReadDir("/")
-	c.Assert(err, IsNil)
-	c.Assert(info, HasLen, 1)
-	c.Assert(info[0].IsDir(), Equals, true)
-
-	info, err = s.FS.ReadDir("/qux")
-	c.Assert(err, IsNil)
-	c.Assert(info, HasLen, 2)
-
-	qux := s.FS.Dir("/qux")
-	info, err = qux.ReadDir("/")
-	c.Assert(err, IsNil)
-	c.Assert(info, HasLen, 2)
-}
-
-func (s *FilesystemSuite) TestReadDirFileInfo(c *C) {
-	err := WriteFile(s.FS, "foo", []byte{'F', 'O', 'O'}, 0644)
-	c.Assert(err, IsNil)
-
-	info, err := s.FS.ReadDir("/")
-	c.Assert(err, IsNil)
-	c.Assert(info, HasLen, 1)
-
-	c.Assert(info[0].Size(), Equals, int64(3))
-	c.Assert(info[0].IsDir(), Equals, false)
-	c.Assert(info[0].Name(), Equals, "foo")
-}
-
-func (s *FilesystemSuite) TestReadDirFileInfoDirs(c *C) {
-	files := []string{"qux/baz/foo"}
-	for _, name := range files {
-		err := WriteFile(s.FS, name, []byte{'F', 'O', 'O'}, 0644)
-		c.Assert(err, IsNil)
-	}
-
-	info, err := s.FS.ReadDir("qux")
-	c.Assert(err, IsNil)
-	c.Assert(info, HasLen, 1)
-	c.Assert(info[0].IsDir(), Equals, true)
-	c.Assert(info[0].Name(), Equals, "baz")
-
-	info, err = s.FS.ReadDir("qux/baz")
-	c.Assert(err, IsNil)
-	c.Assert(info, HasLen, 1)
-	c.Assert(info[0].Size(), Equals, int64(3))
-	c.Assert(info[0].IsDir(), Equals, false)
-	c.Assert(info[0].Name(), Equals, "foo")
-	c.Assert(info[0].Mode(), Not(Equals), 0)
-}
-
-func (s *FilesystemSuite) TestStat(c *C) {
-	WriteFile(s.FS, "foo/bar", []byte("foo"), customMode)
-
-	fi, err := s.FS.Stat("foo/bar")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "bar")
-	c.Assert(fi.Size(), Equals, int64(3))
-	c.Assert(fi.Mode(), Equals, customMode)
-	c.Assert(fi.ModTime().IsZero(), Equals, false)
-	c.Assert(fi.IsDir(), Equals, false)
-}
-
-func (s *FilesystemSuite) TestStatLink(c *C) {
-	WriteFile(s.FS, "foo/bar", []byte("foo"), customMode)
-	s.FS.Symlink("bar", "foo/qux")
-
-	fi, err := s.FS.Stat("foo/qux")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "qux")
-	c.Assert(fi.Size(), Equals, int64(3))
-	c.Assert(fi.Mode(), Equals, customMode)
-	c.Assert(fi.ModTime().IsZero(), Equals, false)
-	c.Assert(fi.IsDir(), Equals, false)
-}
-
-func (s *FilesystemSuite) TestStatDir(c *C) {
-	s.FS.MkdirAll("foo/bar", 0644)
-
-	fi, err := s.FS.Stat("foo/bar")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "bar")
-	c.Assert(fi.Mode().IsDir(), Equals, true)
-	c.Assert(fi.ModTime().IsZero(), Equals, false)
-	c.Assert(fi.IsDir(), Equals, true)
-}
-
-func (s *FilesystemSuite) TestStatNonExistent(c *C) {
-	fi, err := s.FS.Stat("non-existent")
-	comment := Commentf("error: %s", err)
-	c.Assert(os.IsNotExist(err), Equals, true, comment)
-	c.Assert(fi, IsNil)
-}
-
-func (s *FilesystemSuite) TestLstat(c *C) {
-	WriteFile(s.FS, "foo/bar", []byte("foo"), customMode)
-
-	fi, err := s.FS.Lstat("foo/bar")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "bar")
-	c.Assert(fi.Size(), Equals, int64(3))
-	c.Assert(fi.Mode()&os.ModeSymlink != 0, Equals, false)
-	c.Assert(fi.ModTime().IsZero(), Equals, false)
-	c.Assert(fi.IsDir(), Equals, false)
-}
-
-func (s *FilesystemSuite) TestLstatLink(c *C) {
-	WriteFile(s.FS, "foo/bar", []byte("fosddddaaao"), customMode)
-	s.FS.Symlink("bar", "foo/qux")
-
-	fi, err := s.FS.Lstat("foo/qux")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "qux")
-	c.Assert(fi.Mode()&os.ModeSymlink != 0, Equals, true)
-	c.Assert(fi.ModTime().IsZero(), Equals, false)
-	c.Assert(fi.IsDir(), Equals, false)
-}
-
-func (s *FilesystemSuite) TestDirStat(c *C) {
-	files := []string{"foo", "bar", "qux/baz", "qux/qux"}
-	for _, name := range files {
-		err := WriteFile(s.FS, name, nil, 0644)
-		c.Assert(err, IsNil)
-	}
-
-	// Some implementations detect directories based on a prefix
-	// for all files; it's easy to miss path separator handling there.
-	fi, err := s.FS.Stat("qu")
-	c.Assert(os.IsNotExist(err), Equals, true, Commentf("error: %s", err))
-	c.Assert(fi, IsNil)
-
-	fi, err = s.FS.Stat("qux")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "qux")
-	c.Assert(fi.IsDir(), Equals, true)
-
-	qux := s.FS.Dir("qux")
-
-	fi, err = qux.Stat("baz")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "baz")
-	c.Assert(fi.IsDir(), Equals, false)
-
-	fi, err = qux.Stat("/baz")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "baz")
-	c.Assert(fi.IsDir(), Equals, false)
-}
-
-func (s *FilesystemSuite) TestCreateInDir(c *C) {
-	f, err := s.FS.Dir("foo").Create("bar")
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-	c.Assert(f.Filename(), Equals, "bar")
-
-	f, err = s.FS.Open("foo/bar")
-	c.Assert(f.Filename(), Equals, s.FS.Join("foo", "bar"))
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestRename(c *C) {
-	err := WriteFile(s.FS, "foo", nil, 0644)
-	c.Assert(err, IsNil)
-
-	files, err := s.FS.ReadDir("")
-	c.Assert(err, IsNil)
-	c.Assert(files, HasLen, 1)
-
-	err = s.FS.Rename("foo", "bar")
-	c.Assert(err, IsNil)
-
-	foo, err := s.FS.Stat("foo")
-	c.Assert(foo, IsNil)
-	c.Assert(os.IsNotExist(err), Equals, true)
-
-	bar, err := s.FS.Stat("bar")
-	c.Assert(err, IsNil)
-	c.Assert(bar, NotNil)
-
-	files, err = s.FS.ReadDir("")
-	c.Assert(err, IsNil)
-	c.Assert(files, HasLen, 1)
-}
-
-func (s *FilesystemSuite) TestRenameToDir(c *C) {
-	err := WriteFile(s.FS, "foo", nil, 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Rename("foo", "bar/qux")
-	c.Assert(err, IsNil)
-
-	old, err := s.FS.Stat("foo")
-	c.Assert(old, IsNil)
-	c.Assert(os.IsNotExist(err), Equals, true)
-
-	dir, err := s.FS.Stat("bar")
-	c.Assert(dir, NotNil)
-	c.Assert(err, IsNil)
-
-	file, err := s.FS.Stat("bar/qux")
-	c.Assert(file.Name(), Equals, "qux")
-	c.Assert(err, IsNil)
-}
-
-func (s *FilesystemSuite) TestRenameDir(c *C) {
-	err := s.FS.MkdirAll("foo", 0644)
-	c.Assert(err, IsNil)
-
-	err = WriteFile(s.FS, "foo/bar", nil, 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Rename("foo", "bar")
-	c.Assert(err, IsNil)
-
-	dirfoo, err := s.FS.Stat("foo")
-	c.Assert(dirfoo, IsNil)
-	c.Assert(os.IsNotExist(err), Equals, true)
-
-	dirbar, err := s.FS.Stat("bar")
-	c.Assert(err, IsNil)
-	c.Assert(dirbar, NotNil)
-
-	foo, err := s.FS.Stat("foo/bar")
-	c.Assert(os.IsNotExist(err), Equals, true)
-	c.Assert(foo, IsNil)
-
-	bar, err := s.FS.Stat("bar/bar")
-	c.Assert(err, IsNil)
-	c.Assert(bar, NotNil)
-}
-
-func (s *FilesystemSuite) TestTempFile(c *C) {
-	f, err := s.FS.TempFile("", "bar")
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	c.Assert(strings.HasPrefix(f.Filename(), "bar"), Equals, true)
-}
-
-func (s *FilesystemSuite) TestTempFileWithPath(c *C) {
-	f, err := s.FS.TempFile("foo", "bar")
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	c.Assert(strings.HasPrefix(f.Filename(), s.FS.Join("foo", "bar")), Equals, true)
-}
-
-func (s *FilesystemSuite) TestTempFileFullWithPath(c *C) {
-	f, err := s.FS.TempFile("/foo", "bar")
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	c.Assert(strings.HasPrefix(f.Filename(), s.FS.Join("foo", "bar")), Equals, true)
-}
-
-func (s *FilesystemSuite) TestOpenAndWrite(c *C) {
-	err := WriteFile(s.FS, "foo", nil, 0644)
-	c.Assert(err, IsNil)
-
-	foo, err := s.FS.Open("foo")
-	c.Assert(foo, NotNil)
-	c.Assert(err, IsNil)
-
-	n, err := foo.Write([]byte("foo"))
-	c.Assert(err, NotNil)
-	c.Assert(n, Equals, 0)
-
-	c.Assert(foo.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestOpenAndStat(c *C) {
-	err := WriteFile(s.FS, "foo", []byte("foo"), 0644)
-	c.Assert(err, IsNil)
-
-	foo, err := s.FS.Open("foo")
-	c.Assert(foo, NotNil)
-	c.Assert(foo.Filename(), Equals, "foo")
-	c.Assert(err, IsNil)
-	c.Assert(foo.Close(), IsNil)
-
-	stat, err := s.FS.Stat("foo")
-	c.Assert(stat, NotNil)
-	c.Assert(err, IsNil)
-	c.Assert(stat.Name(), Equals, "foo")
-	c.Assert(stat.Size(), Equals, int64(3))
-}
-
-func (s *FilesystemSuite) TestRemove(c *C) {
-	f, err := s.FS.Create("foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	err = s.FS.Remove("foo")
-	c.Assert(err, IsNil)
-}
-
-func (s *FilesystemSuite) TestRemoveNonExisting(c *C) {
-	err := s.FS.Remove("NON-EXISTING")
-	c.Assert(err, NotNil)
-	c.Assert(os.IsNotExist(err), Equals, true)
-}
-
-func (s *FilesystemSuite) TestRemoveNotEmptyDir(c *C) {
-	err := WriteFile(s.FS, "foo", nil, 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Remove("no-exists")
-	c.Assert(err, NotNil)
-}
-
-func (s *FilesystemSuite) TestRemoveTempFile(c *C) {
-	f, err := s.FS.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-
-	fn := f.Filename()
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	c.Assert(s.FS.Remove(fn), IsNil)
-}
-
-func (s *FilesystemSuite) TestJoin(c *C) {
-	c.Assert(s.FS.Join("foo", "bar"), Equals, fmt.Sprintf("foo%cbar", filepath.Separator))
-}
-
-func (s *FilesystemSuite) TestBase(c *C) {
-	c.Assert(s.FS.Base(), Not(Equals), "")
-}
-
-func (s *FilesystemSuite) TestReadAtOnReadWrite(c *C) {
-	f, err := s.FS.Create("foo")
-	c.Assert(err, IsNil)
-	_, err = f.Write([]byte("abcdefg"))
-	c.Assert(err, IsNil)
-
-	rf, ok := f.(io.ReaderAt)
-	c.Assert(ok, Equals, true)
-
-	b := make([]byte, 3)
-	n, err := rf.ReadAt(b, 2)
-	c.Assert(err, IsNil)
-	c.Assert(n, Equals, 3)
-	c.Assert(string(b), Equals, "cde")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestReadAtOnReadOnly(c *C) {
-	err := WriteFile(s.FS, "foo", []byte("abcdefg"), 0644)
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Open("foo")
-	c.Assert(err, IsNil)
-
-	rf, ok := f.(io.ReaderAt)
-	c.Assert(ok, Equals, true)
-
-	b := make([]byte, 3)
-	n, err := rf.ReadAt(b, 2)
-	c.Assert(err, IsNil)
-	c.Assert(n, Equals, 3)
-	c.Assert(string(b), Equals, "cde")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestReadAtEOF(c *C) {
-	err := WriteFile(s.FS, "foo", []byte("TEST"), 0644)
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Open("foo")
-	c.Assert(err, IsNil)
-
-	rf, ok := f.(io.ReaderAt)
-	c.Assert(ok, Equals, true)
-
-	b := make([]byte, 5)
-	n, err := rf.ReadAt(b, 0)
-	c.Assert(err, Equals, io.EOF)
-	c.Assert(n, Equals, 4)
-	c.Assert(string(b), Equals, "TEST\x00")
-
-	err = f.Close()
-	c.Assert(err, IsNil)
-}
-
-func (s *FilesystemSuite) TestReadAtOffset(c *C) {
-	err := WriteFile(s.FS, "foo", []byte("TEST"), 0644)
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Open("foo")
-	c.Assert(err, IsNil)
-
-	rf, ok := f.(io.ReaderAt)
-	c.Assert(ok, Equals, true)
-
-	o, err := f.Seek(0, io.SeekCurrent)
-	c.Assert(err, IsNil)
-	c.Assert(o, Equals, int64(0))
-
-	b := make([]byte, 4)
-	n, err := rf.ReadAt(b, 0)
-	c.Assert(err, IsNil)
-	c.Assert(n, Equals, 4)
-	c.Assert(string(b), Equals, "TEST")
-
-	o, err = f.Seek(0, io.SeekCurrent)
-	c.Assert(err, IsNil)
-	c.Assert(o, Equals, int64(0))
-
-	err = f.Close()
-	c.Assert(err, IsNil)
-}
-
-func (s *FilesystemSuite) TestReadWriteLargeFile(c *C) {
-	f, err := s.FS.Create("foo")
-	c.Assert(err, IsNil)
-
-	size := 1 << 20
-
-	n, err := f.Write(bytes.Repeat([]byte("F"), size))
-	c.Assert(err, IsNil)
-	c.Assert(n, Equals, size)
-
-	c.Assert(f.Close(), IsNil)
-
-	f, err = s.FS.Open("foo")
-	c.Assert(err, IsNil)
-	b, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(len(b), Equals, size)
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestRemoveAllNonExistent(c *C) {
-	c.Assert(RemoveAll(s.FS, "non-existent"), IsNil)
-}
-
-func (s *FilesystemSuite) TestRemoveAllEmptyDir(c *C) {
-	c.Assert(s.FS.MkdirAll("empty", os.FileMode(0755)), IsNil)
-	c.Assert(RemoveAll(s.FS, "empty"), IsNil)
-	_, err := s.FS.Stat("empty")
-	c.Assert(err, NotNil)
-	c.Assert(os.IsNotExist(err), Equals, true)
-}
-
-func (s *FilesystemSuite) TestRemoveAll(c *C) {
-	fnames := []string{
-		"foo/1",
-		"foo/2",
-		"foo/bar/1",
-		"foo/bar/2",
-		"foo/bar/baz/1",
-		"foo/bar/baz/qux/1",
-		"foo/bar/baz/qux/2",
-		"foo/bar/baz/qux/3",
-	}
-
-	for _, fname := range fnames {
-		err := WriteFile(s.FS, fname, nil, 0644)
-		c.Assert(err, IsNil)
-	}
-
-	c.Assert(RemoveAll(s.FS, "foo"), IsNil)
-
-	for _, fname := range fnames {
-		_, err := s.FS.Stat(fname)
-		comment := Commentf("not removed: %s %s", fname, err)
-		c.Assert(os.IsNotExist(err), Equals, true, comment)
-	}
-}
-
-func (s *FilesystemSuite) TestRemoveAllRelative(c *C) {
-	fnames := []string{
-		"foo/1",
-		"foo/2",
-		"foo/bar/1",
-		"foo/bar/2",
-		"foo/bar/baz/1",
-		"foo/bar/baz/qux/1",
-		"foo/bar/baz/qux/2",
-		"foo/bar/baz/qux/3",
-	}
-
-	for _, fname := range fnames {
-		err := WriteFile(s.FS, fname, nil, 0644)
-		c.Assert(err, IsNil)
-	}
-
-	c.Assert(RemoveAll(s.FS, "foo/bar/.."), IsNil)
-
-	for _, fname := range fnames {
-		_, err := s.FS.Stat(fname)
-		comment := Commentf("not removed: %s %s", fname, err)
-		c.Assert(os.IsNotExist(err), Equals, true, comment)
-	}
-}
-
-func (s *FilesystemSuite) TestMkdirAll(c *C) {
-	err := s.FS.MkdirAll("empty", os.FileMode(0755))
-	c.Assert(err, IsNil)
-
-	fi, err := s.FS.Stat("empty")
-	c.Assert(err, IsNil)
-	c.Assert(fi.IsDir(), Equals, true)
-}
-
-func (s *FilesystemSuite) TestMkdirAllNested(c *C) {
-	err := s.FS.MkdirAll("foo/bar/baz", os.FileMode(0755))
-	c.Assert(err, IsNil)
-
-	fi, err := s.FS.Stat("foo/bar/baz")
-	c.Assert(err, IsNil)
-	c.Assert(fi.IsDir(), Equals, true)
-}
-
-func (s *FilesystemSuite) TestMkdirAllIdempotent(c *C) {
-	err := s.FS.MkdirAll("empty", 0755)
-	c.Assert(err, IsNil)
-	fi, err := s.FS.Stat("empty")
-	c.Assert(err, IsNil)
-	c.Assert(fi.IsDir(), Equals, true)
-
-	// idempotent
-	err = s.FS.MkdirAll("empty", 0755)
-	c.Assert(err, IsNil)
-	fi, err = s.FS.Stat("empty")
-	c.Assert(err, IsNil)
-	c.Assert(fi.IsDir(), Equals, true)
-}
-
-func (s *FilesystemSuite) TestMkdirAllAndOpenFile(c *C) {
-	err := s.FS.MkdirAll("dir", os.FileMode(0755))
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Create("dir/bar/foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	fi, err := s.FS.Stat("dir/bar/foo")
-	c.Assert(err, IsNil)
-	c.Assert(fi.IsDir(), Equals, false)
-}
-
-func (s *FilesystemSuite) TestMkdirAllWithExistingFile(c *C) {
-	f, err := s.FS.Create("dir/foo")
-	c.Assert(err, IsNil)
-	c.Assert(f.Close(), IsNil)
-
-	err = s.FS.MkdirAll("dir/foo", os.FileMode(0755))
-	c.Assert(err, NotNil)
-
-	fi, err := s.FS.Stat("dir/foo")
-	c.Assert(err, IsNil)
-	c.Assert(fi.IsDir(), Equals, false)
-}
-
-func (s *FilesystemSuite) TestWriteFile(c *C) {
-	err := WriteFile(s.FS, "foo", []byte("bar"), 0777)
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Open("foo")
-	c.Assert(err, IsNil)
-
-	wrote, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(string(wrote), DeepEquals, "bar")
-
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestSymlinkBasic(c *C) {
-	err := WriteFile(s.FS, "file", nil, 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("file", "link")
-	c.Assert(err, IsNil)
-
-	fi, err := s.FS.Stat("link")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "link")
-}
-
-func (s *FilesystemSuite) TestSymlinkCrossDirs(c *C) {
-	err := WriteFile(s.FS, "foo/file", nil, 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("../foo/file", "bar/link")
-	c.Assert(err, IsNil)
-
-	fi, err := s.FS.Stat("bar/link")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "link")
-}
-
-func (s *FilesystemSuite) TestSymlinkLinkToLink(c *C) {
-	err := WriteFile(s.FS, "file", nil, 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("file", "linkA")
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("linkA", "linkB")
-	c.Assert(err, IsNil)
-
-	fi, err := s.FS.Stat("linkB")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "linkB")
-}
-
-func (s *FilesystemSuite) TestSymlinkToDir(c *C) {
-	err := s.FS.MkdirAll("dir", 0755)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("dir", "link")
-	c.Assert(err, IsNil)
-
-	fi, err := s.FS.Stat("link")
-	c.Assert(err, IsNil)
-	c.Assert(fi.Name(), Equals, "link")
-	c.Assert(fi.IsDir(), Equals, true)
-}
-
-func (s *FilesystemSuite) TestSymlinkWithNonExistentOldname(c *C) {
-	err := s.FS.Symlink("file", "link")
-	c.Assert(err, IsNil)
-
-	_, err = s.FS.Stat("link")
-	c.Assert(os.IsNotExist(err), Equals, true)
-}
-
-func (s *FilesystemSuite) TestSymlinkWithExistingNewname(c *C) {
-	err := WriteFile(s.FS, "link", nil, 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("file", "link")
-	c.Assert(err, Not(IsNil))
-}
-
-func (s *FilesystemSuite) TestSymlinkOpenWithRelativePath(c *C) {
-	err := WriteFile(s.FS, "dir/file", []byte("foo"), 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("file", "dir/link")
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Open("dir/link")
-	c.Assert(err, IsNil)
-
-	all, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(string(all), Equals, "foo")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestSymlinkOpenWithAbsolutePath(c *C) {
-	err := WriteFile(s.FS, "dir/file", []byte("foo"), 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("/dir/file", "dir/link")
-	c.Assert(err, IsNil)
-
-	f, err := s.FS.Open("dir/link")
-	c.Assert(err, IsNil)
-
-	all, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(string(all), Equals, "foo")
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *FilesystemSuite) TestSymlinkReadDir(c *C) {
-	err := WriteFile(s.FS, "dir/file", []byte("foo"), 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("dir", "link")
-	c.Assert(err, IsNil)
-
-	info, err := s.FS.ReadDir("link")
-	c.Assert(err, IsNil)
-	c.Assert(info, HasLen, 1)
-
-	c.Assert(info[0].Size(), Equals, int64(3))
-	c.Assert(info[0].IsDir(), Equals, false)
-	c.Assert(info[0].Name(), Equals, "file")
-}
-
-func (s *FilesystemSuite) TestSymlinkRename(c *C) {
-	err := s.FS.Symlink("file", "link")
-	c.Assert(err, IsNil)
-
-	err = s.FS.Rename("link", "newlink")
-	c.Assert(err, IsNil)
-
-	_, err = s.FS.Readlink("newlink")
-	c.Assert(err, IsNil)
-}
-
-func (s *FilesystemSuite) TestSymlinkRemove(c *C) {
-	err := s.FS.Symlink("file", "link")
-	c.Assert(err, IsNil)
-
-	err = s.FS.Remove("link")
-	c.Assert(err, IsNil)
-
-	_, err = s.FS.Readlink("link")
-	c.Assert(os.IsNotExist(err), Equals, true)
-}
-
-func (s *FilesystemSuite) TestReadlinkWithRelativePath(c *C) {
-	err := WriteFile(s.FS, "dir/file", nil, 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("file", "dir/link")
-	c.Assert(err, IsNil)
-
-	oldname, err := s.FS.Readlink("dir/link")
-	c.Assert(err, IsNil)
-	c.Assert(oldname, Equals, "file")
-}
-
-func (s *FilesystemSuite) TestReadlinkWithAbsolutePath(c *C) {
-	err := WriteFile(s.FS, "dir/file", nil, 0644)
-	c.Assert(err, IsNil)
-
-	err = s.FS.Symlink("/dir/file", "dir/link")
-	c.Assert(err, IsNil)
-
-	oldname, err := s.FS.Readlink("dir/link")
-	c.Assert(err, IsNil)
-	c.Assert(oldname, Equals, expectedSymlinkTarget)
-}
-
-func (s *FilesystemSuite) TestReadlinkWithNonExistentOldname(c *C) {
-	err := s.FS.Symlink("file", "link")
-	c.Assert(err, IsNil)
-
-	oldname, err := s.FS.Readlink("link")
-	c.Assert(err, IsNil)
-	c.Assert(oldname, Equals, "file")
-}
-
-func (s *FilesystemSuite) TestReadlinkWithNonExistentLink(c *C) {
-	_, err := s.FS.Readlink("link")
-	c.Assert(os.IsNotExist(err), Equals, true)
-}
-
-func (s *FilesystemSuite) TestReadlinkWithRegularFile(c *C) {
-	err := WriteFile(s.FS, "file", nil, 0644)
-	c.Assert(err, IsNil)
-
-	_, err = s.FS.Readlink("file")
-	c.Assert(err, Not(IsNil))
-}
diff --git a/test/fs_suite_darwin.go b/test/fs_suite_darwin.go
deleted file mode 100644
index 84dd8c1..0000000
--- a/test/fs_suite_darwin.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// +build !windows
-
-package test
-
-import "os"
-
-var (
-	customMode            os.FileMode = 0755
-	expectedSymlinkTarget             = "/dir/file"
-)
diff --git a/test/mock.go b/test/mock.go
new file mode 100644
index 0000000..aaa15bc
--- /dev/null
+++ b/test/mock.go
@@ -0,0 +1,122 @@
+package test
+
+import (
+	"bytes"
+	"os"
+	"path"
+	"path/filepath"
+
+	"gopkg.in/src-d/go-billy.v3"
+)
+
+type BasicMock struct {
+	CreateArgs   []string
+	OpenArgs     []string
+	OpenFileArgs [][3]interface{}
+	StatArgs     []string
+	RenameArgs   [][2]string
+	RemoveArgs   []string
+}
+
+func (fs *BasicMock) Create(filename string) (billy.File, error) {
+	fs.CreateArgs = append(fs.CreateArgs, filename)
+	return &FileMock{name: filename}, nil
+}
+
+func (fs *BasicMock) Open(filename string) (billy.File, error) {
+	fs.OpenArgs = append(fs.OpenArgs, filename)
+	return &FileMock{name: filename}, nil
+}
+
+func (fs *BasicMock) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) {
+	fs.OpenFileArgs = append(fs.OpenFileArgs, [3]interface{}{filename, flag, mode})
+	return &FileMock{name: filename}, nil
+}
+
+func (fs *BasicMock) Stat(filename string) (os.FileInfo, error) {
+	fs.StatArgs = append(fs.StatArgs, filename)
+	return nil, nil
+}
+
+func (fs *BasicMock) Rename(target, link string) error {
+	fs.RenameArgs = append(fs.RenameArgs, [2]string{target, link})
+	return nil
+}
+
+func (fs *BasicMock) Remove(filename string) error {
+	fs.RemoveArgs = append(fs.RemoveArgs, filename)
+	return nil
+}
+
+func (fs *BasicMock) Join(elem ...string) string {
+	return path.Join(elem...)
+}
+
+type TempFileMock struct {
+	BasicMock
+	TempFileArgs [][2]string
+}
+
+func (fs *TempFileMock) TempFile(dir, prefix string) (billy.File, error) {
+	fs.TempFileArgs = append(fs.TempFileArgs, [2]string{dir, prefix})
+	return &FileMock{name: "/tmp/hardcoded/mock/temp"}, nil
+}
+
+type DirMock struct {
+	BasicMock
+	ReadDirArgs  []string
+	MkdirAllArgs [][2]interface{}
+}
+
+func (fs *DirMock) ReadDir(path string) ([]os.FileInfo, error) {
+	fs.ReadDirArgs = append(fs.ReadDirArgs, path)
+	return nil, nil
+}
+
+func (fs *DirMock) MkdirAll(filename string, perm os.FileMode) error {
+	fs.MkdirAllArgs = append(fs.MkdirAllArgs, [2]interface{}{filename, perm})
+	return nil
+}
+
+type SymlinkMock struct {
+	BasicMock
+	LstatArgs    []string
+	SymlinkArgs  [][2]string
+	ReadlinkArgs []string
+}
+
+func (fs *SymlinkMock) Lstat(filename string) (os.FileInfo, error) {
+	fs.LstatArgs = append(fs.LstatArgs, filename)
+	return nil, nil
+}
+
+func (fs *SymlinkMock) Symlink(target, link string) error {
+	fs.SymlinkArgs = append(fs.SymlinkArgs, [2]string{target, link})
+	return nil
+}
+
+func (fs *SymlinkMock) Readlink(link string) (string, error) {
+	fs.ReadlinkArgs = append(fs.ReadlinkArgs, link)
+	return filepath.FromSlash(link), nil
+}
+
+type FileMock struct {
+	name string
+	bytes.Buffer
+}
+
+func (f *FileMock) Name() string {
+	return f.name
+}
+
+func (*FileMock) ReadAt(b []byte, off int64) (int, error) {
+	return 0, nil
+}
+
+func (*FileMock) Seek(offset int64, whence int) (int64, error) {
+	return 0, nil
+}
+
+func (*FileMock) Close() error {
+	return nil
+}
diff --git a/test/symlink.go b/test/symlink.go
new file mode 100644
index 0000000..ea68736
--- /dev/null
+++ b/test/symlink.go
@@ -0,0 +1,221 @@
+package test
+
+import (
+	"io/ioutil"
+	"os"
+
+	. "gopkg.in/check.v1"
+	. "gopkg.in/src-d/go-billy.v3"
+	"gopkg.in/src-d/go-billy.v3/util"
+)
+
+// SymlinkSuite is a convenient test suite to validate any implementation of
+// billy.Symlink
+type SymlinkSuite struct {
+	FS interface {
+		Basic
+		Symlink
+	}
+}
+
+func (s *SymlinkSuite) TestSymlink(c *C) {
+	err := util.WriteFile(s.FS, "file", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("file", "link")
+	c.Assert(err, IsNil)
+
+	fi, err := s.FS.Stat("link")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "link")
+}
+
+func (s *SymlinkSuite) TestSymlinkCrossDirs(c *C) {
+	err := util.WriteFile(s.FS, "foo/file", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("../foo/file", "bar/link")
+	c.Assert(err, IsNil)
+
+	fi, err := s.FS.Stat("bar/link")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "link")
+}
+
+func (s *SymlinkSuite) TestSymlinkNested(c *C) {
+	err := util.WriteFile(s.FS, "file", []byte("hello world!"), 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("file", "linkA")
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("linkA", "linkB")
+	c.Assert(err, IsNil)
+
+	fi, err := s.FS.Stat("linkB")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "linkB")
+	c.Assert(fi.Size(), Equals, int64(12))
+}
+
+func (s *SymlinkSuite) TestSymlinkWithNonExistentdTarget(c *C) {
+	err := s.FS.Symlink("file", "link")
+	c.Assert(err, IsNil)
+
+	_, err = s.FS.Stat("link")
+	c.Assert(os.IsNotExist(err), Equals, true)
+}
+
+func (s *SymlinkSuite) TestSymlinkWithExistingLink(c *C) {
+	err := util.WriteFile(s.FS, "link", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("file", "link")
+	c.Assert(err, Not(IsNil))
+}
+
+func (s *SymlinkSuite) TestOpenWithSymlinkToRelativePath(c *C) {
+	err := util.WriteFile(s.FS, "dir/file", []byte("foo"), 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("file", "dir/link")
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Open("dir/link")
+	c.Assert(err, IsNil)
+
+	all, err := ioutil.ReadAll(f)
+	c.Assert(err, IsNil)
+	c.Assert(string(all), Equals, "foo")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *SymlinkSuite) TestOpenWithSymlinkToAbsolutePath(c *C) {
+	err := util.WriteFile(s.FS, "dir/file", []byte("foo"), 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("/dir/file", "dir/link")
+	c.Assert(err, IsNil)
+
+	f, err := s.FS.Open("dir/link")
+	c.Assert(err, IsNil)
+
+	all, err := ioutil.ReadAll(f)
+	c.Assert(err, IsNil)
+	c.Assert(string(all), Equals, "foo")
+	c.Assert(f.Close(), IsNil)
+}
+
+func (s *SymlinkSuite) TestReadlink(c *C) {
+	err := util.WriteFile(s.FS, "file", nil, 0644)
+	c.Assert(err, IsNil)
+
+	_, err = s.FS.Readlink("file")
+	c.Assert(err, Not(IsNil))
+}
+
+func (s *SymlinkSuite) TestReadlinkWithRelativePath(c *C) {
+	err := util.WriteFile(s.FS, "dir/file", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("file", "dir/link")
+	c.Assert(err, IsNil)
+
+	oldname, err := s.FS.Readlink("dir/link")
+	c.Assert(err, IsNil)
+	c.Assert(oldname, Equals, "file")
+}
+
+func (s *SymlinkSuite) TestReadlinkWithAbsolutePath(c *C) {
+	err := util.WriteFile(s.FS, "dir/file", nil, 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("/dir/file", "dir/link")
+	c.Assert(err, IsNil)
+
+	oldname, err := s.FS.Readlink("dir/link")
+	c.Assert(err, IsNil)
+	c.Assert(oldname, Equals, expectedSymlinkTarget)
+}
+
+func (s *SymlinkSuite) TestReadlinkWithNonExistentTarget(c *C) {
+	err := s.FS.Symlink("file", "link")
+	c.Assert(err, IsNil)
+
+	oldname, err := s.FS.Readlink("link")
+	c.Assert(err, IsNil)
+	c.Assert(oldname, Equals, "file")
+}
+
+func (s *SymlinkSuite) TestReadlinkWithNonExistentLink(c *C) {
+	_, err := s.FS.Readlink("link")
+	c.Assert(os.IsNotExist(err), Equals, true)
+}
+
+func (s *SymlinkSuite) TestStatLink(c *C) {
+	util.WriteFile(s.FS, "foo/bar", []byte("foo"), customMode)
+	s.FS.Symlink("bar", "foo/qux")
+
+	fi, err := s.FS.Stat("foo/qux")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "qux")
+	c.Assert(fi.Size(), Equals, int64(3))
+	c.Assert(fi.Mode(), Equals, customMode)
+	c.Assert(fi.ModTime().IsZero(), Equals, false)
+	c.Assert(fi.IsDir(), Equals, false)
+}
+
+func (s *SymlinkSuite) TestLstat(c *C) {
+	util.WriteFile(s.FS, "foo/bar", []byte("foo"), customMode)
+
+	fi, err := s.FS.Lstat("foo/bar")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "bar")
+	c.Assert(fi.Size(), Equals, int64(3))
+	c.Assert(fi.Mode()&os.ModeSymlink != 0, Equals, false)
+	c.Assert(fi.ModTime().IsZero(), Equals, false)
+	c.Assert(fi.IsDir(), Equals, false)
+}
+
+func (s *SymlinkSuite) TestLstatLink(c *C) {
+	util.WriteFile(s.FS, "foo/bar", []byte("fosddddaaao"), customMode)
+	s.FS.Symlink("bar", "foo/qux")
+
+	fi, err := s.FS.Lstat("foo/qux")
+	c.Assert(err, IsNil)
+	c.Assert(fi.Name(), Equals, "qux")
+	c.Assert(fi.Mode()&os.ModeSymlink != 0, Equals, true)
+	c.Assert(fi.ModTime().IsZero(), Equals, false)
+	c.Assert(fi.IsDir(), Equals, false)
+}
+
+func (s *SymlinkSuite) TestRenameWithSymlink(c *C) {
+	err := s.FS.Symlink("file", "link")
+	c.Assert(err, IsNil)
+
+	err = s.FS.Rename("link", "newlink")
+	c.Assert(err, IsNil)
+
+	_, err = s.FS.Readlink("newlink")
+	c.Assert(err, IsNil)
+}
+
+func (s *SymlinkSuite) TestRemoveWithSymlink(c *C) {
+	err := util.WriteFile(s.FS, "file", []byte("foo"), 0644)
+	c.Assert(err, IsNil)
+
+	err = s.FS.Symlink("file", "link")
+	c.Assert(err, IsNil)
+
+	err = s.FS.Remove("link")
+	c.Assert(err, IsNil)
+
+	_, err = s.FS.Readlink("link")
+	c.Assert(os.IsNotExist(err), Equals, true)
+
+	_, err = s.FS.Stat("link")
+	c.Assert(os.IsNotExist(err), Equals, true)
+
+	_, err = s.FS.Stat("file")
+	c.Assert(err, IsNil)
+}
diff --git a/test/tempfile.go b/test/tempfile.go
new file mode 100644
index 0000000..5218794
--- /dev/null
+++ b/test/tempfile.go
@@ -0,0 +1,50 @@
+package test
+
+import (
+	"strings"
+
+	. "gopkg.in/check.v1"
+	"gopkg.in/src-d/go-billy.v3"
+)
+
+// TempFileSuite is a convenient test suite to validate any implementation of
+// billy.TempFile
+type TempFileSuite struct {
+	FS interface {
+		billy.Basic
+		billy.TempFile
+	}
+}
+
+func (s *TempFileSuite) TestTempFile(c *C) {
+	f, err := s.FS.TempFile("", "bar")
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+	c.Assert(strings.HasPrefix(f.Name(), "bar"), Equals, true)
+}
+
+func (s *TempFileSuite) TestTempFileWithPath(c *C) {
+	f, err := s.FS.TempFile("foo", "bar")
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+
+	c.Assert(strings.HasPrefix(f.Name(), s.FS.Join("foo", "bar")), Equals, true)
+}
+
+func (s *TempFileSuite) TestTempFileFullWithPath(c *C) {
+	f, err := s.FS.TempFile("/foo", "bar")
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+
+	c.Assert(strings.HasPrefix(f.Name(), s.FS.Join("foo", "bar")), Equals, true)
+}
+
+func (s *TempFileSuite) TestRemoveTempFile(c *C) {
+	f, err := s.FS.TempFile("test-dir", "test-prefix")
+	c.Assert(err, IsNil)
+
+	fn := f.Name()
+	c.Assert(err, IsNil)
+	c.Assert(f.Close(), IsNil)
+	c.Assert(s.FS.Remove(fn), IsNil)
+}
diff --git a/tmpoverlayfs/tmpfs.go b/tmpoverlayfs/tmpfs.go
deleted file mode 100644
index d209447..0000000
--- a/tmpoverlayfs/tmpfs.go
+++ /dev/null
@@ -1,200 +0,0 @@
-package tmpfs
-
-import (
-	"errors"
-	"io"
-	"os"
-	"path"
-
-	"fmt"
-
-	"gopkg.in/src-d/go-billy.v2"
-	"gopkg.in/src-d/go-billy.v2/subdirfs"
-)
-
-type tmpFs struct {
-	fs        billy.Filesystem
-	tmp       billy.Filesystem
-	tempFiles map[string]bool
-}
-
-// New creates a new filesystem wrapping up 'fs' and using 'tmp' for temporary
-// files. Any file created with TempFile will be created in 'tmp'. It will be
-// moved to 'fs' if Rename is called on it.
-//
-// This is particularly useful to provide TempFile for filesystems that do not
-// support it or where there is a performance penalty in doing so.
-func New(fs billy.Filesystem, tmp billy.Filesystem) billy.Filesystem {
-	return &tmpFs{
-		fs:        fs,
-		tmp:       tmp,
-		tempFiles: map[string]bool{},
-	}
-}
-
-func (t *tmpFs) Create(path string) (billy.File, error) {
-	if t.isTmpFile(path) {
-		return t.tmp.Create(path)
-	}
-
-	return t.fs.Create(path)
-}
-
-func (t *tmpFs) Open(path string) (billy.File, error) {
-	if t.isTmpFile(path) {
-		return t.tmp.Open(path)
-	}
-
-	return t.fs.Open(path)
-
-}
-
-func (t *tmpFs) OpenFile(p string, flag int, mode os.FileMode) (
-	billy.File, error) {
-	if t.isTmpFile(p) {
-		return t.tmp.OpenFile(p, flag, mode)
-	}
-
-	return t.fs.OpenFile(p, flag, mode)
-}
-
-func (t *tmpFs) ReadDir(p string) ([]billy.FileInfo, error) {
-	return t.fs.ReadDir(p)
-}
-
-func (t *tmpFs) Join(elem ...string) string {
-	return t.fs.Join(elem...)
-}
-
-func (t *tmpFs) Dir(p string) billy.Filesystem {
-	return subdirfs.New(t, p)
-}
-
-func (t *tmpFs) MkdirAll(filename string, perm os.FileMode) error {
-	return t.fs.MkdirAll(filename, perm)
-}
-
-func (t *tmpFs) Base() string {
-	return t.fs.Base()
-}
-
-func (t *tmpFs) TempFile(dir string, prefix string) (billy.File, error) {
-	tmpFile, err := t.tmp.TempFile(dir, prefix)
-	if err != nil {
-		return nil, err
-	}
-
-	t.tempFiles[tmpFile.Filename()] = true
-
-	return tmpFile, nil
-}
-
-func (t *tmpFs) Rename(from, to string) error {
-	if t.isTmpFile(to) {
-		return errors.New("cannot rename to temp file")
-	}
-
-	if t.isTmpFile(from) {
-		err := copyPath(t.tmp, t.fs, from, to)
-		if err != nil {
-			return err
-		}
-
-		if err := t.tmp.Remove(from); err != nil {
-			return err
-		}
-
-		t.removeTempReference(from)
-
-		return nil
-	}
-
-	return t.fs.Rename(from, to)
-}
-
-func (t *tmpFs) Remove(path string) error {
-	if t.isTmpFile(path) {
-		if err := t.tmp.Remove(path); err != nil {
-			return err
-		}
-
-		t.removeTempReference(path)
-
-		return nil
-	}
-
-	return t.fs.Remove(path)
-}
-
-// Symlink creates a symbolic-link from link to target. Using a temporal file
-// as target is not allowed.
-func (t *tmpFs) Symlink(target, link string) error {
-	if t.isTmpFile(target) {
-		return fmt.Errorf("links to temporal file are not supported")
-	}
-
-	if t.isTmpFile(link) {
-		return os.ErrExist
-	}
-
-	return t.fs.Symlink(target, link)
-}
-
-func (t *tmpFs) Readlink(link string) (string, error) {
-	return t.fs.Readlink(link)
-}
-
-func (t *tmpFs) Stat(path string) (billy.FileInfo, error) {
-	if t.isTmpFile(path) {
-		return t.tmp.Stat(path)
-	}
-
-	return t.fs.Stat(path)
-}
-
-func (t *tmpFs) Lstat(path string) (billy.FileInfo, error) {
-	if t.isTmpFile(path) {
-		return t.tmp.Lstat(path)
-	}
-
-	return t.fs.Lstat(path)
-}
-
-func (t *tmpFs) isTmpFile(p string) bool {
-	p = path.Clean(p)
-	_, ok := t.tempFiles[p]
-	return ok
-}
-
-func (t *tmpFs) removeTempReference(p string) {
-	p = path.Clean(p)
-	delete(t.tempFiles, p)
-}
-
-// copyPath copies a file across filesystems.
-func copyPath(src billy.Filesystem, dst billy.Filesystem,
-	srcPath string, dstPath string) error {
-
-	dstFile, err := dst.Create(dstPath)
-	if err != nil {
-		return err
-	}
-
-	srcFile, err := src.Open(srcPath)
-	if err != nil {
-		return nil
-	}
-
-	_, err = io.Copy(dstFile, srcFile)
-	if err != nil {
-		return nil
-	}
-
-	err = dstFile.Close()
-	if err != nil {
-		_ = srcFile.Close()
-		return err
-	}
-
-	return srcFile.Close()
-}
diff --git a/tmpoverlayfs/tmpfs_specific_test.go b/tmpoverlayfs/tmpfs_specific_test.go
deleted file mode 100644
index c6f068d..0000000
--- a/tmpoverlayfs/tmpfs_specific_test.go
+++ /dev/null
@@ -1,247 +0,0 @@
-package tmpfs
-
-import (
-	"io"
-	"io/ioutil"
-	stdos "os"
-	"path/filepath"
-
-	"gopkg.in/src-d/go-billy.v2"
-	"gopkg.in/src-d/go-billy.v2/osfs"
-
-	. "gopkg.in/check.v1"
-)
-
-type SpecificFilesystemSuite struct {
-	src     billy.Filesystem
-	tmp     billy.Filesystem
-	srcPath string
-	tmpPath string
-}
-
-var _ = Suite(&SpecificFilesystemSuite{})
-
-func (s *SpecificFilesystemSuite) SetUpTest(c *C) {
-	s.srcPath = c.MkDir()
-	s.tmpPath = c.MkDir()
-	s.src = osfs.New(s.srcPath)
-	s.tmp = osfs.New(s.tmpPath)
-}
-
-func (s *SpecificFilesystemSuite) TestTempFileInTmpFs(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	filename := f.Filename()
-	c.Assert(f.Close(), IsNil)
-
-	_, err = stdos.Stat(filepath.Join(s.srcPath, filename))
-	c.Assert(stdos.IsNotExist(err), Equals, true)
-
-	_, err = stdos.Stat(filepath.Join(s.tmpPath, filename))
-	c.Assert(err, IsNil)
-}
-
-func (s *SpecificFilesystemSuite) TestNonTempFileInSrcFs(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.Create("foo")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	c.Assert(f.Close(), IsNil)
-
-	_, err = stdos.Stat(filepath.Join(s.srcPath, "foo"))
-	c.Assert(err, IsNil)
-
-	_, err = stdos.Stat(filepath.Join(s.tmpPath, "foo"))
-	c.Assert(stdos.IsNotExist(err), Equals, true)
-}
-
-func (s *SpecificFilesystemSuite) TestTempFileCanBeReopened(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	n, err := f.Write([]byte("foo"))
-	c.Assert(err, IsNil)
-	c.Assert(n, Equals, 3)
-
-	filename := f.Filename()
-	c.Assert(f.Close(), IsNil)
-
-	f, err = tmpFs.Open(filename)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, filename)
-
-	content, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(string(content), Equals, "foo")
-
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *SpecificFilesystemSuite) TestTempFileCanBeReopenedByOpenFile(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	n, err := f.Write([]byte("foo"))
-	c.Assert(err, IsNil)
-	c.Assert(n, Equals, 3)
-
-	filename := f.Filename()
-	c.Assert(f.Close(), IsNil)
-
-	f, err = tmpFs.OpenFile(filename, stdos.O_RDONLY, 0)
-	c.Assert(err, IsNil)
-	c.Assert(f.Filename(), Equals, filename)
-
-	content, err := ioutil.ReadAll(f)
-	c.Assert(err, IsNil)
-	c.Assert(string(content), Equals, "foo")
-
-	c.Assert(f.Close(), IsNil)
-}
-
-func (s *SpecificFilesystemSuite) TestStatTempFile(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	tempFilename := f.Filename()
-	c.Assert(f.Close(), IsNil)
-
-	fi, err := tmpFs.Stat(tempFilename)
-	c.Assert(err, IsNil)
-	c.Assert(fi, NotNil)
-}
-
-func (s *SpecificFilesystemSuite) TestSymlinkTmpFile(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	tempFilename := f.Filename()
-	c.Assert(f.Close(), IsNil)
-
-	err = tmpFs.Symlink(tempFilename, "foo")
-	c.Assert(err, NotNil)
-}
-
-func (s *SpecificFilesystemSuite) TestSymlinkOverTmpFile(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	tempFilename := f.Filename()
-	c.Assert(f.Close(), IsNil)
-
-	err = tmpFs.Symlink("foo", tempFilename)
-	c.Assert(err, NotNil)
-}
-
-func (s *SpecificFilesystemSuite) TestRenameFromTempFile(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	tempFilename := f.Filename()
-	c.Assert(f.Close(), IsNil)
-
-	err = tmpFs.Rename(tempFilename, "foo")
-	c.Assert(err, IsNil)
-
-	_, err = s.src.Stat("foo")
-	c.Assert(err, IsNil)
-
-	_, err = s.tmp.Stat("foo")
-	c.Assert(err, NotNil)
-
-	_, err = s.tmp.Stat(tempFilename)
-	c.Assert(err, NotNil)
-}
-
-func (s *SpecificFilesystemSuite) TestCannotRenameToTempFile(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	tempFilename := f.Filename()
-	c.Assert(f.Close(), IsNil)
-
-	f, err = tmpFs.Create("foo")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-	c.Assert(f.Close(), IsNil)
-
-	err = tmpFs.Rename("foo", tempFilename)
-	c.Assert(err, NotNil)
-}
-
-func (s *SpecificFilesystemSuite) TestRemoveTempFile(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	tempFilename := f.Filename()
-	c.Assert(f.Close(), IsNil)
-
-	err = tmpFs.Remove(tempFilename)
-	c.Assert(err, IsNil)
-
-	_, err = s.tmp.Stat(tempFilename)
-	c.Assert(err, NotNil)
-}
-
-func (s *SpecificFilesystemSuite) TestCreateTempFile(c *C) {
-	tmpFs := New(s.src, s.tmp)
-	c.Assert(tmpFs, NotNil)
-
-	f, err := tmpFs.TempFile("test-dir", "test-prefix")
-	c.Assert(err, IsNil)
-	c.Assert(f, NotNil)
-
-	n, err := f.Write([]byte("TEST"))
-
-	tempFilename := f.Filename()
-	c.Assert(f.Close(), IsNil)
-
-	createdFile, err := tmpFs.Create(tempFilename)
-	c.Assert(err, IsNil)
-	c.Assert(createdFile, NotNil)
-
-	bRead := make([]byte, 4)
-	n, err = createdFile.Read(bRead)
-	c.Assert(n, Equals, 0)
-	c.Assert(err, Equals, io.EOF)
-}
diff --git a/tmpoverlayfs/tmpfs_test.go b/tmpoverlayfs/tmpfs_test.go
deleted file mode 100644
index 5a88773..0000000
--- a/tmpoverlayfs/tmpfs_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package tmpfs
-
-import (
-	"io/ioutil"
-	stdos "os"
-	"testing"
-
-	"gopkg.in/src-d/go-billy.v2"
-	"gopkg.in/src-d/go-billy.v2/osfs"
-	"gopkg.in/src-d/go-billy.v2/test"
-
-	. "gopkg.in/check.v1"
-)
-
-func Test(t *testing.T) { TestingT(t) }
-
-type FilesystemSuite struct {
-	test.FilesystemSuite
-	cfs     billy.Filesystem
-	path    string
-	pathTmp string
-}
-
-var _ = Suite(&FilesystemSuite{})
-
-func (s *FilesystemSuite) SetUpTest(c *C) {
-	s.path, _ = ioutil.TempDir(stdos.TempDir(), "go-billy-tmpfs-test")
-	fs := osfs.New(s.path)
-
-	s.pathTmp, _ = ioutil.TempDir(stdos.TempDir(), "go-billy-tmpfs-test-tmp")
-	fsTmp := osfs.New(s.pathTmp)
-
-	s.cfs = New(fs, fsTmp)
-	s.FilesystemSuite.FS = s.cfs
-}
-
-func (s *FilesystemSuite) TearDownTest(c *C) {
-	_, err := ioutil.ReadDir(s.path)
-	c.Assert(err, IsNil)
-
-	err = stdos.RemoveAll(s.path)
-	c.Assert(err, IsNil)
-
-	err = stdos.RemoveAll(s.pathTmp)
-	c.Assert(err, IsNil)
-}
diff --git a/util/util.go b/util/util.go
new file mode 100644
index 0000000..e476aa3
--- /dev/null
+++ b/util/util.go
@@ -0,0 +1,165 @@
+package util
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"sync/atomic"
+	"time"
+
+	"gopkg.in/src-d/go-billy.v3"
+)
+
+// RemoveAll removes path and any children it contains. It removes everything it
+// can but returns the first error it encounters. If the path does not exist,
+// RemoveAll returns nil (no error).
+func RemoveAll(fs billy.Basic, path string) error {
+	fs, path = getUnderlyingAndPath(fs, path)
+
+	if r, ok := fs.(removerAll); ok {
+		return r.RemoveAll(path)
+	}
+
+	return removeAll(fs, path)
+}
+
+type removerAll interface {
+	RemoveAll(string) error
+}
+
+func removeAll(fs billy.Basic, path string) error {
+	// This implementation is adapted from os.RemoveAll.
+
+	// Simple case: if Remove works, we're done.
+	err := fs.Remove(path)
+	if err == nil || os.IsNotExist(err) {
+		return nil
+	}
+
+	// Otherwise, is this a directory we need to recurse into?
+	dir, serr := fs.Stat(path)
+	if serr != nil {
+		if os.IsNotExist(serr) {
+			return nil
+		}
+
+		return serr
+	}
+
+	if !dir.IsDir() {
+		// Not a directory; return the error from Remove.
+		return err
+	}
+
+	dirfs, ok := fs.(billy.Dir)
+	if !ok {
+		return billy.ErrNotSupported
+	}
+
+	// Directory.
+	fis, err := dirfs.ReadDir(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			// Race. It was deleted between the Lstat and Open.
+			// Return nil per RemoveAll's docs.
+			return nil
+		}
+
+		return err
+	}
+
+	// Remove contents & return first error.
+	err = nil
+	for _, fi := range fis {
+		cpath := fs.Join(path, fi.Name())
+		err1 := removeAll(fs, cpath)
+		if err == nil {
+			err = err1
+		}
+	}
+
+	// Remove directory.
+	err1 := fs.Remove(path)
+	if err1 == nil || os.IsNotExist(err1) {
+		return nil
+	}
+
+	if err == nil {
+		err = err1
+	}
+
+	return err
+
+}
+
+// WriteFile writes data to a file named by filename in the given filesystem.
+// If the file does not exist, WriteFile creates it with permissions perm;
+// otherwise WriteFile truncates it before writing.
+func WriteFile(fs billy.Basic, filename string, data []byte, perm os.FileMode) error {
+	f, err := fs.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
+	if err != nil {
+		return err
+	}
+
+	n, err := f.Write(data)
+	if err == nil && n < len(data) {
+		err = io.ErrShortWrite
+	}
+
+	if err1 := f.Close(); err == nil {
+		err = err1
+	}
+
+	return err
+}
+
+var (
+	MaxTempFiles int32 = 1024 * 4
+	tempCount    int32
+)
+
+// TempFile creates a new temporary file in the directory dir with a name
+// beginning with prefix, opens the file for reading and writing, and returns
+// the resulting *os.File. If dir is the empty string, TempFile uses the default
+// directory for temporary files (see os.TempDir).
+//
+// Multiple programs calling TempFile simultaneously will not choose the same
+// file. The caller can use f.Name() to find the pathname of the file.
+//
+// It is the caller's responsibility to remove the file when no longer needed.
+func TempFile(fs billy.Basic, dir, prefix string) (billy.File, error) {
+	var fullpath string
+	for {
+		if tempCount >= MaxTempFiles {
+			return nil, errors.New("max. number of tempfiles reached")
+		}
+
+		fullpath = getTempFilename(fs, dir, prefix)
+		break
+	}
+
+	return fs.Create(fullpath)
+}
+
+func getTempFilename(fs billy.Basic, dir, prefix string) string {
+	atomic.AddInt32(&tempCount, 1)
+	filename := fmt.Sprintf("%s_%d_%d", prefix, tempCount, time.Now().UnixNano())
+	return fs.Join(dir, filename)
+}
+
+type underlying interface {
+	Underlying() billy.Basic
+}
+
+func getUnderlyingAndPath(fs billy.Basic, path string) (billy.Basic, string) {
+	u, ok := fs.(underlying)
+	if !ok {
+		return fs, path
+	}
+	if ch, ok := fs.(billy.Chroot); ok {
+		path = fs.Join(ch.Root(), path)
+	}
+
+	return u.Underlying(), path
+}
diff --git a/utils.go b/utils.go
deleted file mode 100644
index 110f954..0000000
--- a/utils.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package billy
-
-import (
-	"io"
-	"os"
-)
-
-// RemoveAll removes path and any children it contains.
-// It removes everything it can but returns the first error
-// it encounters. If the path does not exist, RemoveAll
-// returns nil (no error).
-func RemoveAll(fs Filesystem, path string) error {
-	r, ok := fs.(removerAll)
-	if ok {
-		return r.RemoveAll(path)
-	}
-
-	return removeAll(fs, path)
-}
-
-func removeAll(fs Filesystem, path string) error {
-	// This implementation is adapted from os.RemoveAll.
-
-	// Simple case: if Remove works, we're done.
-	err := fs.Remove(path)
-	if err == nil || os.IsNotExist(err) {
-		return nil
-	}
-
-	// Otherwise, is this a directory we need to recurse into?
-	dir, serr := fs.Stat(path)
-	if serr != nil {
-		if os.IsNotExist(serr) {
-			return nil
-		}
-
-		return serr
-	}
-
-	if !dir.IsDir() {
-		// Not a directory; return the error from Remove.
-		return err
-	}
-
-	// Directory.
-	fis, err := fs.ReadDir(path)
-	if err != nil {
-		if os.IsNotExist(err) {
-			// Race. It was deleted between the Lstat and Open.
-			// Return nil per RemoveAll's docs.
-			return nil
-		}
-
-		return err
-	}
-
-	// Remove contents & return first error.
-	err = nil
-	for _, fi := range fis {
-		cpath := fs.Join(path, fi.Name())
-		err1 := removeAll(fs, cpath)
-		if err == nil {
-			err = err1
-		}
-	}
-
-	// Remove directory.
-	err1 := fs.Remove(path)
-	if err1 == nil || os.IsNotExist(err1) {
-		return nil
-	}
-
-	if err == nil {
-		err = err1
-	}
-
-	return err
-
-}
-
-// WriteFile writes data to a file named by filename in the given filesystem.
-// If the file does not exist, WriteFile creates it with permissions perm;
-// otherwise WriteFile truncates it before writing.
-func WriteFile(fs Filesystem, filename string, data []byte, perm os.FileMode) error {
-	f, err := fs.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
-	if err != nil {
-		return err
-	}
-
-	n, err := f.Write(data)
-	if err == nil && n < len(data) {
-		err = io.ErrShortWrite
-	}
-
-	if err1 := f.Close(); err == nil {
-		err = err1
-	}
-
-	return err
-}