From 94e8fc12ea5756a130e7ac9ed67ddd519b5f3a22 Mon Sep 17 00:00:00 2001
From: Jakob Unterwurzacher
Date: Sat, 15 Aug 2020 17:31:25 +0200
Subject: v2api/reverse: finish -exclude

Tests pass now.
---
 internal/fusefrontend_reverse/excluder.go      |  6 +--
 internal/fusefrontend_reverse/excluder_test.go |  8 ----
 internal/fusefrontend_reverse/node.go          | 33 ++++++++---------
 internal/fusefrontend_reverse/node_dir_ops.go  | 17 ++++-----
 internal/fusefrontend_reverse/node_helpers.go  | 51 +++++++++++++++++++-------
 internal/fusefrontend_reverse/root_node.go     | 37 +++++++++++++++++--
 internal/fusefrontend_reverse/rpath.go         | 13 +++----
 7 files changed, 104 insertions(+), 61 deletions(-)

(limited to 'internal/fusefrontend_reverse')

diff --git a/internal/fusefrontend_reverse/excluder.go b/internal/fusefrontend_reverse/excluder.go
index b6cb961..8a13fa7 100644
--- a/internal/fusefrontend_reverse/excluder.go
+++ b/internal/fusefrontend_reverse/excluder.go
@@ -2,6 +2,7 @@ package fusefrontend_reverse
 
 import (
 	"io/ioutil"
+	"log"
 	"os"
 	"strings"
 
@@ -15,12 +16,9 @@ import (
 // prepareExcluder creates an object to check if paths are excluded
 // based on the patterns specified in the command line.
 func prepareExcluder(args fusefrontend.Args) *ignore.GitIgnore {
-	if len(args.Exclude) == 0 && len(args.ExcludeWildcard) == 0 && len(args.ExcludeFrom) == 0 {
-		return nil
-	}
 	patterns := getExclusionPatterns(args)
 	if len(patterns) == 0 {
-		panic(patterns)
+		log.Panic(patterns)
 	}
 	excluder, err := ignore.CompileIgnoreLines(patterns...)
 	if err != nil {
diff --git a/internal/fusefrontend_reverse/excluder_test.go b/internal/fusefrontend_reverse/excluder_test.go
index 47b430a..d6cfef3 100644
--- a/internal/fusefrontend_reverse/excluder_test.go
+++ b/internal/fusefrontend_reverse/excluder_test.go
@@ -9,14 +9,6 @@ import (
 	"github.com/rfjakob/gocryptfs/internal/fusefrontend"
 )
 
-func TestShouldNoCreateExcluderIfNoPattersWereSpecified(t *testing.T) {
-	var args fusefrontend.Args
-	excluder := prepareExcluder(args)
-	if excluder != nil {
-		t.Error("Should not have created excluder")
-	}
-}
-
 func TestShouldPrefixExcludeValuesWithSlash(t *testing.T) {
 	var args fusefrontend.Args
 	args.Exclude = []string{"file1", "dir1/file2.txt"}
diff --git a/internal/fusefrontend_reverse/node.go b/internal/fusefrontend_reverse/node.go
index deaf953..de1ee49 100644
--- a/internal/fusefrontend_reverse/node.go
+++ b/internal/fusefrontend_reverse/node.go
@@ -26,8 +26,7 @@ type Node struct {
 
 // Lookup - FUSE call for discovering a file.
 func (n *Node) Lookup(ctx context.Context, cName string, out *fuse.EntryOut) (ch *fs.Inode, errno syscall.Errno) {
-	dirfd := int(-1)
-	pName := ""
+	var d *dirfdPlus
 	t := n.lookupFileType(cName)
 	if t == typeDiriv {
 		// gocryptfs.diriv
@@ -40,14 +39,15 @@ func (n *Node) Lookup(ctx context.Context, cName string, out *fuse.EntryOut) (ch
 		return n.lookupConf(ctx, out)
 	} else if t == typeReal {
 		// real file
-		dirfd, pName, errno = n.prepareAtSyscall(cName)
+		d, errno = n.prepareAtSyscall(cName)
+		//fmt.Printf("Lookup: prepareAtSyscall -> d=%#v, errno=%d\n", d, errno)
 		if errno != 0 {
 			return
 		}
-		defer syscall.Close(dirfd)
+		defer syscall.Close(d.dirfd)
 	}
 	// Get device number and inode number into `st`
-	st, err := syscallcompat.Fstatat2(dirfd, pName, unix.AT_SYMLINK_NOFOLLOW)
+	st, err := syscallcompat.Fstatat2(d.dirfd, d.pName, unix.AT_SYMLINK_NOFOLLOW)
 	if err != nil {
 		return nil, fs.ToErrno(err)
 	}
@@ -55,7 +55,7 @@ func (n *Node) Lookup(ctx context.Context, cName string, out *fuse.EntryOut) (ch
 	ch = n.newChild(ctx, st, out)
 	// Translate ciphertext size in `out.Attr.Size` to plaintext size
 	if t == typeReal {
-		n.translateSize(dirfd, cName, pName, &out.Attr)
+		n.translateSize(d.dirfd, cName, d.pName, &out.Attr)
 	}
 	return ch, 0
 }
@@ -69,13 +69,13 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut)
 		return f.(fs.FileGetattrer).Getattr(ctx, out)
 	}
 
-	dirfd, pName, errno := n.prepareAtSyscall("")
+	d, errno := n.prepareAtSyscall("")
 	if errno != 0 {
 		return
 	}
-	defer syscall.Close(dirfd)
+	defer syscall.Close(d.dirfd)
 
-	st, err := syscallcompat.Fstatat2(dirfd, pName, unix.AT_SYMLINK_NOFOLLOW)
+	st, err := syscallcompat.Fstatat2(d.dirfd, d.pName, unix.AT_SYMLINK_NOFOLLOW)
 	if err != nil {
 		return fs.ToErrno(err)
 	}
@@ -87,7 +87,7 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut)
 
 	// Translate ciphertext size in `out.Attr.Size` to plaintext size
 	cName := filepath.Base(n.Path())
-	n.translateSize(dirfd, cName, pName, &out.Attr)
+	n.translateSize(d.dirfd, cName, d.pName, &out.Attr)
 
 	if rn.args.ForceOwner != nil {
 		out.Owner = *rn.args.ForceOwner
@@ -99,27 +99,26 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut)
 //
 // Symlink-safe through openBackingDir() + Readlinkat().
 func (n *Node) Readlink(ctx context.Context) (out []byte, errno syscall.Errno) {
-	dirfd, pName, errno := n.prepareAtSyscall("")
+	d, errno := n.prepareAtSyscall("")
 	if errno != 0 {
 		return
 	}
-	defer syscall.Close(dirfd)
+	defer syscall.Close(d.dirfd)
 
-	cName := filepath.Base(n.Path())
-	return n.readlink(dirfd, cName, pName)
+	return n.readlink(d.dirfd, d.cName, d.pName)
 }
 
 // Open - FUSE call. Open already-existing file.
 //
 // Symlink-safe through Openat().
 func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
-	dirfd, pName, errno := n.prepareAtSyscall("")
+	d, errno := n.prepareAtSyscall("")
 	if errno != 0 {
 		return
 	}
-	defer syscall.Close(dirfd)
+	defer syscall.Close(d.dirfd)
 
-	fd, err := syscallcompat.Openat(dirfd, pName, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0)
+	fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0)
 	if err != nil {
 		errno = fs.ToErrno(err)
 		return
diff --git a/internal/fusefrontend_reverse/node_dir_ops.go b/internal/fusefrontend_reverse/node_dir_ops.go
index 5ec1e95..22f8122 100644
--- a/internal/fusefrontend_reverse/node_dir_ops.go
+++ b/internal/fusefrontend_reverse/node_dir_ops.go
@@ -23,15 +23,15 @@ import (
 // This function is symlink-safe through use of openBackingDir() and
 // ReadDirIVAt().
 func (n *Node) Readdir(ctx context.Context) (stream fs.DirStream, errno syscall.Errno) {
-	dirfd, cName, errno := n.prepareAtSyscall("")
+	d, errno := n.prepareAtSyscall("")
 	if errno != 0 {
 		return
 	}
-	defer syscall.Close(dirfd)
+	defer syscall.Close(d.dirfd)
 
 	// Read plaintext directory
 	var entries []fuse.DirEntry
-	fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
+	fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
 	if err != nil {
 		return nil, fs.ToErrno(err)
 	}
@@ -42,21 +42,20 @@ func (n *Node) Readdir(ctx context.Context) (stream fs.DirStream, errno syscall.
 	}
 
 	rn := n.rootNode()
+
+	// Filter out excluded entries
+	entries = rn.excludeDirEntries(d, entries)
+
 	if rn.args.PlaintextNames {
 		return n.readdirPlaintextnames(entries)
 	}
 
-	// Filter out excluded entries
-	//TODO
-	//entries = rfs.excludeDirEntries(relPath, entries)
-
 	// Virtual files: at least one gocryptfs.diriv file
 	virtualFiles := []fuse.DirEntry{
 		{Mode: virtualFileMode, Name: nametransform.DirIVFilename},
 	}
 
-	cipherPath := n.Path()
-	dirIV := pathiv.Derive(cipherPath, pathiv.PurposeDirIV)
+	dirIV := pathiv.Derive(d.cPath, pathiv.PurposeDirIV)
 	// Encrypt names
 	for i := range entries {
 		var cName string
diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go
index 76ddd42..92f6a87 100644
--- a/internal/fusefrontend_reverse/node_helpers.go
+++ b/internal/fusefrontend_reverse/node_helpers.go
@@ -47,6 +47,20 @@ func (n *Node) rootNode() *RootNode {
 	return n.Root().Operations().(*RootNode)
 }
 
+// dirfdPlus gets filled out as we gather information about a node
+type dirfdPlus struct {
+	// fd to the directory, opened with O_DIRECTORY|O_PATH
+	dirfd int
+	// Relative plaintext path
+	pPath string
+	// Plaintext basename: filepath.Base(pPath)
+	pName string
+	// Relative ciphertext path
+	cPath string
+	// Ciphertext basename: filepath.Base(cPath)
+	cName string
+}
+
 // prepareAtSyscall returns a (dirfd, cName) pair that can be used
 // with the "___at" family of system calls (openat, fstatat, unlinkat...) to
 // access the backing encrypted directory.
@@ -54,16 +68,23 @@ func (n *Node) rootNode() *RootNode {
 // If you pass a `child` file name, the (dirfd, cName) pair will refer to
 // a child of this node.
 // If `child` is empty, the (dirfd, cName) pair refers to this node itself.
-func (n *Node) prepareAtSyscall(child string) (dirfd int, pName string, errno syscall.Errno) {
-	p := n.Path()
+func (n *Node) prepareAtSyscall(child string) (d *dirfdPlus, errno syscall.Errno) {
+	cPath := n.Path()
 	if child != "" {
-		p = filepath.Join(p, child)
+		cPath = filepath.Join(cPath, child)
 	}
 	rn := n.rootNode()
-	dirfd, pName, err := rn.openBackingDir(p)
+	dirfd, pPath, err := rn.openBackingDir(cPath)
 	if err != nil {
 		errno = fs.ToErrno(err)
 	}
+	d = &dirfdPlus{
+		dirfd: dirfd,
+		pPath: pPath,
+		pName: filepath.Base(pPath),
+		cPath: cPath,
+		cName: filepath.Base(cPath),
+	}
 	return
 }
 
@@ -91,28 +112,32 @@ func (n *Node) isRoot() bool {
 }
 
 func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fuse.EntryOut) (ch *fs.Inode, errno syscall.Errno) {
-	dirfd, pName1, errno := n.prepareAtSyscall("")
+	d, errno := n.prepareAtSyscall("")
 	if errno != 0 {
 		return
 	}
-	defer syscall.Close(dirfd)
+	defer syscall.Close(d.dirfd)
 
 	// Find the file the gocryptfs.longname.XYZ.name file belongs to in the
 	// directory listing
-	fd, err := syscallcompat.Openat(dirfd, pName1, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
+	fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
 	if err != nil {
 		errno = fs.ToErrno(err)
 		return
 	}
 	defer syscall.Close(fd)
-	diriv := pathiv.Derive(n.Path(), pathiv.PurposeDirIV)
+	diriv := pathiv.Derive(d.cPath, pathiv.PurposeDirIV)
 	rn := n.rootNode()
 	pName, cFullname, errno := rn.findLongnameParent(fd, diriv, nameFile)
 	if errno != 0 {
 		return
 	}
+	if rn.isExcludedPlain(filepath.Join(d.cPath, pName)) {
+		errno = syscall.EPERM
+		return
+	}
 	// Get attrs from parent file
-	st, err := syscallcompat.Fstatat2(dirfd, pName, unix.AT_SYMLINK_NOFOLLOW)
+	st, err := syscallcompat.Fstatat2(fd, pName, unix.AT_SYMLINK_NOFOLLOW)
 	if err != nil {
 		errno = fs.ToErrno(err)
 		return
@@ -132,17 +157,17 @@ func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fus
 
 // lookupDiriv returns a new Inode for a gocryptfs.diriv file inside `n`.
 func (n *Node) lookupDiriv(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inode, errno syscall.Errno) {
-	dirfd, pName, errno := n.prepareAtSyscall("")
+	d, errno := n.prepareAtSyscall("")
 	if errno != 0 {
 		return
 	}
-	defer syscall.Close(dirfd)
-	st, err := syscallcompat.Fstatat2(dirfd, pName, unix.AT_SYMLINK_NOFOLLOW)
+	defer syscall.Close(d.dirfd)
+	st, err := syscallcompat.Fstatat2(d.dirfd, d.pName, unix.AT_SYMLINK_NOFOLLOW)
 	if err != nil {
 		errno = fs.ToErrno(err)
 		return
 	}
-	content := pathiv.Derive(n.Path(), pathiv.PurposeDirIV)
+	content := pathiv.Derive(d.cPath, pathiv.PurposeDirIV)
 	var vf *VirtualMemNode
 	vf, errno = n.newVirtualMemNode(content, st, inoTagDirIV)
 	if errno != 0 {
diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go
index 4346306..b7a259a 100644
--- a/internal/fusefrontend_reverse/root_node.go
+++ b/internal/fusefrontend_reverse/root_node.go
@@ -2,12 +2,16 @@ package fusefrontend_reverse
 
 import (
 	"log"
+	"path/filepath"
 	"strings"
 	"syscall"
 
+	"github.com/rfjakob/gocryptfs/internal/tlog"
+
 	"golang.org/x/sys/unix"
 
 	"github.com/hanwen/go-fuse/v2/fs"
+	"github.com/hanwen/go-fuse/v2/fuse"
 
 	"github.com/rfjakob/gocryptfs/internal/contentenc"
 	"github.com/rfjakob/gocryptfs/internal/fusefrontend"
@@ -28,7 +32,7 @@ type RootNode struct {
 	// Content encryption helper
 	contentEnc *contentenc.ContentEnc
 	// Tests whether a path is excluded (hidden) from the user. Used by -exclude.
-	excluder *ignore.GitIgnore
+	excluder ignore.IgnoreParser
 	// inoMap translates inode numbers from different devices to unique inode
 	// numbers.
 	inoMap *inomap.InoMap
@@ -38,17 +42,23 @@ type RootNode struct {
 // In this case (reverse mode) the backing directory is plain-text and
 // ReverseFS provides an encrypted view.
 func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *RootNode {
-	return &RootNode{
+	rn := &RootNode{
 		args:          args,
 		nameTransform: n,
 		contentEnc:    c,
 		inoMap:        inomap.New(),
-		excluder:      prepareExcluder(args),
 	}
+	if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 {
+		rn.excluder = prepareExcluder(args)
+	}
+	return rn
 }
 
 // You can pass either gocryptfs.longname.XYZ.name or gocryptfs.longname.XYZ.
 func (rn *RootNode) findLongnameParent(fd int, diriv []byte, longname string) (pName string, cFullName string, errno syscall.Errno) {
+	defer func() {
+		tlog.Debug.Printf("findLongnameParent: %d %x %q -> %q %q %d\n", fd, diriv, longname, pName, cFullName, errno)
+	}()
 	if strings.HasSuffix(longname, nametransform.LongNameSuffix) {
 		longname = nametransform.RemoveLongNameSuffix(longname)
 	}
@@ -84,3 +94,24 @@ func (rn *RootNode) findLongnameParent(fd int, diriv []byte, longname string) (p
 func (rn *RootNode) isExcludedPlain(pPath string) bool {
 	return rn.excluder != nil && rn.excluder.MatchesPath(pPath)
 }
+
+// excludeDirEntries filters out directory entries that are "-exclude"d.
+// pDir is the relative plaintext path to the directory these entries are
+// from. The entries should be plaintext files.
+func (rn *RootNode) excludeDirEntries(d *dirfdPlus, entries []fuse.DirEntry) (filtered []fuse.DirEntry) {
+	if rn.excluder == nil {
+		return entries
+	}
+	filtered = make([]fuse.DirEntry, 0, len(entries))
+	for _, entry := range entries {
+		// filepath.Join handles the case of pDir="" correctly:
+		// Join("", "foo") -> "foo". This does not: pDir + "/" + name"
+		p := filepath.Join(d.pPath, entry.Name)
+		if rn.isExcludedPlain(p) {
+			// Skip file
+			continue
+		}
+		filtered = append(filtered, entry)
+	}
+	return filtered
+}
diff --git a/internal/fusefrontend_reverse/rpath.go b/internal/fusefrontend_reverse/rpath.go
index 1e44638..f29bbf5 100644
--- a/internal/fusefrontend_reverse/rpath.go
+++ b/internal/fusefrontend_reverse/rpath.go
@@ -100,25 +100,24 @@ func (rn *RootNode) decryptPath(cPath string) (string, error) {
 // and returns the fd to the directory and the decrypted name of the
 // target file. The fd/name pair is intended for use with fchownat and
 // friends.
-func (rn *RootNode) openBackingDir(cPath string) (dirfd int, pName string, err error) {
+func (rn *RootNode) openBackingDir(cPath string) (dirfd int, pPath string, err error) {
 	defer func() {
-		tlog.Debug.Printf("openBackingDir %q -> %d %q %v\n", cPath, dirfd, pName, err)
+		tlog.Debug.Printf("openBackingDir %q -> %d %q %v\n", cPath, dirfd, pPath, err)
 	}()
 	dirfd = -1
-	pRelPath, err := rn.decryptPath(cPath)
+	pPath, err = rn.decryptPath(cPath)
 	if err != nil {
 		return
 	}
-	if rn.isExcludedPlain(pRelPath) {
+	if rn.isExcludedPlain(pPath) {
 		err = syscall.EPERM
 		return
 	}
 	// Open directory, safe against symlink races
-	pDir := filepath.Dir(pRelPath)
+	pDir := filepath.Dir(pPath)
 	dirfd, err = syscallcompat.OpenDirNofollow(rn.args.Cipherdir, pDir)
 	if err != nil {
 		return
 	}
-	pName = filepath.Base(pRelPath)
-	return dirfd, pName, nil
+	return dirfd, pPath, nil
 }
-- 
cgit v1.2.3