From 6d4f1a6888cafd218cb97bd11de6a7553d9bc8f1 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 9 Aug 2020 22:11:46 +0200 Subject: v2api/reverse: implement Read --- internal/fusefrontend_reverse/file.go | 70 ++++++++++++++ internal/fusefrontend_reverse/file_api_check.go | 23 +++++ internal/fusefrontend_reverse/file_helpers.go | 62 +++++++++++++ internal/fusefrontend_reverse/node.go | 86 +++++++++++++++-- internal/fusefrontend_reverse/node_api_check.go | 6 +- internal/fusefrontend_reverse/node_helpers.go | 33 ++++++- internal/fusefrontend_reverse/virtualconf.go | 55 +++++++++++ internal/fusefrontend_reverse/virtualfile.go | 115 ----------------------- internal/fusefrontend_reverse/virtualnode.go | 117 ++++++++++++++++++++++++ 9 files changed, 435 insertions(+), 132 deletions(-) create mode 100644 internal/fusefrontend_reverse/file.go create mode 100644 internal/fusefrontend_reverse/file_api_check.go create mode 100644 internal/fusefrontend_reverse/file_helpers.go create mode 100644 internal/fusefrontend_reverse/virtualconf.go delete mode 100644 internal/fusefrontend_reverse/virtualfile.go create mode 100644 internal/fusefrontend_reverse/virtualnode.go diff --git a/internal/fusefrontend_reverse/file.go b/internal/fusefrontend_reverse/file.go new file mode 100644 index 0000000..55f5f80 --- /dev/null +++ b/internal/fusefrontend_reverse/file.go @@ -0,0 +1,70 @@ +package fusefrontend_reverse + +import ( + "bytes" + "context" + "os" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + + "github.com/rfjakob/gocryptfs/internal/contentenc" +) + +type File struct { + // 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 +} + +// Read - FUSE call +func (f *File) Read(ctx context.Context, buf []byte, ioff int64) (resultData fuse.ReadResult, errno syscall.Errno) { + length := uint64(len(buf)) + off := uint64(ioff) + out := bytes.NewBuffer(buf[:0]) + var header []byte + + // Synthesize file header + if off < contentenc.HeaderLen { + header = f.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 := f.readBackingFile(off, length) + if err != nil { + return nil, fs.ToErrno(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, 0 + } + out.Write(fileData) + } + + return fuse.ReadResultData(out.Bytes()), 0 +} + +// Release - FUSE call, close file +func (f *File) Release(context.Context) syscall.Errno { + return fs.ToErrno(f.fd.Close()) +} diff --git a/internal/fusefrontend_reverse/file_api_check.go b/internal/fusefrontend_reverse/file_api_check.go new file mode 100644 index 0000000..6dd1771 --- /dev/null +++ b/internal/fusefrontend_reverse/file_api_check.go @@ -0,0 +1,23 @@ +package fusefrontend_reverse + +import ( + "github.com/hanwen/go-fuse/v2/fs" +) + +// Check that we have implemented the fs.File* interfaces +var _ = (fs.FileReader)((*File)(nil)) +var _ = (fs.FileReleaser)((*File)(nil)) + +/* TODO +var _ = (fs.FileGetattrer)((*File2)(nil)) +var _ = (fs.FileSetattrer)((*File2)(nil)) +var _ = (fs.FileWriter)((*File2)(nil)) +var _ = (fs.FileFsyncer)((*File2)(nil)) +var _ = (fs.FileFlusher)((*File2)(nil)) +var _ = (fs.FileAllocater)((*File2)(nil)) +var _ = (fs.FileLseeker)((*File2)(nil)) +var _ = (fs.FileHandle)((*File2)(nil)) +var _ = (fs.FileGetlker)((*File2)(nil)) +var _ = (fs.FileSetlker)((*File2)(nil)) +var _ = (fs.FileSetlkwer)((*File2)(nil)) +*/ diff --git a/internal/fusefrontend_reverse/file_helpers.go b/internal/fusefrontend_reverse/file_helpers.go new file mode 100644 index 0000000..f024e69 --- /dev/null +++ b/internal/fusefrontend_reverse/file_helpers.go @@ -0,0 +1,62 @@ +package fusefrontend_reverse + +import ( + "bytes" + "io" + "sync" + + "github.com/rfjakob/gocryptfs/internal/contentenc" + "github.com/rfjakob/gocryptfs/internal/pathiv" + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +var inodeTable sync.Map + +// encryptBlocks - encrypt "plaintext" into a number of ciphertext blocks. +// "plaintext" must already be block-aligned. +func (rf *File) 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 (f *File) readBackingFile(off uint64, length uint64) (out []byte, err error) { + blocks := f.contentEnc.ExplodeCipherRange(off, length) + + // Read the backing plaintext in one go + alignedOffset, alignedLength := contentenc.JointPlaintextRange(blocks) + plaintext := make([]byte, int(alignedLength)) + n, err := f.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 := f.encryptBlocks(plaintext, blocks[0].BlockNo, f.header.ID, f.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 +} diff --git a/internal/fusefrontend_reverse/node.go b/internal/fusefrontend_reverse/node.go index 884a97e..deaf953 100644 --- a/internal/fusefrontend_reverse/node.go +++ b/internal/fusefrontend_reverse/node.go @@ -2,6 +2,8 @@ package fusefrontend_reverse import ( "context" + "fmt" + "os" "path/filepath" "syscall" @@ -10,8 +12,10 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" - "github.com/rfjakob/gocryptfs/internal/configfile" + "github.com/rfjakob/gocryptfs/internal/contentenc" + "github.com/rfjakob/gocryptfs/internal/pathiv" "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" ) // Node is a file or directory in the filesystem tree @@ -33,15 +37,7 @@ func (n *Node) Lookup(ctx context.Context, cName string, out *fuse.EntryOut) (ch return n.lookupLongnameName(ctx, cName, out) } else if t == typeConfig { // gocryptfs.conf - var err error - pName = configfile.ConfReverseName - rn := n.rootNode() - dirfd, err = syscallcompat.OpenDirNofollow(rn.args.Cipherdir, "") - if err != nil { - errno = fs.ToErrno(err) - return - } - defer syscall.Close(dirfd) + return n.lookupConf(ctx, out) } else if t == typeReal { // real file dirfd, pName, errno = n.prepareAtSyscall(cName) @@ -112,3 +108,73 @@ func (n *Node) Readlink(ctx context.Context) (out []byte, errno syscall.Errno) { cName := filepath.Base(n.Path()) return n.readlink(dirfd, cName, pName) } + +// Open - FUSE call. Open already-existing file. +// +// Symlink-safe through Openat(). +func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + dirfd, pName, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + fd, err := syscallcompat.Openat(dirfd, pName, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0) + if err != nil { + errno = fs.ToErrno(err) + return + } + + // Reject access if the file descriptor does not refer to a regular file. + var st syscall.Stat_t + err = syscall.Fstat(fd, &st) + if err != nil { + tlog.Warn.Printf("Open: Fstat error: %v", err) + syscall.Close(fd) + errno = fs.ToErrno(err) + return + } + 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) + errno = syscall.EACCES + return + } + // 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 { + p := n.Path() + derivedIVs = pathiv.DeriveFile(p) + // 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, + } + fh = &File{ + fd: os.NewFile(uintptr(fd), fmt.Sprintf("fd%d", fd)), + header: header, + block0IV: derivedIVs.Block0IV, + contentEnc: n.rootNode().contentEnc, + } + return +} diff --git a/internal/fusefrontend_reverse/node_api_check.go b/internal/fusefrontend_reverse/node_api_check.go index e926fc3..875c9b9 100644 --- a/internal/fusefrontend_reverse/node_api_check.go +++ b/internal/fusefrontend_reverse/node_api_check.go @@ -9,11 +9,10 @@ var _ = (fs.NodeGetattrer)((*Node)(nil)) var _ = (fs.NodeLookuper)((*Node)(nil)) var _ = (fs.NodeReaddirer)((*Node)(nil)) var _ = (fs.NodeReadlinker)((*Node)(nil)) - -/* TODO var _ = (fs.NodeOpener)((*Node)(nil)) + +/* var _ = (fs.NodeStatfser)((*Node)(nil)) -var _ = (fs.NodeMknoder)((*Node)(nil)) var _ = (fs.NodeGetxattrer)((*Node)(nil)) var _ = (fs.NodeListxattrer)((*Node)(nil)) */ @@ -23,6 +22,7 @@ var _ = (fs.NodeOpendirer)((*Node)(nil)) */ /* Will not implement these - reverse mode is read-only! +var _ = (fs.NodeMknoder)((*Node)(nil)) var _ = (fs.NodeCreater)((*Node)(nil)) var _ = (fs.NodeMkdirer)((*Node)(nil)) var _ = (fs.NodeRmdirer)((*Node)(nil)) diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go index 7669a17..fd0abfc 100644 --- a/internal/fusefrontend_reverse/node_helpers.go +++ b/internal/fusefrontend_reverse/node_helpers.go @@ -10,6 +10,7 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" + "github.com/rfjakob/gocryptfs/internal/configfile" "github.com/rfjakob/gocryptfs/internal/pathiv" "github.com/rfjakob/gocryptfs/internal/syscallcompat" ) @@ -115,8 +116,8 @@ func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fus errno = fs.ToErrno(err) return } - var vf *virtualFile - vf, errno = n.newVirtualFile([]byte(cFullname), st, inoTagNameFile) + var vf *VirtualMemNode + vf, errno = n.newVirtualMemNode([]byte(cFullname), st, inoTagNameFile) if errno != 0 { return nil, errno } @@ -141,8 +142,8 @@ func (n *Node) lookupDiriv(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inod return } content := pathiv.Derive(n.Path(), pathiv.PurposeDirIV) - var vf *virtualFile - vf, errno = n.newVirtualFile(content, st, inoTagDirIV) + var vf *VirtualMemNode + vf, errno = n.newVirtualMemNode(content, st, inoTagDirIV) if errno != 0 { return nil, errno } @@ -153,6 +154,30 @@ func (n *Node) lookupDiriv(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inod return } +// lookupConf returns a new Inode for the gocryptfs.conf file +func (n *Node) lookupConf(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inode, errno syscall.Errno) { + rn := n.rootNode() + p := filepath.Join(rn.args.Cipherdir, configfile.ConfReverseName) + var st syscall.Stat_t + err := syscall.Stat(p, &st) + if err != nil { + errno = fs.ToErrno(err) + return + } + // Get unique inode number + rn.inoMap.TranslateStat(&st) + out.Attr.FromStat(&st) + // Create child node + id := fs.StableAttr{ + Mode: uint32(st.Mode), + Gen: 1, + Ino: st.Ino, + } + node := &VirtualConfNode{path: p} + ch = n.NewInode(ctx, node, id) + return +} + // readlink reads and encrypts a symlink. Used by Readlink, Getattr, Lookup. func (n *Node) readlink(dirfd int, cName string, pName string) (out []byte, errno syscall.Errno) { plainTarget, err := syscallcompat.Readlinkat(dirfd, pName) diff --git a/internal/fusefrontend_reverse/virtualconf.go b/internal/fusefrontend_reverse/virtualconf.go new file mode 100644 index 0000000..8620f6d --- /dev/null +++ b/internal/fusefrontend_reverse/virtualconf.go @@ -0,0 +1,55 @@ +package fusefrontend_reverse + +import ( + "context" + "sync" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" +) + +var _ = (fs.NodeOpener)((*VirtualConfNode)(nil)) + +type VirtualConfNode struct { + fs.Inode + + path string +} + +func (n *VirtualConfNode) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + fd, err := syscall.Open(n.path, syscall.O_RDONLY, 0) + if err != nil { + errno = fs.ToErrno(err) + return + } + fh = &VirtualConfFile{fd: fd} + return +} + +// Check that we have implemented the fs.File* interfaces +var _ = (fs.FileReader)((*VirtualConfFile)(nil)) +var _ = (fs.FileReleaser)((*VirtualConfFile)(nil)) + +type VirtualConfFile struct { + mu sync.Mutex + fd int +} + +func (f *VirtualConfFile) Read(ctx context.Context, buf []byte, off int64) (res fuse.ReadResult, errno syscall.Errno) { + f.mu.Lock() + defer f.mu.Unlock() + res = fuse.ReadResultFd(uintptr(f.fd), off, len(buf)) + return +} + +func (f *VirtualConfFile) Release(ctx context.Context) syscall.Errno { + f.mu.Lock() + defer f.mu.Unlock() + if f.fd != -1 { + err := syscall.Close(f.fd) + f.fd = -1 + return fs.ToErrno(err) + } + return syscall.EBADF +} diff --git a/internal/fusefrontend_reverse/virtualfile.go b/internal/fusefrontend_reverse/virtualfile.go deleted file mode 100644 index a7f6913..0000000 --- a/internal/fusefrontend_reverse/virtualfile.go +++ /dev/null @@ -1,115 +0,0 @@ -package fusefrontend_reverse - -import ( - "context" - "log" - "syscall" - - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" - - "github.com/rfjakob/gocryptfs/internal/configfile" - "github.com/rfjakob/gocryptfs/internal/inomap" - "github.com/rfjakob/gocryptfs/internal/nametransform" -) - -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 -) - -type fileType int - -// Values returned by lookupFileType -const ( - // A real file/directory/symlink in the backing plaintext directory - typeReal 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 -) - -// lookupFileType returns the type of child file name -// (one of the fileType constants above). Called from Lookup(). -func (n *Node) lookupFileType(cName string) fileType { - rn := n.rootNode() - // In -plaintextname mode, neither diriv nor longname files exist. - if !rn.args.PlaintextNames { - // 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 - } - } - // gocryptfs.conf in the root directory. This is passed through to - // .gocryptfs.reverse.conf in the backing plaintext directory. - if n.isRoot() && !rn.args.ConfigCustom && cName == configfile.ConfDefaultName { - return typeConfig - } - return typeReal -} - -type virtualFile struct { - fs.Inode - - // file content - content []byte - // attributes for Getattr() - attr fuse.Attr -} - -// 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" (file descriptor). -// For a "gocryptfs.diriv" file, you would use the parent directory as -// "parentFile". -func (n *Node) newVirtualFile(content []byte, parentStat *syscall.Stat_t, inoTag uint8) (vf *virtualFile, errno syscall.Errno) { - if inoTag == 0 { - log.Panicf("BUG: inoTag for virtual file is zero - this will cause ino collisions!") - } - - // Adjust inode number and size - rn := n.rootNode() - st := parentStat - q := inomap.NewQIno(uint64(st.Dev), inoTag, uint64(st.Ino)) - st.Ino = rn.inoMap.Translate(q) - st.Size = int64(len(content)) - st.Mode = virtualFileMode - st.Nlink = 1 - var a fuse.Attr - a.FromStat(st) - - vf = &virtualFile{content: content, attr: a} - return -} - -// Open - FUSE call -func (f *virtualFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - return nil, fuse.FOPEN_KEEP_CACHE, 0 -} - -// GetAttr - FUSE call -func (f *virtualFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - out.Attr = f.attr - return 0 -} - -// Read - FUSE call -func (f *virtualFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { - end := int(off) + len(dest) - if end > len(f.content) { - end = len(f.content) - } - return fuse.ReadResultData(f.content[off:end]), 0 -} diff --git a/internal/fusefrontend_reverse/virtualnode.go b/internal/fusefrontend_reverse/virtualnode.go new file mode 100644 index 0000000..2ee9548 --- /dev/null +++ b/internal/fusefrontend_reverse/virtualnode.go @@ -0,0 +1,117 @@ +package fusefrontend_reverse + +import ( + "context" + "log" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + + "github.com/rfjakob/gocryptfs/internal/configfile" + "github.com/rfjakob/gocryptfs/internal/inomap" + "github.com/rfjakob/gocryptfs/internal/nametransform" +) + +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 +) + +type fileType int + +// Values returned by lookupFileType +const ( + // A real file/directory/symlink in the backing plaintext directory + typeReal 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 +) + +// lookupFileType returns the type of child file name +// (one of the fileType constants above). Called from Lookup(). +func (n *Node) lookupFileType(cName string) fileType { + rn := n.rootNode() + // In -plaintextname mode, neither diriv nor longname files exist. + if !rn.args.PlaintextNames { + // 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 + } + } + // gocryptfs.conf in the root directory. This is passed through to + // .gocryptfs.reverse.conf in the backing plaintext directory. + if n.isRoot() && !rn.args.ConfigCustom && cName == configfile.ConfDefaultName { + return typeConfig + } + return typeReal +} + +// VirtualMemNode is an in-memory node that does not have a representation +// on disk. +type VirtualMemNode struct { + fs.Inode + + // file content + content []byte + // attributes for Getattr() + attr fuse.Attr +} + +// newVirtualMemNode 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" (file descriptor). +// For a "gocryptfs.diriv" file, you would use the parent directory as +// "parentFile". +func (n *Node) newVirtualMemNode(content []byte, parentStat *syscall.Stat_t, inoTag uint8) (vf *VirtualMemNode, errno syscall.Errno) { + if inoTag == 0 { + log.Panicf("BUG: inoTag for virtual file is zero - this will cause ino collisions!") + } + + // Adjust inode number and size + rn := n.rootNode() + st := parentStat + q := inomap.NewQIno(uint64(st.Dev), inoTag, uint64(st.Ino)) + st.Ino = rn.inoMap.Translate(q) + st.Size = int64(len(content)) + st.Mode = virtualFileMode + st.Nlink = 1 + var a fuse.Attr + a.FromStat(st) + + vf = &VirtualMemNode{content: content, attr: a} + return +} + +// Open - FUSE call +func (f *VirtualMemNode) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + return nil, fuse.FOPEN_KEEP_CACHE, 0 +} + +// GetAttr - FUSE call +func (f *VirtualMemNode) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + out.Attr = f.attr + return 0 +} + +// Read - FUSE call +func (f *VirtualMemNode) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { + end := int(off) + len(dest) + if end > len(f.content) { + end = len(f.content) + } + return fuse.ReadResultData(f.content[off:end]), 0 +} -- cgit v1.2.3