v2pai: delete fusefrontend_reverse_v1api
Served its mission a copy-paste source but can now be deleted.
This commit is contained in:
parent
f03f56844b
commit
fcb28e4ff3
@ -1,38 +0,0 @@
|
|||||||
package fusefrontend_reverse
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/ctlsocksrv"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/pathiv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ ctlsocksrv.Interface = &ReverseFS{} // Verify that interface is implemented.
|
|
||||||
|
|
||||||
// EncryptPath implements ctlsock.Backend.
|
|
||||||
// This is used for the control socket and for the "-exclude" logic.
|
|
||||||
func (rfs *ReverseFS) EncryptPath(plainPath string) (string, error) {
|
|
||||||
if rfs.args.PlaintextNames || plainPath == "" {
|
|
||||||
return plainPath, nil
|
|
||||||
}
|
|
||||||
cipherPath := ""
|
|
||||||
parts := strings.Split(plainPath, "/")
|
|
||||||
for _, part := range parts {
|
|
||||||
dirIV := pathiv.Derive(cipherPath, pathiv.PurposeDirIV)
|
|
||||||
encryptedPart := rfs.nameTransform.EncryptName(part, dirIV)
|
|
||||||
if rfs.args.LongNames && len(encryptedPart) > unix.NAME_MAX {
|
|
||||||
encryptedPart = rfs.nameTransform.HashLongName(encryptedPart)
|
|
||||||
}
|
|
||||||
cipherPath = filepath.Join(cipherPath, encryptedPart)
|
|
||||||
}
|
|
||||||
return cipherPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecryptPath implements ctlsock.Backend
|
|
||||||
func (rfs *ReverseFS) DecryptPath(cipherPath string) (string, error) {
|
|
||||||
p, err := rfs.decryptPath(cipherPath)
|
|
||||||
return p, err
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
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
|
|
||||||
// against 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)
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
package fusefrontend_reverse
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/pathiv"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// File names are padded to 16-byte multiples, encrypted and
|
|
||||||
// base64-encoded. We can encode at most 176 bytes to stay below the 255
|
|
||||||
// bytes limit:
|
|
||||||
// * base64(176 bytes) = 235 bytes
|
|
||||||
// * base64(192 bytes) = 256 bytes (over 255!)
|
|
||||||
// But the PKCS#7 padding is at least one byte. This means we can only use
|
|
||||||
// 175 bytes for the file name.
|
|
||||||
shortNameMax = 175
|
|
||||||
)
|
|
||||||
|
|
||||||
// longnameParentCache maps dir+"/"+longname to plaintextname.
|
|
||||||
// Yes, the combination of relative plaintext dir path and encrypted
|
|
||||||
// longname is strange, but works fine as a map index.
|
|
||||||
var longnameParentCache map[string]string
|
|
||||||
var longnameCacheLock sync.Mutex
|
|
||||||
|
|
||||||
// Very simple cache cleaner: Nuke it every hour
|
|
||||||
func longnameCacheCleaner() {
|
|
||||||
for {
|
|
||||||
time.Sleep(time.Hour)
|
|
||||||
longnameCacheLock.Lock()
|
|
||||||
longnameParentCache = map[string]string{}
|
|
||||||
longnameCacheLock.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initLongnameCache() {
|
|
||||||
if longnameParentCache != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
longnameParentCache = map[string]string{}
|
|
||||||
go longnameCacheCleaner()
|
|
||||||
}
|
|
||||||
|
|
||||||
// findLongnameParent converts "longname" = "gocryptfs.longname.XYZ" to the
|
|
||||||
// plaintext name. "dir" = relative plaintext path to the directory the
|
|
||||||
// longname file is in, "dirIV" = directory IV of the directory.
|
|
||||||
func (rfs *ReverseFS) findLongnameParent(dir string, dirIV []byte, longname string) (plaintextName string, err error) {
|
|
||||||
longnameCacheLock.Lock()
|
|
||||||
hit := longnameParentCache[dir+"/"+longname]
|
|
||||||
longnameCacheLock.Unlock()
|
|
||||||
if hit != "" {
|
|
||||||
return hit, nil
|
|
||||||
}
|
|
||||||
dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, filepath.Dir(dir))
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("findLongnameParent: OpenDirNofollow failed: %v\n", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
fd, err := syscallcompat.Openat(dirfd, filepath.Base(dir), syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("findLongnameParent: Openat failed: %v\n", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
dirEntries, err := syscallcompat.Getdents(fd)
|
|
||||||
syscall.Close(fd)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("findLongnameParent: Getdents failed: %v\n", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
longnameCacheLock.Lock()
|
|
||||||
defer longnameCacheLock.Unlock()
|
|
||||||
for _, entry := range dirEntries {
|
|
||||||
plaintextName := entry.Name
|
|
||||||
if len(plaintextName) <= shortNameMax {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cName := rfs.nameTransform.EncryptName(plaintextName, dirIV)
|
|
||||||
if len(cName) <= unix.NAME_MAX {
|
|
||||||
// Entry should have been skipped by the "continue" above
|
|
||||||
log.Panic("logic error or wrong shortNameMax constant?")
|
|
||||||
}
|
|
||||||
hName := rfs.nameTransform.HashLongName(cName)
|
|
||||||
longnameParentCache[dir+"/"+hName] = plaintextName
|
|
||||||
if longname == hName {
|
|
||||||
hit = plaintextName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hit == "" {
|
|
||||||
return "", syscall.ENOENT
|
|
||||||
}
|
|
||||||
return hit, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rfs *ReverseFS) newNameFile(relPath string) (nodefs.File, fuse.Status) {
|
|
||||||
dotName := filepath.Base(relPath) // gocryptfs.longname.XYZ.name
|
|
||||||
longname := nametransform.RemoveLongNameSuffix(dotName) // gocryptfs.longname.XYZ
|
|
||||||
// cipher directory
|
|
||||||
cDir := nametransform.Dir(relPath)
|
|
||||||
// plain directory
|
|
||||||
pDir, err := rfs.decryptPath(cDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
dirIV := pathiv.Derive(cDir, pathiv.PurposeDirIV)
|
|
||||||
// plain name
|
|
||||||
pName, err := rfs.findLongnameParent(pDir, dirIV, longname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
content := []byte(rfs.nameTransform.EncryptName(pName, dirIV))
|
|
||||||
parentFile := filepath.Join(pDir, pName)
|
|
||||||
return rfs.newVirtualFile(content, rfs.args.Cipherdir, parentFile, inoTagNameFile)
|
|
||||||
}
|
|
@ -1,209 +0,0 @@
|
|||||||
package fusefrontend_reverse
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
// In newer Go versions, this has moved to just "sync/syncmap".
|
|
||||||
"golang.org/x/sync/syncmap"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/contentenc"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/pathiv"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type reverseFile struct {
|
|
||||||
// Embed nodefs.defaultFile for a ENOSYS implementation of all methods
|
|
||||||
nodefs.File
|
|
||||||
// Backing FD
|
|
||||||
fd *os.File
|
|
||||||
// File header (contains the IV)
|
|
||||||
header contentenc.FileHeader
|
|
||||||
// IV for block 0
|
|
||||||
block0IV []byte
|
|
||||||
// Content encryption helper
|
|
||||||
contentEnc *contentenc.ContentEnc
|
|
||||||
}
|
|
||||||
|
|
||||||
var inodeTable syncmap.Map
|
|
||||||
|
|
||||||
// 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, 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
|
|
||||||
}
|
|
||||||
dir := filepath.Dir(pRelPath)
|
|
||||||
dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
fd, err := syscallcompat.Openat(dirfd, filepath.Base(pRelPath), syscall.O_RDONLY|syscall.O_NOFOLLOW, 0)
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
var st syscall.Stat_t
|
|
||||||
err = syscall.Fstat(fd, &st)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Warn.Printf("newFile: Fstat error: %v", err)
|
|
||||||
syscall.Close(fd)
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Reject access if the file descriptor does not refer to a regular file.
|
|
||||||
var a fuse.Attr
|
|
||||||
a.FromStat(&st)
|
|
||||||
if !a.IsRegular() {
|
|
||||||
tlog.Warn.Printf("ino%d: newFile: not a regular file", st.Ino)
|
|
||||||
syscall.Close(fd)
|
|
||||||
return nil, fuse.ToStatus(syscall.EACCES)
|
|
||||||
}
|
|
||||||
// See if we have that inode number already in the table
|
|
||||||
// (even if Nlink has dropped to 1)
|
|
||||||
var derivedIVs pathiv.FileIVs
|
|
||||||
v, found := inodeTable.Load(st.Ino)
|
|
||||||
if found {
|
|
||||||
tlog.Debug.Printf("ino%d: newFile: found in the inode table", st.Ino)
|
|
||||||
derivedIVs = v.(pathiv.FileIVs)
|
|
||||||
} else {
|
|
||||||
derivedIVs = pathiv.DeriveFile(relPath)
|
|
||||||
// Nlink > 1 means there is more than one path to this file.
|
|
||||||
// Store the derived values so we always return the same data,
|
|
||||||
// regardless of the path that is used to access the file.
|
|
||||||
// This means that the first path wins.
|
|
||||||
if st.Nlink > 1 {
|
|
||||||
v, found = inodeTable.LoadOrStore(st.Ino, derivedIVs)
|
|
||||||
if found {
|
|
||||||
// Another thread has stored a different value before we could.
|
|
||||||
derivedIVs = v.(pathiv.FileIVs)
|
|
||||||
} else {
|
|
||||||
tlog.Debug.Printf("ino%d: newFile: Nlink=%d, stored in the inode table", st.Ino, st.Nlink)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
header := contentenc.FileHeader{
|
|
||||||
Version: contentenc.CurrentVersion,
|
|
||||||
ID: derivedIVs.ID,
|
|
||||||
}
|
|
||||||
return &reverseFile{
|
|
||||||
File: nodefs.NewDefaultFile(),
|
|
||||||
fd: os.NewFile(uintptr(fd), pRelPath),
|
|
||||||
header: header,
|
|
||||||
block0IV: derivedIVs.Block0IV,
|
|
||||||
contentEnc: rfs.contentEnc,
|
|
||||||
}, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAttr - FUSE call
|
|
||||||
// Triggered by fstat() from userspace
|
|
||||||
func (rf *reverseFile) GetAttr(*fuse.Attr) fuse.Status {
|
|
||||||
tlog.Debug.Printf("reverseFile.GetAttr fd=%d\n", rf.fd.Fd())
|
|
||||||
// The kernel should fall back to stat()
|
|
||||||
return fuse.ENOSYS
|
|
||||||
}
|
|
||||||
|
|
||||||
// encryptBlocks - encrypt "plaintext" into a number of ciphertext blocks.
|
|
||||||
// "plaintext" must already be block-aligned.
|
|
||||||
func (rf *reverseFile) encryptBlocks(plaintext []byte, firstBlockNo uint64, fileID []byte, block0IV []byte) []byte {
|
|
||||||
inBuf := bytes.NewBuffer(plaintext)
|
|
||||||
var outBuf bytes.Buffer
|
|
||||||
bs := int(rf.contentEnc.PlainBS())
|
|
||||||
for blockNo := firstBlockNo; inBuf.Len() > 0; blockNo++ {
|
|
||||||
inBlock := inBuf.Next(bs)
|
|
||||||
iv := pathiv.BlockIV(block0IV, blockNo)
|
|
||||||
outBlock := rf.contentEnc.EncryptBlockNonce(inBlock, blockNo, fileID, iv)
|
|
||||||
outBuf.Write(outBlock)
|
|
||||||
}
|
|
||||||
return outBuf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// readBackingFile: read from the backing plaintext file, encrypt it, return the
|
|
||||||
// ciphertext.
|
|
||||||
// "off" ... ciphertext offset (must be >= HEADER_LEN)
|
|
||||||
// "length" ... ciphertext length
|
|
||||||
func (rf *reverseFile) readBackingFile(off uint64, length uint64) (out []byte, err error) {
|
|
||||||
blocks := rf.contentEnc.ExplodeCipherRange(off, length)
|
|
||||||
|
|
||||||
// Read the backing plaintext in one go
|
|
||||||
alignedOffset, alignedLength := contentenc.JointPlaintextRange(blocks)
|
|
||||||
plaintext := make([]byte, int(alignedLength))
|
|
||||||
n, err := rf.fd.ReadAt(plaintext, int64(alignedOffset))
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
tlog.Warn.Printf("readBackingFile: ReadAt: %s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Truncate buffer down to actually read bytes
|
|
||||||
plaintext = plaintext[0:n]
|
|
||||||
|
|
||||||
// Encrypt blocks
|
|
||||||
ciphertext := rf.encryptBlocks(plaintext, blocks[0].BlockNo, rf.header.ID, rf.block0IV)
|
|
||||||
|
|
||||||
// Crop down to the relevant part
|
|
||||||
lenHave := len(ciphertext)
|
|
||||||
skip := blocks[0].Skip
|
|
||||||
endWant := int(skip + length)
|
|
||||||
if lenHave > endWant {
|
|
||||||
out = ciphertext[skip:endWant]
|
|
||||||
} else if lenHave > int(skip) {
|
|
||||||
out = ciphertext[skip:lenHave]
|
|
||||||
} // else: out stays empty, file was smaller than the requested offset
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read - FUSE call
|
|
||||||
func (rf *reverseFile) Read(buf []byte, ioff int64) (resultData fuse.ReadResult, status fuse.Status) {
|
|
||||||
length := uint64(len(buf))
|
|
||||||
off := uint64(ioff)
|
|
||||||
var out bytes.Buffer
|
|
||||||
var header []byte
|
|
||||||
|
|
||||||
// Synthesize file header
|
|
||||||
if off < contentenc.HeaderLen {
|
|
||||||
header = rf.header.Pack()
|
|
||||||
// Truncate to requested part
|
|
||||||
end := int(off) + len(buf)
|
|
||||||
if end > len(header) {
|
|
||||||
end = len(header)
|
|
||||||
}
|
|
||||||
header = header[off:end]
|
|
||||||
// Write into output buffer and adjust offsets
|
|
||||||
out.Write(header)
|
|
||||||
hLen := uint64(len(header))
|
|
||||||
off += hLen
|
|
||||||
length -= hLen
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read actual file data
|
|
||||||
if length > 0 {
|
|
||||||
fileData, err := rf.readBackingFile(off, length)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
if len(fileData) == 0 {
|
|
||||||
// If we could not read any actual data, we also don't want to
|
|
||||||
// return the file header. An empty file stays empty in encrypted
|
|
||||||
// form.
|
|
||||||
return nil, fuse.OK
|
|
||||||
}
|
|
||||||
out.Write(fileData)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fuse.ReadResultData(out.Bytes()), fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release - FUSE call, close file
|
|
||||||
func (rf *reverseFile) Release() {
|
|
||||||
rf.fd.Close()
|
|
||||||
}
|
|
@ -1,472 +0,0 @@
|
|||||||
package fusefrontend_reverse
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/configfile"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/contentenc"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/cryptocore"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/fusefrontend"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/inomap"
|
|
||||||
"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
|
|
||||||
// encrypted view of a plaintext directory.
|
|
||||||
type ReverseFS struct {
|
|
||||||
// Embed pathfs.defaultFileSystem for a ENOSYS implementation of all methods
|
|
||||||
pathfs.FileSystem
|
|
||||||
// pathfs.loopbackFileSystem, see go-fuse/fuse/pathfs/loopback.go
|
|
||||||
loopbackfs pathfs.FileSystem
|
|
||||||
// Stores configuration arguments
|
|
||||||
args fusefrontend.Args
|
|
||||||
// Filename encryption helper
|
|
||||||
nameTransform nametransform.NameTransformer
|
|
||||||
// Content encryption helper
|
|
||||||
contentEnc *contentenc.ContentEnc
|
|
||||||
// Tests whether a path is excluded (hiden) from the user. Used by -exclude.
|
|
||||||
excluder ignore.IgnoreParser
|
|
||||||
// inoMap translates inode numbers from different devices to unique inode
|
|
||||||
// numbers.
|
|
||||||
inoMap *inomap.InoMap
|
|
||||||
}
|
|
||||||
|
|
||||||
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.NameTransformer) *ReverseFS {
|
|
||||||
initLongnameCache()
|
|
||||||
fs := &ReverseFS{
|
|
||||||
// pathfs.defaultFileSystem returns ENOSYS for all operations
|
|
||||||
FileSystem: pathfs.NewDefaultFileSystem(),
|
|
||||||
loopbackfs: pathfs.NewLoopbackFileSystem(args.Cipherdir),
|
|
||||||
args: args,
|
|
||||||
nameTransform: n,
|
|
||||||
contentEnc: c,
|
|
||||||
inoMap: inomap.New(),
|
|
||||||
}
|
|
||||||
fs.prepareExcluder(args)
|
|
||||||
return fs
|
|
||||||
}
|
|
||||||
|
|
||||||
// relDir is identical to filepath.Dir excepts that it returns "" when
|
|
||||||
// filepath.Dir would return ".".
|
|
||||||
// In the FUSE API, the root directory is called "", and we actually want that.
|
|
||||||
func relDir(path string) string {
|
|
||||||
dir := filepath.Dir(path)
|
|
||||||
if dir == "." {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
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 gocryptfs.longname.*.name file for a file with a long name
|
|
||||||
typeName
|
|
||||||
// The config file gocryptfs.conf
|
|
||||||
typeConfig
|
|
||||||
)
|
|
||||||
|
|
||||||
// getFileType returns the type of file (one of the fileType constants above).
|
|
||||||
func (rfs *ReverseFS) getFileType(cPath string) fileType {
|
|
||||||
if !rfs.args.PlaintextNames {
|
|
||||||
cName := filepath.Base(cPath)
|
|
||||||
// Is it a gocryptfs.diriv file?
|
|
||||||
if cName == nametransform.DirIVFilename {
|
|
||||||
return typeDiriv
|
|
||||||
}
|
|
||||||
// Is it a gocryptfs.longname.*.name file?
|
|
||||||
if t := nametransform.NameType(cName); t == nametransform.LongNameFilename {
|
|
||||||
return typeName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rfs.isTranslatedConfig(cPath) {
|
|
||||||
return typeConfig
|
|
||||||
}
|
|
||||||
return typeRegular
|
|
||||||
}
|
|
||||||
|
|
||||||
// isTranslatedConfig returns true if the default config file name is in use
|
|
||||||
// and the ciphertext path is "gocryptfs.conf".
|
|
||||||
// "gocryptfs.conf" then maps to ".gocryptfs.reverse.conf" in the plaintext
|
|
||||||
// directory.
|
|
||||||
func (rfs *ReverseFS) isTranslatedConfig(relPath string) bool {
|
|
||||||
if rfs.args.ConfigCustom {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if relPath == configfile.ConfDefaultName {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAttr - FUSE call
|
|
||||||
// "relPath" is the relative ciphertext path
|
|
||||||
func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
|
|
||||||
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 ftype == typeConfig {
|
|
||||||
absConfPath, _ := rfs.abs(configfile.ConfReverseName, nil)
|
|
||||||
var st syscall.Stat_t
|
|
||||||
err = syscall.Lstat(absConfPath, &st)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
rfs.inoMap.TranslateStat(&st)
|
|
||||||
var a fuse.Attr
|
|
||||||
a.FromStat(&st)
|
|
||||||
if rfs.args.ForceOwner != nil {
|
|
||||||
a.Owner = *rfs.args.ForceOwner
|
|
||||||
}
|
|
||||||
return &a, fuse.OK
|
|
||||||
}
|
|
||||||
// Handle virtual files (gocryptfs.diriv, *.name)
|
|
||||||
var f nodefs.File
|
|
||||||
var status fuse.Status
|
|
||||||
virtual := false
|
|
||||||
if ftype == typeDiriv {
|
|
||||||
virtual = true
|
|
||||||
f, status = rfs.newDirIVFile(relPath)
|
|
||||||
}
|
|
||||||
if ftype == typeName {
|
|
||||||
virtual = true
|
|
||||||
f, status = rfs.newNameFile(relPath)
|
|
||||||
}
|
|
||||||
if virtual {
|
|
||||||
if !status.Ok() {
|
|
||||||
tlog.Warn.Printf("GetAttr %q: newXFile failed: %v\n", relPath, status)
|
|
||||||
return nil, status
|
|
||||||
}
|
|
||||||
var a fuse.Attr
|
|
||||||
status = f.GetAttr(&a)
|
|
||||||
if rfs.args.ForceOwner != nil {
|
|
||||||
a.Owner = *rfs.args.ForceOwner
|
|
||||||
}
|
|
||||||
return &a, status
|
|
||||||
}
|
|
||||||
// Normal file / directory
|
|
||||||
dirfd, name, err := rfs.openBackingDir(pPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
// Stat the backing file/dir using Fstatat
|
|
||||||
var st syscall.Stat_t
|
|
||||||
{
|
|
||||||
var st2 unix.Stat_t
|
|
||||||
err = syscallcompat.Fstatat(dirfd, name, &st2, unix.AT_SYMLINK_NOFOLLOW)
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
st = syscallcompat.Unix2syscall(st2)
|
|
||||||
}
|
|
||||||
rfs.inoMap.TranslateStat(&st)
|
|
||||||
var a fuse.Attr
|
|
||||||
a.FromStat(&st)
|
|
||||||
// Calculate encrypted file size
|
|
||||||
if a.IsRegular() {
|
|
||||||
a.Size = rfs.contentEnc.PlainSizeToCipherSize(a.Size)
|
|
||||||
} else if a.IsSymlink() {
|
|
||||||
var linkTarget string
|
|
||||||
var readlinkStatus fuse.Status
|
|
||||||
|
|
||||||
linkTarget, readlinkStatus = rfs.Readlink(relPath, context)
|
|
||||||
if !readlinkStatus.Ok() {
|
|
||||||
return nil, readlinkStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
a.Size = uint64(len(linkTarget))
|
|
||||||
}
|
|
||||||
if rfs.args.ForceOwner != nil {
|
|
||||||
a.Owner = *rfs.args.ForceOwner
|
|
||||||
}
|
|
||||||
return &a, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access - FUSE call
|
|
||||||
func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context) fuse.Status {
|
|
||||||
ftype, excluded, pPath, err := rfs.getFileInfo(relPath)
|
|
||||||
if excluded {
|
|
||||||
return fuse.ENOENT
|
|
||||||
}
|
|
||||||
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
|
|
||||||
if mode == ROK || mode == 0 {
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
||||||
return fuse.EPERM
|
|
||||||
}
|
|
||||||
dirfd, name, err := rfs.openBackingDir(pPath)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
err = syscallcompat.Faccessat(dirfd, name, mode)
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open - FUSE call
|
|
||||||
func (rfs *ReverseFS) Open(relPath string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) {
|
|
||||||
ftype, excluded, pPath, err := rfs.getFileInfo(relPath)
|
|
||||||
if excluded {
|
|
||||||
return nil, fuse.ENOENT
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
if ftype == typeConfig {
|
|
||||||
return rfs.loopbackfs.Open(configfile.ConfReverseName, flags, context)
|
|
||||||
}
|
|
||||||
if ftype == typeDiriv {
|
|
||||||
return rfs.newDirIVFile(relPath)
|
|
||||||
}
|
|
||||||
if ftype == typeName {
|
|
||||||
return rfs.newNameFile(relPath)
|
|
||||||
}
|
|
||||||
return rfs.newFile(relPath, pPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rfs *ReverseFS) openDirPlaintextnames(relPath string, entries []fuse.DirEntry) ([]fuse.DirEntry, fuse.Status) {
|
|
||||||
if relPath != "" || rfs.args.ConfigCustom {
|
|
||||||
return entries, fuse.OK
|
|
||||||
}
|
|
||||||
// We are in the root dir and the default config file name
|
|
||||||
// ".gocryptfs.reverse.conf" is used. We map it to "gocryptfs.conf".
|
|
||||||
dupe := -1
|
|
||||||
status := fuse.OK
|
|
||||||
for i := range entries {
|
|
||||||
if entries[i].Name == configfile.ConfReverseName {
|
|
||||||
entries[i].Name = configfile.ConfDefaultName
|
|
||||||
} else if entries[i].Name == configfile.ConfDefaultName {
|
|
||||||
dupe = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dupe >= 0 {
|
|
||||||
// Warn the user loudly: The gocryptfs.conf_NAME_COLLISION file will
|
|
||||||
// throw ENOENT errors that are hard to miss.
|
|
||||||
tlog.Warn.Printf("The file %q is mapped to %q and shadows another file. Please rename %q in directory %q.",
|
|
||||||
configfile.ConfReverseName, configfile.ConfDefaultName, configfile.ConfDefaultName, rfs.args.Cipherdir)
|
|
||||||
entries[dupe].Name = "gocryptfs.conf_NAME_COLLISION_" + fmt.Sprintf("%d", cryptocore.RandUint64())
|
|
||||||
}
|
|
||||||
return entries, status
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenDir - FUSE readdir call
|
|
||||||
func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) {
|
|
||||||
ftype, excluded, relPath, err := rfs.getFileInfo(cipherPath)
|
|
||||||
if excluded {
|
|
||||||
return nil, fuse.ENOENT
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
fd, err := syscallcompat.Openat(dirfd, filepath.Base(relPath), syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
entries, err := syscallcompat.Getdents(fd)
|
|
||||||
syscall.Close(fd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
if rfs.args.PlaintextNames {
|
|
||||||
var status fuse.Status
|
|
||||||
entries, status = rfs.openDirPlaintextnames(cipherPath, entries)
|
|
||||||
if !status.Ok() {
|
|
||||||
return nil, status
|
|
||||||
}
|
|
||||||
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.
|
|
||||||
virtualFiles := make([]fuse.DirEntry, len(entries)+1)
|
|
||||||
// Virtual gocryptfs.diriv file
|
|
||||||
virtualFiles[0] = fuse.DirEntry{
|
|
||||||
Mode: virtualFileMode,
|
|
||||||
Name: nametransform.DirIVFilename,
|
|
||||||
}
|
|
||||||
// Actually used entries
|
|
||||||
nVirtual := 1
|
|
||||||
|
|
||||||
// Encrypt names
|
|
||||||
dirIV := pathiv.Derive(cipherPath, pathiv.PurposeDirIV)
|
|
||||||
for i := range entries {
|
|
||||||
var cName string
|
|
||||||
// ".gocryptfs.reverse.conf" in the root directory is mapped to "gocryptfs.conf"
|
|
||||||
if cipherPath == "" && entries[i].Name == configfile.ConfReverseName &&
|
|
||||||
!rfs.args.ConfigCustom {
|
|
||||||
cName = configfile.ConfDefaultName
|
|
||||||
} else {
|
|
||||||
cName = rfs.nameTransform.EncryptName(entries[i].Name, dirIV)
|
|
||||||
if len(cName) > unix.NAME_MAX {
|
|
||||||
cName = rfs.nameTransform.HashLongName(cName)
|
|
||||||
dotNameFile := fuse.DirEntry{
|
|
||||||
Mode: virtualFileMode,
|
|
||||||
Name: cName + nametransform.LongNameSuffix,
|
|
||||||
}
|
|
||||||
virtualFiles[nVirtual] = dotNameFile
|
|
||||||
nVirtual++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entries[i].Name = cName
|
|
||||||
}
|
|
||||||
// Add virtual files
|
|
||||||
entries = append(entries, virtualFiles[:nVirtual]...)
|
|
||||||
return entries, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (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 pDir="" correctly:
|
|
||||||
// Join("", "foo") -> "foo". This does not: pDir + "/" + name"
|
|
||||||
p := filepath.Join(pDir, entry.Name)
|
|
||||||
if rfs.isExcludedPlain(p) {
|
|
||||||
// Skip file
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, entry)
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatFs - FUSE call. Returns information about the filesystem (free space
|
|
||||||
// etc).
|
|
||||||
// Securing statfs against symlink races seems to be more trouble than
|
|
||||||
// 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 {
|
|
||||||
_, excluded, _, err := rfs.getFileInfo(relPath)
|
|
||||||
if excluded || err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var s syscall.Statfs_t
|
|
||||||
err = syscall.Statfs(rfs.args.Cipherdir, &s)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := &fuse.StatfsOut{}
|
|
||||||
out.FromStatfsT(&s)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Readlink - FUSE call
|
|
||||||
func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, fuse.Status) {
|
|
||||||
ftype, excluded, pPath, err := rfs.getFileInfo(relPath)
|
|
||||||
if excluded {
|
|
||||||
return "", fuse.ENOENT
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
// read the link target using Readlinkat
|
|
||||||
plainTarget, err := syscallcompat.Readlinkat(dirfd, name)
|
|
||||||
syscall.Close(dirfd)
|
|
||||||
if err != nil {
|
|
||||||
return "", fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
if rfs.args.PlaintextNames {
|
|
||||||
return plainTarget, fuse.OK
|
|
||||||
}
|
|
||||||
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.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 {
|
|
||||||
return "", fuse.Status(syscall.ENAMETOOLONG)
|
|
||||||
}
|
|
||||||
return cTarget, fuse.OK
|
|
||||||
}
|
|
@ -1,124 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
package fusefrontend_reverse
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/pathiv"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// abs basically returns storage dir + "/" + relPath.
|
|
||||||
// It takes an error parameter so it can directly wrap decryptPath like this:
|
|
||||||
// a, err := rfs.abs(rfs.decryptPath(relPath))
|
|
||||||
// abs never generates an error on its own. In other words, abs(p, nil) never
|
|
||||||
// fails.
|
|
||||||
func (rfs *ReverseFS) abs(relPath string, err error) (string, error) {
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Join(rfs.args.Cipherdir, relPath), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// rDecryptName decrypts the ciphertext name "cName", given the dirIV of the
|
|
||||||
// directory "cName" lies in. The relative plaintext path to the directory
|
|
||||||
// "pDir" is used if a "gocryptfs.longname.XYZ.name" must be resolved.
|
|
||||||
func (rfs *ReverseFS) rDecryptName(cName string, dirIV []byte, pDir string) (pName string, err error) {
|
|
||||||
nameType := nametransform.NameType(cName)
|
|
||||||
if nameType == nametransform.LongNameNone {
|
|
||||||
pName, err = rfs.nameTransform.DecryptName(cName, dirIV)
|
|
||||||
if err != nil {
|
|
||||||
// We get lots of decrypt requests for names like ".Trash" that
|
|
||||||
// are invalid base64. Convert them to ENOENT so the correct
|
|
||||||
// error gets returned to the user.
|
|
||||||
if _, ok := err.(base64.CorruptInputError); ok {
|
|
||||||
return "", syscall.ENOENT
|
|
||||||
}
|
|
||||||
// Stat attempts on the link target of encrypted symlinks.
|
|
||||||
// These are always valid base64 but the length is not a
|
|
||||||
// multiple of 16.
|
|
||||||
if err == syscall.EBADMSG {
|
|
||||||
return "", syscall.ENOENT
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
} else if nameType == nametransform.LongNameContent {
|
|
||||||
pName, err = rfs.findLongnameParent(pDir, dirIV, cName)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// It makes no sense to decrypt a ".name" file. This is a virtual file
|
|
||||||
// that has no representation in the plaintext filesystem. ".name"
|
|
||||||
// files should have already been handled in virtualfile.go.
|
|
||||||
tlog.Warn.Printf("rDecryptName: cannot decrypt virtual file %q", cName)
|
|
||||||
return "", syscall.EINVAL
|
|
||||||
}
|
|
||||||
return pName, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// decryptPath decrypts a relative ciphertext path to a relative plaintext
|
|
||||||
// path.
|
|
||||||
func (rfs *ReverseFS) decryptPath(relPath string) (string, error) {
|
|
||||||
if rfs.args.PlaintextNames || relPath == "" {
|
|
||||||
return relPath, nil
|
|
||||||
}
|
|
||||||
// Check if the parent dir is in the cache
|
|
||||||
cDir := nametransform.Dir(relPath)
|
|
||||||
dirIV, pDir := rPathCache.lookup(cDir)
|
|
||||||
if dirIV != nil {
|
|
||||||
cName := filepath.Base(relPath)
|
|
||||||
pName, err := rfs.rDecryptName(cName, dirIV, pDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Join(pDir, pName), nil
|
|
||||||
}
|
|
||||||
parts := strings.Split(relPath, "/")
|
|
||||||
var transformedParts []string
|
|
||||||
for i := range parts {
|
|
||||||
// Start at the top and recurse
|
|
||||||
currentCipherDir := filepath.Join(parts[:i]...)
|
|
||||||
currentPlainDir := filepath.Join(transformedParts[:i]...)
|
|
||||||
dirIV = pathiv.Derive(currentCipherDir, pathiv.PurposeDirIV)
|
|
||||||
transformedPart, err := rfs.rDecryptName(parts[i], dirIV, currentPlainDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
transformedParts = append(transformedParts, transformedPart)
|
|
||||||
}
|
|
||||||
pRelPath := filepath.Join(transformedParts...)
|
|
||||||
rPathCache.store(cDir, dirIV, nametransform.Dir(pRelPath))
|
|
||||||
return pRelPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
if err != nil {
|
|
||||||
return -1, "", err
|
|
||||||
}
|
|
||||||
pName = filepath.Base(pRelPath)
|
|
||||||
return dirfd, pName, nil
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package fusefrontend_reverse
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// rPathCacheContainer is a simple one entry path cache. Because the dirIV
|
|
||||||
// is generated deterministically from the directory path, there is no need
|
|
||||||
// to ever invalidate entries.
|
|
||||||
type rPathCacheContainer struct {
|
|
||||||
sync.Mutex
|
|
||||||
// Relative ciphertext path to the directory
|
|
||||||
cPath string
|
|
||||||
// Relative plaintext path
|
|
||||||
pPath string
|
|
||||||
// Directory IV of the directory
|
|
||||||
dirIV []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookup relative ciphertext path "cPath". Returns dirIV, relative
|
|
||||||
// plaintext path.
|
|
||||||
func (c *rPathCacheContainer) lookup(cPath string) ([]byte, string) {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
if cPath == c.cPath {
|
|
||||||
// hit
|
|
||||||
return c.dirIV, c.pPath
|
|
||||||
}
|
|
||||||
// miss
|
|
||||||
return nil, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// store - write entry for the directory at relative ciphertext path "cPath"
|
|
||||||
// into the cache.
|
|
||||||
// "dirIV" = directory IV of the directory, "pPath" = relative plaintext path
|
|
||||||
func (c *rPathCacheContainer) store(cPath string, dirIV []byte, pPath string) {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
c.cPath = cPath
|
|
||||||
c.dirIV = dirIV
|
|
||||||
c.pPath = pPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// rPathCache: see rPathCacheContainer above for a detailed description
|
|
||||||
var rPathCache rPathCacheContainer
|
|
@ -1,110 +0,0 @@
|
|||||||
package fusefrontend_reverse
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
|
||||||
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
|
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/inomap"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/pathiv"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// virtualFileMode is the mode to use for virtual files (gocryptfs.diriv and
|
|
||||||
// *.name). They are always readable, as stated in func Access
|
|
||||||
virtualFileMode = syscall.S_IFREG | 0444
|
|
||||||
// We use inomap's `Tag` feature to generate unique inode numbers for
|
|
||||||
// virtual files. These are the tags we use.
|
|
||||||
inoTagDirIV = 1
|
|
||||||
inoTagNameFile = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
func (rfs *ReverseFS) newDirIVFile(cRelPath string) (nodefs.File, fuse.Status) {
|
|
||||||
cDir := nametransform.Dir(cRelPath)
|
|
||||||
dir, err := rfs.decryptPath(cDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
iv := pathiv.Derive(cDir, pathiv.PurposeDirIV)
|
|
||||||
return rfs.newVirtualFile(iv, rfs.args.Cipherdir, dir, inoTagDirIV)
|
|
||||||
}
|
|
||||||
|
|
||||||
type virtualFile struct {
|
|
||||||
// Embed nodefs.defaultFile for a ENOSYS implementation of all methods
|
|
||||||
nodefs.File
|
|
||||||
// pointer to parent filesystem
|
|
||||||
rfs *ReverseFS
|
|
||||||
// file content
|
|
||||||
content []byte
|
|
||||||
// backing directory
|
|
||||||
cipherdir string
|
|
||||||
// path to a parent file (relative to cipherdir)
|
|
||||||
parentFile string
|
|
||||||
// inomap `Tag`.
|
|
||||||
// Depending on the file type, either `inoTagDirIV` or `inoTagNameFile`.
|
|
||||||
inoTag uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
// newVirtualFile creates a new in-memory file that does not have a representation
|
|
||||||
// on disk. "content" is the file content. Timestamps and file owner are copied
|
|
||||||
// from "parentFile" (plaintext path relative to "cipherdir").
|
|
||||||
// For a "gocryptfs.diriv" file, you would use the parent directory as
|
|
||||||
// "parentFile".
|
|
||||||
func (rfs *ReverseFS) newVirtualFile(content []byte, cipherdir string, parentFile string, inoTag uint8) (nodefs.File, fuse.Status) {
|
|
||||||
if inoTag == 0 {
|
|
||||||
log.Panicf("BUG: inoTag for virtual file is zero - this will cause ino collisions!")
|
|
||||||
}
|
|
||||||
return &virtualFile{
|
|
||||||
File: nodefs.NewDefaultFile(),
|
|
||||||
rfs: rfs,
|
|
||||||
content: content,
|
|
||||||
cipherdir: cipherdir,
|
|
||||||
parentFile: parentFile,
|
|
||||||
inoTag: inoTag,
|
|
||||||
}, fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read - FUSE call
|
|
||||||
func (f *virtualFile) Read(buf []byte, off int64) (resultData fuse.ReadResult, status fuse.Status) {
|
|
||||||
if off >= int64(len(f.content)) {
|
|
||||||
return nil, fuse.OK
|
|
||||||
}
|
|
||||||
end := int(off) + len(buf)
|
|
||||||
if end > len(f.content) {
|
|
||||||
end = len(f.content)
|
|
||||||
}
|
|
||||||
return fuse.ReadResultData(f.content[off:end]), fuse.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAttr - FUSE call
|
|
||||||
func (f *virtualFile) GetAttr(a *fuse.Attr) fuse.Status {
|
|
||||||
dir := filepath.Dir(f.parentFile)
|
|
||||||
dirfd, err := syscallcompat.OpenDirNofollow(f.cipherdir, dir)
|
|
||||||
if err != nil {
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
defer syscall.Close(dirfd)
|
|
||||||
name := filepath.Base(f.parentFile)
|
|
||||||
var st2 unix.Stat_t
|
|
||||||
err = syscallcompat.Fstatat(dirfd, name, &st2, unix.AT_SYMLINK_NOFOLLOW)
|
|
||||||
if err != nil {
|
|
||||||
tlog.Debug.Printf("GetAttr: Fstatat %q: %v\n", f.parentFile, err)
|
|
||||||
return fuse.ToStatus(err)
|
|
||||||
}
|
|
||||||
st := syscallcompat.Unix2syscall(st2)
|
|
||||||
q := inomap.NewQIno(uint64(st.Dev), f.inoTag, uint64(st.Ino))
|
|
||||||
st.Ino = f.rfs.inoMap.Translate(q)
|
|
||||||
st.Size = int64(len(f.content))
|
|
||||||
st.Mode = virtualFileMode
|
|
||||||
st.Nlink = 1
|
|
||||||
a.FromStat(&st)
|
|
||||||
return fuse.OK
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user