diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 5802fb3..6fa5156 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -78,10 +78,29 @@ harvest enough entropy. #### -e PATH, -exclude PATH Only for reverse mode: exclude relative plaintext path from the encrypted -view. Can be passed multiple times. Example: +view, matching only from root of mounted filesystem. Can be passed multiple +times. Example: gocryptfs -reverse -exclude Music -exclude Movies /home/user /mnt/user.encrypted +See also `-exclude-wildcard`, `-exclude-from` and the [EXCLUDING FILES](#excluding-files) section. + +#### -ew PATH, -exclude-wildcard PATH +Only for reverse mode: exclude paths from the encrypted view, matching anywhere. +Wildcards supported. Can be passed multiple times. Example: + + gocryptfs -reverse -exclude-wildcard '*~' /home/user /mnt/user.encrypted + +See also `-exclude`, `-exclude-from` and the [EXCLUDING FILES](#excluding-files) section. + +#### -exclude-from FILE +Only for reverse mode: reads exclusion patters (using `-exclude-wildcard` syntax) +from a file. Can be passed multiple times. Example: + + gocryptfs -reverse -exclude-from ~/crypt-exclusions /home/user /mnt/user.encrypted + +See also `-exclude`, `-exclude-wildcard` and the [EXCLUDING FILES](#excluding-files) section. + #### -exec, -noexec Enable (`-exec`) or disable (`-noexec`) executables in a gocryptfs mount (default: `-exec`). If both are specified, `-noexec` takes precedence. @@ -404,6 +423,67 @@ automated testing as it does not provide any security. Stop option parsing. Helpful when CIPHERDIR may start with a dash "-". +EXCLUDING FILES +=============== + +In reverse mode, it is possible to exclude files from the encrypted view, using +the `-exclude`, `-exclude-wildcard` and `-exclude-from` options. + +`-exclude` matches complete paths, so `-exclude file.txt` only excludes a file +named `file.txt` in the root of the mounted filesystem; files named `file.txt` +in subdirectories are still visible. (This option is kept for compatibility +with the behavior up to version 1.6.x) + +`-exclude-wildcard` matches files anywhere, so `-exclude-wildcard file.txt` +excludes files named `file.txt` in any directory. If you want to match complete +paths, you can prefix the filename with a `/`: `-exclude-wildcard /file.txt` +excludes only `file.txt` in the root of the mounted filesystem. + +If there are many exclusions, you can use `-exclude-from` to read exclusion +patterns from a file. The syntax is that of `-exclude-wildcard`, so use a +leading `/` to match complete paths. + +The rules for exclusion are that of [gitignore](https://git-scm.com/docs/gitignore#_pattern_format). +In short: + +1. A blank line matches no files, so it can serve as a separator + for readability. +2. A line starting with `#` serves as a comment. Put a backslash (`\`) + in front of the first hash for patterns that begin with a hash. +3. Trailing spaces are ignored unless they are quoted with backslash (`\`). +4. An optional prefix `!` negates the pattern; any matching file + excluded by a previous pattern will become included again. It is not + possible to re-include a file if a parent directory of that file is + excluded. Put a backslash (`\`) in front of the first `!` for + patterns that begin with a literal `!`, for example, `\!important!.txt`. +5. If the pattern ends with a slash, it is removed for the purpose of the + following description, but it would only find a match with a directory. + In other words, `foo/` will match a directory foo and paths underneath it, + but will not match a regular file or a symbolic link foo. +6. If the pattern does not contain a slash `/`, it is treated as a shell glob + pattern and checked for a match against the pathname relative to the + root of the mounted filesystem. +7. Otherwise, the pattern is treated as a shell glob suitable for + consumption by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the + pattern will not match a `/` in the pathname. For example, + `Documentation/*.html` matches `Documentation/git.html` but not + `Documentation/ppc/ppc.html` or `tools/perf/Documentation/perf.html`. +8. A leading slash matches the beginning of the pathname. For example, + `/*.c` matches `cat-file.c` but not `mozilla-sha1/sha1.c`. +9. Two consecutive asterisks (`**`) in patterns matched against full + pathname may have special meaning: + i. A leading `**` followed by a slash means match in all directories. + For example, `**/foo` matches file or directory `foo` anywhere, + the same as pattern `foo`. `**/foo/bar` matches file or directory + `bar` anywhere that is directly under directory `foo`. + ii. A trailing `/**` matches everything inside. For example, `abc/**` + matches all files inside directory `abc`, with infinite depth. + iii. A slash followed by two consecutive asterisks then a slash matches + zero or more directories. For example, `a/**/b` matches `a/b`, + `a/x/b`, `a/x/y/b` and so on. + iv. Other consecutive asterisks are considered invalid. + + EXAMPLES ======== diff --git a/cli_args.go b/cli_args.go index 425730a..c434abb 100644 --- a/cli_args.go +++ b/cli_args.go @@ -36,8 +36,8 @@ type argContainer struct { memprofile, ko, passfile, ctlsock, fsname, force_owner, trace string // -extpass can be passed multiple times extpass multipleStrings - // For reverse mode, -exclude is available. It can be specified multiple times. - exclude multipleStrings + // For reverse mode, several ways to specify exclusions. All can be specified multiple times. + exclude, excludeWildcard, excludeFrom multipleStrings // Configuration file name override config string notifypid, scryptn int @@ -193,9 +193,13 @@ func parseCliOpts() (args argContainer) { flagSet.StringVar(&args.force_owner, "force_owner", "", "uid:gid pair to coerce ownership") flagSet.StringVar(&args.trace, "trace", "", "Write execution trace to file") - // -e, --exclude + // Exclusion options flagSet.Var(&args.exclude, "e", "Alias for -exclude") flagSet.Var(&args.exclude, "exclude", "Exclude relative path from reverse view") + flagSet.Var(&args.excludeWildcard, "ew", "Alias for -exclude-wildcard") + flagSet.Var(&args.excludeWildcard, "exclude-wildcard", "Exclude path from reverse view, supporting wildcards") + flagSet.Var(&args.excludeFrom, "exclude-from", "File from which to read exclusion patterns (with -exclude-wildcard syntax)") + // -extpass flagSet.Var(&args.extpass, "extpass", "Use external program for the password prompt") diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go index 5fb72cd..e767f28 100644 --- a/internal/fusefrontend/args.go +++ b/internal/fusefrontend/args.go @@ -30,6 +30,13 @@ type Args struct { SerializeReads bool // Force decode even if integrity check fails (openSSL only) ForceDecode bool - // Exclude is a list of paths to make inaccessible + // Exclude is a list of paths to make inaccessible, starting match at + // the filesystem root Exclude []string + // ExcludeWildcards is a list of paths to make inaccessible, matched + // anywhere, and supporting wildcards + ExcludeWildcard []string + // ExcludeFrom is a list of files from which to read exclusion patterns + // (with wildcard syntax) + ExcludeFrom []string } diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go index ff628e5..2c40942 100644 --- a/internal/fusefrontend/fs.go +++ b/internal/fusefrontend/fs.go @@ -35,7 +35,7 @@ type FS struct { // states dirIVLock sync.RWMutex // Filename encryption helper - nameTransform *nametransform.NameTransform + nameTransform nametransform.NameTransformer // Content encryption helper contentEnc *contentenc.ContentEnc // This lock is used by openWriteOnlyFile() to block concurrent opens while @@ -62,7 +62,7 @@ type FS struct { //var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented. // NewFS returns a new encrypted FUSE overlay filesystem. -func NewFS(args Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *FS { +func NewFS(args Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *FS { if args.SerializeReads { serialize_reads.InitSerializer() } @@ -399,7 +399,7 @@ func (fs *FS) decryptSymlinkTarget(cData64 string) (string, error) { if cData64 == "" { return "", nil } - cData, err := fs.nameTransform.B64.DecodeString(cData64) + cData, err := fs.nameTransform.B64DecodeString(cData64) if err != nil { return "", err } @@ -472,7 +472,7 @@ func (fs *FS) encryptSymlinkTarget(data string) (cData64 string) { return "" } cData := fs.contentEnc.EncryptBlock([]byte(data), 0, nil) - cData64 = fs.nameTransform.B64.EncodeToString(cData) + cData64 = fs.nameTransform.B64EncodeToString(cData) return cData64 } diff --git a/internal/fusefrontend/xattr.go b/internal/fusefrontend/xattr.go index 1de9cac..20e8db7 100644 --- a/internal/fusefrontend/xattr.go +++ b/internal/fusefrontend/xattr.go @@ -150,7 +150,7 @@ func (fs *FS) decryptXattrValue(cData []byte) (data []byte, err error) { } // This backward compatibility is needed to support old // file systems having xattr values base64-encoded. - cData, err2 := fs.nameTransform.B64.DecodeString(string(cData)) + cData, err2 := fs.nameTransform.B64DecodeString(string(cData)) if err2 != nil { // Looks like the value was not base64-encoded, but just corrupt. // Return the original decryption error: err1 diff --git a/internal/fusefrontend_reverse/excluder.go b/internal/fusefrontend_reverse/excluder.go new file mode 100644 index 0000000..5c0941a --- /dev/null +++ b/internal/fusefrontend_reverse/excluder.go @@ -0,0 +1,66 @@ +package fusefrontend_reverse + +import ( + "io/ioutil" + "os" + "strings" + + "github.com/rfjakob/gocryptfs/internal/exitcodes" + "github.com/rfjakob/gocryptfs/internal/fusefrontend" + "github.com/rfjakob/gocryptfs/internal/tlog" + + "github.com/sabhiram/go-gitignore" +) + +// prepareExcluder creates an object to check if paths are excluded +// based on the patterns specified in the command line. +func (rfs *ReverseFS) prepareExcluder(args fusefrontend.Args) { + if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 { + excluder, err := ignore.CompileIgnoreLines(getExclusionPatterns(args)...) + if err != nil { + tlog.Fatal.Printf("Error compiling exclusion rules: %q", err) + os.Exit(exitcodes.ExcludeError) + } + rfs.excluder = excluder + } +} + +// getExclusionPatters prepares a list of patterns to be excluded. +// Patterns passed in the -exclude command line option are prefixed +// with a leading '/' to preserve backwards compatibility (before +// wildcard matching was implemented, exclusions always were matched +// agains the full path). +func getExclusionPatterns(args fusefrontend.Args) []string { + patterns := make([]string, len(args.Exclude)+len(args.ExcludeWildcard)) + // add -exclude + for i, p := range args.Exclude { + patterns[i] = "/" + p + } + // add -exclude-wildcard + copy(patterns[len(args.Exclude):], args.ExcludeWildcard) + // add -exclude-from + for _, file := range args.ExcludeFrom { + lines, err := getLines(file) + if err != nil { + tlog.Fatal.Printf("Error reading exclusion patterns: %q", err) + os.Exit(exitcodes.ExcludeError) + } + patterns = append(patterns, lines...) + } + return patterns +} + +// getLines reads a file and splits it into lines +func getLines(file string) ([]string, error) { + buffer, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + return strings.Split(string(buffer), "\n"), nil +} + +// isExcludedPlain finds out if the plaintext path "pPath" is +// excluded (used when -exclude is passed by the user). +func (rfs *ReverseFS) isExcludedPlain(pPath string) bool { + return rfs.excluder != nil && rfs.excluder.MatchesPath(pPath) +} diff --git a/internal/fusefrontend_reverse/excluder_test.go b/internal/fusefrontend_reverse/excluder_test.go new file mode 100644 index 0000000..e19b99f --- /dev/null +++ b/internal/fusefrontend_reverse/excluder_test.go @@ -0,0 +1,84 @@ +package fusefrontend_reverse + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/rfjakob/gocryptfs/internal/fusefrontend" +) + +func TestShouldNoCreateExcluderIfNoPattersWereSpecified(t *testing.T) { + var rfs ReverseFS + var args fusefrontend.Args + rfs.prepareExcluder(args) + if rfs.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"} + args.ExcludeWildcard = []string{"*~", "build/*.o"} + + expected := []string{"/file1", "/dir1/file2.txt", "*~", "build/*.o"} + + patterns := getExclusionPatterns(args) + if !reflect.DeepEqual(patterns, expected) { + t.Errorf("expected %q, got %q", expected, patterns) + } +} + +func TestShouldReadExcludePatternsFromFiles(t *testing.T) { + tmpfile1, err := ioutil.TempFile("", "excludetest") + if err != nil { + t.Fatal(err) + } + exclude1 := tmpfile1.Name() + defer os.Remove(exclude1) + defer tmpfile1.Close() + + tmpfile2, err := ioutil.TempFile("", "excludetest") + if err != nil { + t.Fatal(err) + } + exclude2 := tmpfile2.Name() + defer os.Remove(exclude2) + defer tmpfile2.Close() + + tmpfile1.WriteString("file1.1\n") + tmpfile1.WriteString("file1.2\n") + tmpfile2.WriteString("file2.1\n") + tmpfile2.WriteString("file2.2\n") + + var args fusefrontend.Args + args.ExcludeWildcard = []string{"cmdline1"} + args.ExcludeFrom = []string{exclude1, exclude2} + + // An empty string is returned for the last empty line + // It's ignored when the patterns are actually compiled + expected := []string{"cmdline1", "file1.1", "file1.2", "", "file2.1", "file2.2", ""} + + patterns := getExclusionPatterns(args) + if !reflect.DeepEqual(patterns, expected) { + t.Errorf("expected %q, got %q", expected, patterns) + } +} + +func TestShouldReturnFalseIfThereAreNoExclusions(t *testing.T) { + var rfs ReverseFS + if rfs.isExcludedPlain("any/path") { + t.Error("Should not exclude any path if no exclusions were specified") + } +} + +func TestShouldCallIgnoreParserToCheckExclusion(t *testing.T) { + rfs, ignorerMock := createRFSWithMocks() + + rfs.isExcludedPlain("some/path") + if ignorerMock.calledWith != "some/path" { + t.Error("Failed to call IgnoreParser") + } +} diff --git a/internal/fusefrontend_reverse/isexcluded_test.go b/internal/fusefrontend_reverse/isexcluded_test.go deleted file mode 100644 index fc3831a..0000000 --- a/internal/fusefrontend_reverse/isexcluded_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package fusefrontend_reverse - -import ( - "testing" -) - -func verifyExcluded(t *testing.T, rfs *ReverseFS, paths []string) { - for _, p := range paths { - if !rfs.isExcluded(p) { - t.Errorf("Path %q should be excluded, but is not", p) - } - } - if t.Failed() { - t.Logf("cExclude = %#v", rfs.cExclude) - } -} - -// Note: See also the integration tests in -// tests/reverse/exclude_test.go -func TestIsExcluded(t *testing.T) { - var rfs ReverseFS - // If the root directory is excluded, all files and subdirs should be excluded - // as well - rfs.cExclude = []string{""} - verifyExcluded(t, &rfs, []string{"", "foo", "foo/bar"}) -} diff --git a/internal/fusefrontend_reverse/mocks_test.go b/internal/fusefrontend_reverse/mocks_test.go new file mode 100644 index 0000000..fd2f940 --- /dev/null +++ b/internal/fusefrontend_reverse/mocks_test.go @@ -0,0 +1,32 @@ +package fusefrontend_reverse + +import ( + "github.com/rfjakob/gocryptfs/internal/nametransform" +) + +type IgnoreParserMock struct { + toExclude string + calledWith string +} + +func (parser *IgnoreParserMock) MatchesPath(f string) bool { + parser.calledWith = f + return f == parser.toExclude +} + +type NameTransformMock struct { + nametransform.NameTransform +} + +func (n *NameTransformMock) DecryptName(cipherName string, iv []byte) (string, error) { + return "mockdecrypt_" + cipherName, nil +} + +func createRFSWithMocks() (*ReverseFS, *IgnoreParserMock) { + ignorerMock := &IgnoreParserMock{} + nameTransformMock := &NameTransformMock{} + var rfs ReverseFS + rfs.excluder = ignorerMock + rfs.nameTransform = nameTransformMock + return &rfs, ignorerMock +} diff --git a/internal/fusefrontend_reverse/reverse_longnames.go b/internal/fusefrontend_reverse/reverse_longnames.go index a425e64..9f044e8 100644 --- a/internal/fusefrontend_reverse/reverse_longnames.go +++ b/internal/fusefrontend_reverse/reverse_longnames.go @@ -106,7 +106,7 @@ func (rfs *ReverseFS) findLongnameParent(dir string, dirIV []byte, longname stri func (rfs *ReverseFS) newNameFile(relPath string) (nodefs.File, fuse.Status) { dotName := filepath.Base(relPath) // gocryptfs.longname.XYZ.name - longname := dotName[:len(dotName)-len(nametransform.LongNameSuffix)] // gocryptfs.longname.XYZ + longname := nametransform.RemoveLongNameSuffix(dotName) // gocryptfs.longname.XYZ // cipher directory cDir := nametransform.Dir(relPath) // plain directory diff --git a/internal/fusefrontend_reverse/rfile.go b/internal/fusefrontend_reverse/rfile.go index 75932cb..9c31681 100644 --- a/internal/fusefrontend_reverse/rfile.go +++ b/internal/fusefrontend_reverse/rfile.go @@ -34,19 +34,16 @@ type reverseFile struct { var inodeTable syncmap.Map -// newFile decrypts and opens the path "relPath" and returns a reverseFile +// newFile receives a ciphered path "relPath" and its corresponding +// decrypted path "pRelPath", opens it and returns a reverseFile // object. The backing file descriptor is always read-only. -func (rfs *ReverseFS) newFile(relPath string) (*reverseFile, fuse.Status) { - if rfs.isExcluded(relPath) { +func (rfs *ReverseFS) newFile(relPath string, pRelPath string) (*reverseFile, fuse.Status) { + if rfs.isExcludedPlain(pRelPath) { // Excluded paths should have been filtered out beforehand. Better safe // than sorry. tlog.Warn.Printf("BUG: newFile: received excluded path %q. This should not happen.", relPath) return nil, fuse.ENOENT } - pRelPath, err := rfs.decryptPath(relPath) - if err != nil { - return nil, fuse.ToStatus(err) - } dir := filepath.Dir(pRelPath) dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, dir) if err != nil { diff --git a/internal/fusefrontend_reverse/rfs.go b/internal/fusefrontend_reverse/rfs.go index 6ee10fc..01ed122 100644 --- a/internal/fusefrontend_reverse/rfs.go +++ b/internal/fusefrontend_reverse/rfs.go @@ -2,9 +2,7 @@ package fusefrontend_reverse import ( "fmt" - "os" "path/filepath" - "strings" "syscall" "golang.org/x/sys/unix" @@ -16,13 +14,13 @@ import ( "github.com/rfjakob/gocryptfs/internal/configfile" "github.com/rfjakob/gocryptfs/internal/contentenc" "github.com/rfjakob/gocryptfs/internal/cryptocore" - "github.com/rfjakob/gocryptfs/internal/ctlsock" - "github.com/rfjakob/gocryptfs/internal/exitcodes" "github.com/rfjakob/gocryptfs/internal/fusefrontend" "github.com/rfjakob/gocryptfs/internal/nametransform" "github.com/rfjakob/gocryptfs/internal/pathiv" "github.com/rfjakob/gocryptfs/internal/syscallcompat" "github.com/rfjakob/gocryptfs/internal/tlog" + + "github.com/sabhiram/go-gitignore" ) // ReverseFS implements the pathfs.FileSystem interface and provides an @@ -35,12 +33,11 @@ type ReverseFS struct { // Stores configuration arguments args fusefrontend.Args // Filename encryption helper - nameTransform *nametransform.NameTransform + nameTransform nametransform.NameTransformer // Content encryption helper contentEnc *contentenc.ContentEnc - // Relative ciphertext paths to exclude (hide) from the user. Used by -exclude. - // With -plaintextnames, these are relative *plaintext* paths. - cExclude []string + // Tests wheter a path is excluded (hiden) from the user. Used by -exclude. + excluder ignore.IgnoreParser } var _ pathfs.FileSystem = &ReverseFS{} @@ -48,7 +45,7 @@ var _ pathfs.FileSystem = &ReverseFS{} // NewFS returns an encrypted FUSE overlay filesystem. // In this case (reverse mode) the backing directory is plain-text and // ReverseFS provides an encrypted view. -func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *ReverseFS { +func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *ReverseFS { initLongnameCache() fs := &ReverseFS{ // pathfs.defaultFileSystem returns ENOSYS for all operations @@ -58,34 +55,7 @@ func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransform.Na nameTransform: n, contentEnc: c, } - if len(args.Exclude) > 0 { - for _, dirty := range args.Exclude { - clean := ctlsock.SanitizePath(dirty) - if clean != dirty { - tlog.Warn.Printf("-exclude: non-canonical path %q has been interpreted as %q", dirty, clean) - } - if clean == "" { - tlog.Fatal.Printf("-exclude: excluding the root dir %q makes no sense", clean) - os.Exit(exitcodes.ExcludeError) - } - cPath, err := fs.EncryptPath(clean) - if err != nil { - tlog.Fatal.Printf("-exclude: EncryptPath %q failed: %v", clean, err) - os.Exit(exitcodes.ExcludeError) - } - fs.cExclude = append(fs.cExclude, cPath) - if !fs.args.PlaintextNames { - // If we exclude - // gocryptfs.longname.3vZ_r3eDPb1_fL3j5VA4rd_bcKWLKT9eaxOVIGK5HFA - // we should also exclude - // gocryptfs.longname.3vZ_r3eDPb1_fL3j5VA4rd_bcKWLKT9eaxOVIGK5HFA.name - if nametransform.IsLongContent(filepath.Base(cPath)) { - fs.cExclude = append(fs.cExclude, cPath+nametransform.LongNameSuffix) - } - } - } - tlog.Debug.Printf("-exclude: %v -> %v", fs.args.Exclude, fs.cExclude) - } + fs.prepareExcluder(args) return fs } @@ -100,24 +70,64 @@ func relDir(path string) string { return dir } -// isExcluded finds out if relative ciphertext path "relPath" is excluded -// (used when -exclude is passed by the user) -func (rfs *ReverseFS) isExcluded(relPath string) bool { - for _, e := range rfs.cExclude { - // If the root dir is excluded, everything is excluded. - if e == "" { - return true - } - // This exact path is excluded - if e == relPath { - return true - } - // Files inside an excluded directory are also excluded - if strings.HasPrefix(relPath, e+"/") { - return true - } +// getFileInfo returns information on a ciphertext path "relPath": +// - ftype: file type (as returned by getFileType) +// - excluded: if the path is excluded +// - pPath: if it's not a special file, the decrypted path +// - err: non nil if any error happens +func (rfs *ReverseFS) getFileInfo(relPath string) (ftype fileType, excluded bool, pPath string, err error) { + ftype = rfs.getFileType(relPath) + if ftype == typeConfig { + excluded, pPath, err = false, "", nil + return } - return false + if ftype == typeDiriv { + parentDir := nametransform.Dir(relPath) + _, excluded, _, err = rfs.getFileInfo(parentDir) + pPath = "" + return + } + if ftype == typeName { + parentDir := nametransform.Dir(relPath) + var parentExcluded bool + _, parentExcluded, _, err = rfs.getFileInfo(parentDir) + if parentExcluded || err != nil { + excluded, pPath = parentExcluded, "" + return + } + relPath = nametransform.RemoveLongNameSuffix(relPath) + } + pPath, err = rfs.decryptPath(relPath) + excluded = err == nil && rfs.isExcludedPlain(pPath) + return +} + +type fileType int + +// Values returned by getFileType +const ( + // A regular file/directory/symlink + typeRegular fileType = iota + // A DirIV (gocryptfs.diriv) file + typeDiriv + // A .name file for a file with a long name + typeName + // The config file + typeConfig +) + +// getFileType returns the type of file. Only the name is checked +func (rfs *ReverseFS) getFileType(cPath string) fileType { + if rfs.isDirIV(cPath) { + return typeDiriv + } + if rfs.isNameFile(cPath) { + return typeName + } + if rfs.isTranslatedConfig(cPath) { + return typeConfig + } + return typeRegular } // isDirIV determines if the path points to a gocryptfs.diriv file @@ -155,14 +165,18 @@ func (rfs *ReverseFS) isTranslatedConfig(relPath string) bool { // GetAttr - FUSE call // "relPath" is the relative ciphertext path func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr, fuse.Status) { - if rfs.isExcluded(relPath) { + ftype, excluded, pPath, err := rfs.getFileInfo(relPath) + if excluded { return nil, fuse.ENOENT } + if err != nil { + return nil, fuse.ToStatus(err) + } // Handle "gocryptfs.conf" - if rfs.isTranslatedConfig(relPath) { + if ftype == typeConfig { absConfPath, _ := rfs.abs(configfile.ConfReverseName, nil) var st syscall.Stat_t - err := syscall.Lstat(absConfPath, &st) + err = syscall.Lstat(absConfPath, &st) if err != nil { return nil, fuse.ToStatus(err) } @@ -177,11 +191,11 @@ func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr var f nodefs.File var status fuse.Status virtual := false - if rfs.isDirIV(relPath) { + if ftype == typeDiriv { virtual = true f, status = rfs.newDirIVFile(relPath) } - if rfs.isNameFile(relPath) { + if ftype == typeName { virtual = true f, status = rfs.newNameFile(relPath) } @@ -197,7 +211,7 @@ func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr } return &a, status } - dirfd, name, err := rfs.openBackingDir(relPath) + dirfd, name, err := rfs.openBackingDir(pPath) if err != nil { return nil, fuse.ToStatus(err) } @@ -239,10 +253,14 @@ func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr // Access - FUSE call func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context) fuse.Status { - if rfs.isExcluded(relPath) { + ftype, excluded, pPath, err := rfs.getFileInfo(relPath) + if excluded { return fuse.ENOENT } - if rfs.isTranslatedConfig(relPath) || rfs.isDirIV(relPath) || rfs.isNameFile(relPath) { + if err != nil { + return fuse.ToStatus(err) + } + if ftype != typeRegular { // access(2) R_OK flag for checking if the file is readable, always 4 as defined in POSIX. ROK := uint32(0x4) // Virtual files can always be read and never written @@ -251,7 +269,7 @@ func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context) } return fuse.EPERM } - dirfd, name, err := rfs.openBackingDir(relPath) + dirfd, name, err := rfs.openBackingDir(pPath) if err != nil { return fuse.ToStatus(err) } @@ -262,19 +280,23 @@ func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context) // Open - FUSE call func (rfs *ReverseFS) Open(relPath string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) { - if rfs.isExcluded(relPath) { + ftype, excluded, pPath, err := rfs.getFileInfo(relPath) + if excluded { return nil, fuse.ENOENT } - if rfs.isTranslatedConfig(relPath) { + if err != nil { + return nil, fuse.ToStatus(err) + } + if ftype == typeConfig { return rfs.loopbackfs.Open(configfile.ConfReverseName, flags, context) } - if rfs.isDirIV(relPath) { + if ftype == typeDiriv { return rfs.newDirIVFile(relPath) } - if rfs.isNameFile(relPath) { + if ftype == typeName { return rfs.newNameFile(relPath) } - return rfs.newFile(relPath) + return rfs.newFile(relPath, pPath) } func (rfs *ReverseFS) openDirPlaintextnames(relPath string, entries []fuse.DirEntry) ([]fuse.DirEntry, fuse.Status) { @@ -304,13 +326,16 @@ func (rfs *ReverseFS) openDirPlaintextnames(relPath string, entries []fuse.DirEn // OpenDir - FUSE readdir call func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) { - if rfs.isExcluded(cipherPath) { + ftype, excluded, relPath, err := rfs.getFileInfo(cipherPath) + if excluded { return nil, fuse.ENOENT } - relPath, err := rfs.decryptPath(cipherPath) if err != nil { return nil, fuse.ToStatus(err) } + if ftype != typeRegular { + return nil, fuse.ENOTDIR + } // Read plaintext dir dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, filepath.Dir(relPath)) if err != nil { @@ -332,9 +357,11 @@ func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse. if !status.Ok() { return nil, status } - entries = rfs.excludeDirEntries(cipherPath, entries) + entries = rfs.excludeDirEntries(relPath, entries) return entries, fuse.OK } + // Filter out excluded entries + entries = rfs.excludeDirEntries(relPath, entries) // Allocate maximum possible number of virtual files. // If all files have long names we need a virtual ".name" file for each, // plus one for gocryptfs.diriv. @@ -370,24 +397,22 @@ func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse. } // Add virtual files entries = append(entries, virtualFiles[:nVirtual]...) - // Filter out excluded entries - entries = rfs.excludeDirEntries(cipherPath, entries) return entries, fuse.OK } // excludeDirEntries filters out directory entries that are "-exclude"d. -// cDir is the relative ciphertext path to the directory these entries are -// from. -func (rfs *ReverseFS) excludeDirEntries(cDir string, entries []fuse.DirEntry) (filtered []fuse.DirEntry) { - if rfs.cExclude == nil { +// pDir is the relative plaintext path to the directory these entries are +// from. The entries should be plaintext files. +func (rfs *ReverseFS) excludeDirEntries(pDir string, entries []fuse.DirEntry) (filtered []fuse.DirEntry) { + if rfs.excluder == nil { return entries } filtered = make([]fuse.DirEntry, 0, len(entries)) for _, entry := range entries { - // filepath.Join handles the case of cipherPath="" correctly: - // Join("", "foo") -> "foo". This does not: cipherPath + "/" + name" - p := filepath.Join(cDir, entry.Name) - if rfs.isExcluded(p) { + // filepath.Join handles the case of pDir="" correctly: + // Join("", "foo") -> "foo". This does not: pDir + "/" + name" + p := filepath.Join(pDir, entry.Name) + if rfs.isExcludedPlain(p) { // Skip file continue } @@ -402,11 +427,12 @@ func (rfs *ReverseFS) excludeDirEntries(cDir string, entries []fuse.DirEntry) (f // it's worth, so we just ignore the path and always return info about the // backing storage root dir. func (rfs *ReverseFS) StatFs(relPath string) *fuse.StatfsOut { - if rfs.isExcluded(relPath) { + _, excluded, _, err := rfs.getFileInfo(relPath) + if excluded || err != nil { return nil } var s syscall.Statfs_t - err := syscall.Statfs(rfs.args.Cipherdir, &s) + err = syscall.Statfs(rfs.args.Cipherdir, &s) if err != nil { return nil } @@ -417,10 +443,17 @@ func (rfs *ReverseFS) StatFs(relPath string) *fuse.StatfsOut { // Readlink - FUSE call func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, fuse.Status) { - if rfs.isExcluded(relPath) { + ftype, excluded, pPath, err := rfs.getFileInfo(relPath) + if excluded { return "", fuse.ENOENT } - dirfd, name, err := rfs.openBackingDir(relPath) + if err != nil { + return "", fuse.ToStatus(err) + } + if ftype != typeRegular { + return "", fuse.EINVAL + } + dirfd, name, err := rfs.openBackingDir(pPath) if err != nil { return "", fuse.ToStatus(err) } @@ -436,7 +469,7 @@ func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, f nonce := pathiv.Derive(relPath, pathiv.PurposeSymlinkIV) // Symlinks are encrypted like file contents and base64-encoded cBinTarget := rfs.contentEnc.EncryptBlockNonce([]byte(plainTarget), 0, nil, nonce) - cTarget := rfs.nameTransform.B64.EncodeToString(cBinTarget) + cTarget := rfs.nameTransform.B64EncodeToString(cBinTarget) // The kernel will reject a symlink target above 4096 chars and return // and I/O error to the user. Better emit the proper error ourselves. if len(cTarget) > syscallcompat.PATH_MAX { diff --git a/internal/fusefrontend_reverse/rfs_test.go b/internal/fusefrontend_reverse/rfs_test.go new file mode 100644 index 0000000..eccaab9 --- /dev/null +++ b/internal/fusefrontend_reverse/rfs_test.go @@ -0,0 +1,124 @@ +package fusefrontend_reverse + +import ( + "testing" + + "github.com/rfjakob/gocryptfs/internal/configfile" + "github.com/rfjakob/gocryptfs/internal/nametransform" +) + +func TestShouldDetectDirIV(t *testing.T) { + var rfs ReverseFS + ftype := rfs.getFileType("some/path/" + nametransform.DirIVFilename) + if ftype != typeDiriv { + t.Errorf("Expecting %d, got %d\n", typeDiriv, ftype) + } +} + +func TestShouldDetectNameFile(t *testing.T) { + var rfs ReverseFS + ftype := rfs.getFileType("dir1/dir2/gocryptfs.longname.URrM8kgxTKYMgCk4hKk7RO9Lcfr30XQof4L_5bD9Iro=" + nametransform.LongNameSuffix) + if ftype != typeName { + t.Errorf("Expecting %d, got %d\n", typeName, ftype) + } +} + +func TestShouldDetectConfigFile(t *testing.T) { + var rfs ReverseFS + ftype := rfs.getFileType(configfile.ConfDefaultName) + if ftype != typeConfig { + t.Errorf("Expecting %d, got %d\n", typeConfig, ftype) + } +} + +func TestShouldDetectRegularFile(t *testing.T) { + var rfs ReverseFS + ftype := rfs.getFileType("documents/text_file.txt") + if ftype != typeRegular { + t.Errorf("Expecting %d, got %d\n", typeRegular, ftype) + } +} + +// Note: For path exclusion, see also the integration tests in +// tests/reverse/exclude_test.go +func TestShouldNotCallIgnoreParserForTranslatedConfig(t *testing.T) { + rfs, ignorerMock := createRFSWithMocks() + + ftype, excluded, _, err := rfs.getFileInfo(configfile.ConfDefaultName) + if err != nil { + t.Errorf("Unexpected error %q\n", err) + } + if ftype != typeConfig { + t.Errorf("Wrong file type, expecting %d, got %d\n", typeConfig, ftype) + } + if excluded { + t.Error("Should not exclude translated config") + } + if ignorerMock.calledWith != "" { + t.Error("Should not call IgnoreParser for translated config") + } +} + +func TestShouldCheckIfParentIsExcludedForDirIV(t *testing.T) { + rfs, ignorerMock := createRFSWithMocks() + path := "dir" + ignorerMock.toExclude = "mockdecrypt_dir" + dirIV := path + "/" + nametransform.DirIVFilename + + ftype, excluded, _, err := rfs.getFileInfo(dirIV) + if err != nil { + t.Errorf("Unexpected error %q\n", err) + } + if ftype != typeDiriv { + t.Errorf("Wrong file type, expecting %d, got %d\n", typeDiriv, ftype) + } + if !excluded { + t.Error("Should have excluded DirIV based on parent") + } + if ignorerMock.calledWith != "mockdecrypt_dir" { + t.Errorf("Should have checked parent dir, checked %q", ignorerMock.calledWith) + } +} + +func TestShouldCheckIfParentIsExcludedForLongName(t *testing.T) { + rfs, ignorerMock := createRFSWithMocks() + path := "parent" + ignorerMock.toExclude = "mockdecrypt_parent" + dirIV := path + "/" + "gocryptfs.longname.fake.name" + + ftype, excluded, _, err := rfs.getFileInfo(dirIV) + if err != nil { + t.Errorf("Unexpected error %q\n", err) + } + if ftype != typeName { + t.Errorf("Wrong file type, expecting %d, got %d\n", typeName, ftype) + } + if !excluded { + t.Error("Should have excluded LongName based on parent") + } + if ignorerMock.calledWith != "mockdecrypt_parent" { + t.Errorf("Should have checked parent dir, checked %q", ignorerMock.calledWith) + } +} + +func TestShouldDecryptPathAndReturnTrueForExcludedPath(t *testing.T) { + rfs, ignorerMock := createRFSWithMocks() + ignorerMock.toExclude = "mockdecrypt_file.txt" + + ftype, excluded, pPath, err := rfs.getFileInfo("file.txt") + if err != nil { + t.Errorf("Unexpected error %q\n", err) + } + if ftype != typeRegular { + t.Errorf("Wrong file type, expecting %d, got %d\n", typeRegular, ftype) + } + if !excluded { + t.Error("Should have excluded") + } + if pPath != "mockdecrypt_file.txt" { + t.Errorf("Wrong pPath returned, got %q\n", pPath) + } + if ignorerMock.calledWith != "mockdecrypt_file.txt" { + t.Error("Didn't call IgnoreParser with decrypted path") + } +} diff --git a/internal/fusefrontend_reverse/rpath.go b/internal/fusefrontend_reverse/rpath.go index 7115426..2da8379 100644 --- a/internal/fusefrontend_reverse/rpath.go +++ b/internal/fusefrontend_reverse/rpath.go @@ -96,16 +96,12 @@ func (rfs *ReverseFS) decryptPath(relPath string) (string, error) { return pRelPath, nil } -// openBackingDir decrypt the relative ciphertext path "cRelPath", opens -// the directory that contains the target file/dir 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 (rfs *ReverseFS) openBackingDir(cRelPath string) (dirfd int, pName string, err error) { - // Decrypt relative path - pRelPath, err := rfs.decryptPath(cRelPath) - if err != nil { - return -1, "", err - } +// openBackingDir receives an already decrypted relative path +// "pRelPath", opens the directory that contains the target file/dir +// 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 (rfs *ReverseFS) openBackingDir(pRelPath string) (dirfd int, pName string, err error) { // Open directory, safe against symlink races pDir := filepath.Dir(pRelPath) dirfd, err = syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, pDir) diff --git a/internal/nametransform/longnames.go b/internal/nametransform/longnames.go index 7a6d413..1bbcbb6 100644 --- a/internal/nametransform/longnames.go +++ b/internal/nametransform/longnames.go @@ -69,6 +69,13 @@ func IsLongContent(cName string) bool { return NameType(cName) == LongNameContent } +// RemoveLongNameSuffix removes the ".name" suffix from cName, returning the corresponding +// content file name. +// No check is made if cName actually is a LongNameFilename. +func RemoveLongNameSuffix(cName string) string { + return cName[:len(cName)-len(LongNameSuffix)] +} + // ReadLongName - read cName + ".name" from the directory opened as dirfd. // // Symlink-safe through Openat(). diff --git a/internal/nametransform/longnames_test.go b/internal/nametransform/longnames_test.go index 8fa19fe..4210492 100644 --- a/internal/nametransform/longnames_test.go +++ b/internal/nametransform/longnames_test.go @@ -20,3 +20,11 @@ func TestIsLongName(t *testing.T) { t.Errorf("False positive") } } + +func TestRemoveLongNameSuffix(t *testing.T) { + filename := "gocryptfs.longname.LkwUdALvV_ANnzQN6ZZMYnxxfARD3IeZWCKnxGJjYmU=.name" + content := "gocryptfs.longname.LkwUdALvV_ANnzQN6ZZMYnxxfARD3IeZWCKnxGJjYmU=" + if RemoveLongNameSuffix(filename) != content { + t.Error(".name suffix not removed") + } +} diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 20fbede..d5c2c8b 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -17,6 +17,17 @@ const ( NameMax = 255 ) +// NameTransformer is an interface used to transform filenames. +type NameTransformer interface { + DecryptName(cipherName string, iv []byte) (string, error) + EncryptName(plainName string, iv []byte) string + EncryptAndHashName(name string, iv []byte) (string, error) + HashLongName(name string) string + WriteLongNameAt(dirfd int, hashName string, plainName string) error + B64EncodeToString(src []byte) string + B64DecodeString(s string) ([]byte, error) +} + // NameTransform is used to transform filenames. type NameTransform struct { emeCipher *eme.EMECipher @@ -88,3 +99,13 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s cipherName64 = n.B64.EncodeToString(bin) return cipherName64 } + +// B64EncodeToString returns a Base64-encoded string +func (n *NameTransform) B64EncodeToString(src []byte) string { + return n.B64.EncodeToString(src) +} + +// B64DecodeString decodes a Base64-encoded string +func (n *NameTransform) B64DecodeString(s string) ([]byte, error) { + return n.B64.DecodeString(s) +} diff --git a/mount.go b/mount.go index 03cbc0b..6c94240 100644 --- a/mount.go +++ b/mount.go @@ -231,15 +231,17 @@ func initFuseFrontend(args *argContainer) (pfs pathfs.FileSystem, wipeKeys func( args.allow_other = true } frontendArgs := fusefrontend.Args{ - Cipherdir: args.cipherdir, - PlaintextNames: args.plaintextnames, - LongNames: args.longnames, - ConfigCustom: args._configCustom, - NoPrealloc: args.noprealloc, - SerializeReads: args.serialize_reads, - ForceDecode: args.forcedecode, - ForceOwner: args._forceOwner, - Exclude: args.exclude, + Cipherdir: args.cipherdir, + PlaintextNames: args.plaintextnames, + LongNames: args.longnames, + ConfigCustom: args._configCustom, + NoPrealloc: args.noprealloc, + SerializeReads: args.serialize_reads, + ForceDecode: args.forcedecode, + ForceOwner: args._forceOwner, + Exclude: args.exclude, + ExcludeWildcard: args.excludeWildcard, + ExcludeFrom: args.excludeFrom, } // confFile is nil when "-zerokey" or "-masterkey" was used if confFile != nil { diff --git a/tests/reverse/exclude_test.go b/tests/reverse/exclude_test.go index 150b358..1662f93 100644 --- a/tests/reverse/exclude_test.go +++ b/tests/reverse/exclude_test.go @@ -15,11 +15,19 @@ const xxx = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx /* tree exclude_test_fs exclude_test_fs/ +├── bkp1~ ├── dir1 │ ├── file1 │ ├── file2 +│ ├── exclude +│ ├── longbkp1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~ │ ├── longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -│ └── longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +│ ├── longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +│ ├── longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +│ └── subdir1 +│ ├── exclude +│ └── subdir2 +│ └── exclude ├── dir2 │ ├── file │ ├── longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -30,11 +38,13 @@ exclude_test_fs/ ├── file1 ├── file2 ├── longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -│ └── file +│ └── file1 ├── longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +│ ├── bkp~ │ └── file ├── longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -└── longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +├── longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +└── longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx */ func ctlsockEncryptPath(t *testing.T, sock string, path string) string { @@ -47,26 +57,46 @@ func ctlsockEncryptPath(t *testing.T, sock string, path string) string { } func testExclude(t *testing.T, flag string) { + pPatterns := []string{ + "file1", // matches file1 anywhere + "!longdir1" + xxx + "/file1", // ! includes an otherwise file + "file2/", // a trailing slash matches only a directory + "dir1/file2", // matches file2 inside dir1 anywhere + "#file2", // comments are ignored + "dir2", // excludes the whole directory + "longfile2" + xxx, // matches longfile2 anywhere + "/longfile3" + xxx, // a leading / anchors the match at the root + "*~", // wildcards are supported + "dir1/**/exclude", // ** matches any number of directories + } pOk := []string{ "file2", - "dir1/file1", "dir1/longfile1" + xxx, + "dir1/longfile3" + xxx, "longdir1" + xxx, - "longdir1" + xxx + "/file", + "longdir1" + xxx + "/file1", + "longdir2" + xxx + "/file", "longfile1" + xxx, } pExclude := []string{ - "file1", + "bkp1~", + "dir1/file1", "dir1/file2", + "dir1/exclude", + "dir1/longbkp1" + xxx + "~", "dir1/longfile2" + xxx, + "dir1/subdir1/exclude", + "dir1/subdir1/subdir2/exclude", "dir2", "dir2/file", - "dir2/file/xxx", - "dir2/subdir", - "dir2/subdir/file", "dir2/longdir1" + xxx + "/file", "dir2/longfile." + xxx, + "dir2/subdir", + "dir2/subdir/file", + "file1", + "longdir2" + xxx + "/bkp~", "longfile2" + xxx, + "longfile3" + xxx, } // Mount reverse fs mnt, err := ioutil.TempDir(test_helpers.TmpDir, "TestExclude") @@ -75,7 +105,7 @@ func testExclude(t *testing.T, flag string) { } sock := mnt + ".sock" cliArgs := []string{"-reverse", "-extpass", "echo test", "-ctlsock", sock} - for _, v := range pExclude { + for _, v := range pPatterns { cliArgs = append(cliArgs, flag, v) } if plaintextnames { @@ -120,6 +150,6 @@ func encryptExcludeTestPaths(t *testing.T, socket string, pRelPaths []string) (o } func TestExclude(t *testing.T) { - testExclude(t, "-exclude") - testExclude(t, "-e") + testExclude(t, "-exclude-wildcard") + testExclude(t, "-ew") } diff --git a/tests/reverse/exclude_test_fs/longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file b/tests/reverse/exclude_test_fs/bkp1~ similarity index 100% rename from tests/reverse/exclude_test_fs/longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file rename to tests/reverse/exclude_test_fs/bkp1~ diff --git a/tests/reverse/exclude_test_fs/dir1/exclude b/tests/reverse/exclude_test_fs/dir1/exclude new file mode 100644 index 0000000..e69de29 diff --git a/tests/reverse/exclude_test_fs/dir1/longbkp1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~ b/tests/reverse/exclude_test_fs/dir1/longbkp1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~ new file mode 100644 index 0000000..e69de29 diff --git a/tests/reverse/exclude_test_fs/dir1/longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx b/tests/reverse/exclude_test_fs/dir1/longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx new file mode 100644 index 0000000..e69de29 diff --git a/tests/reverse/exclude_test_fs/dir1/subdir1/exclude b/tests/reverse/exclude_test_fs/dir1/subdir1/exclude new file mode 100644 index 0000000..e69de29 diff --git a/tests/reverse/exclude_test_fs/dir1/subdir1/subdir2/exclude b/tests/reverse/exclude_test_fs/dir1/subdir1/subdir2/exclude new file mode 100644 index 0000000..e69de29 diff --git a/tests/reverse/exclude_test_fs/longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file1 b/tests/reverse/exclude_test_fs/longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file1 new file mode 100644 index 0000000..e69de29 diff --git a/tests/reverse/exclude_test_fs/longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/bkp~ b/tests/reverse/exclude_test_fs/longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/bkp~ new file mode 100644 index 0000000..e69de29 diff --git a/tests/reverse/exclude_test_fs/longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx b/tests/reverse/exclude_test_fs/longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx new file mode 100644 index 0000000..e69de29