diff options
Diffstat (limited to 'internal/fusefrontend/file2_allocate_truncate.go')
-rw-r--r-- | internal/fusefrontend/file2_allocate_truncate.go | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/internal/fusefrontend/file2_allocate_truncate.go b/internal/fusefrontend/file2_allocate_truncate.go new file mode 100644 index 0000000..f799a3e --- /dev/null +++ b/internal/fusefrontend/file2_allocate_truncate.go @@ -0,0 +1,217 @@ +package fusefrontend + +// FUSE operations Truncate and Allocate on file handles +// i.e. ftruncate and fallocate + +import ( + "log" + "syscall" + + "github.com/hanwen/go-fuse/v2/fuse" + + "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +// Allocate - FUSE call for fallocate(2) +// +// mode=FALLOC_FL_KEEP_SIZE is implemented directly. +// +// mode=FALLOC_DEFAULT is implemented as a two-step process: +// +// (1) Allocate the space using FALLOC_FL_KEEP_SIZE +// (2) Set the file size using ftruncate (via truncateGrowFile) +// +// This allows us to reuse the file grow mechanics from Truncate as they are +// complicated and hard to get right. +// +// Other modes (hole punching, zeroing) are not supported. +func (f *File2) Allocate(off uint64, sz uint64, mode uint32) fuse.Status { + if mode != FALLOC_DEFAULT && mode != FALLOC_FL_KEEP_SIZE { + f := func() { + tlog.Info.Printf("fallocate: only mode 0 (default) and 1 (keep size) are supported") + } + allocateWarnOnce.Do(f) + return fuse.Status(syscall.EOPNOTSUPP) + } + + f.fdLock.RLock() + defer f.fdLock.RUnlock() + if f.released { + return fuse.EBADF + } + f.fileTableEntry.ContentLock.Lock() + defer f.fileTableEntry.ContentLock.Unlock() + + blocks := f.contentEnc.ExplodePlainRange(off, sz) + firstBlock := blocks[0] + lastBlock := blocks[len(blocks)-1] + + // Step (1): Allocate the space the user wants using FALLOC_FL_KEEP_SIZE. + // This will fill file holes and/or allocate additional space past the end of + // the file. + cipherOff := firstBlock.BlockCipherOff() + cipherSz := lastBlock.BlockCipherOff() - cipherOff + + f.contentEnc.BlockOverhead() + lastBlock.Skip + lastBlock.Length + err := syscallcompat.Fallocate(f.intFd(), FALLOC_FL_KEEP_SIZE, int64(cipherOff), int64(cipherSz)) + tlog.Debug.Printf("Allocate off=%d sz=%d mode=%x cipherOff=%d cipherSz=%d\n", + off, sz, mode, cipherOff, cipherSz) + if err != nil { + return fuse.ToStatus(err) + } + if mode == FALLOC_FL_KEEP_SIZE { + // The user did not want to change the apparent size. We are done. + return fuse.OK + } + // Step (2): Grow the apparent file size + // We need the old file size to determine if we are growing the file at all. + newPlainSz := off + sz + oldPlainSz, err := f.statPlainSize() + if err != nil { + return fuse.ToStatus(err) + } + if newPlainSz <= oldPlainSz { + // The new size is smaller (or equal). Fallocate with mode = 0 never + // truncates a file, so we are done. + return fuse.OK + } + // The file grows. The space has already been allocated in (1), so what is + // left to do is to pad the first and last block and call truncate. + // truncateGrowFile does just that. + return f.truncateGrowFile(oldPlainSz, newPlainSz) +} + +// Truncate - FUSE call +func (f *File2) Truncate(newSize uint64) fuse.Status { + f.fdLock.RLock() + defer f.fdLock.RUnlock() + if f.released { + // The file descriptor has been closed concurrently. + tlog.Warn.Printf("ino%d fh%d: Truncate on released file", f.qIno.Ino, f.intFd()) + return fuse.EBADF + } + f.fileTableEntry.ContentLock.Lock() + defer f.fileTableEntry.ContentLock.Unlock() + var err error + // Common case first: Truncate to zero + if newSize == 0 { + err = syscall.Ftruncate(int(f.fd.Fd()), 0) + if err != nil { + tlog.Warn.Printf("ino%d fh%d: Ftruncate(fd, 0) returned error: %v", f.qIno.Ino, f.intFd(), err) + return fuse.ToStatus(err) + } + // Truncate to zero kills the file header + f.fileTableEntry.ID = nil + return fuse.OK + } + // We need the old file size to determine if we are growing or shrinking + // the file + oldSize, err := f.statPlainSize() + if err != nil { + return fuse.ToStatus(err) + } + + oldB := float32(oldSize) / float32(f.contentEnc.PlainBS()) + newB := float32(newSize) / float32(f.contentEnc.PlainBS()) + tlog.Debug.Printf("ino%d: FUSE Truncate from %.2f to %.2f blocks (%d to %d bytes)", f.qIno.Ino, oldB, newB, oldSize, newSize) + + // File size stays the same - nothing to do + if newSize == oldSize { + return fuse.OK + } + // File grows + if newSize > oldSize { + return f.truncateGrowFile(oldSize, newSize) + } + + // File shrinks + blockNo := f.contentEnc.PlainOffToBlockNo(newSize) + cipherOff := f.contentEnc.BlockNoToCipherOff(blockNo) + plainOff := f.contentEnc.BlockNoToPlainOff(blockNo) + lastBlockLen := newSize - plainOff + var data []byte + if lastBlockLen > 0 { + var status fuse.Status + data, status = f.doRead(nil, plainOff, lastBlockLen) + if status != fuse.OK { + tlog.Warn.Printf("Truncate: shrink doRead returned error: %v", err) + return status + } + } + // Truncate down to the last complete block + err = syscall.Ftruncate(int(f.fd.Fd()), int64(cipherOff)) + if err != nil { + tlog.Warn.Printf("Truncate: shrink Ftruncate returned error: %v", err) + return fuse.ToStatus(err) + } + // Append partial block + if lastBlockLen > 0 { + _, status := f.doWrite(data, int64(plainOff)) + return status + } + return fuse.OK +} + +// statPlainSize stats the file and returns the plaintext size +func (f *File2) statPlainSize() (uint64, error) { + fi, err := f.fd.Stat() + if err != nil { + tlog.Warn.Printf("ino%d fh%d: statPlainSize: %v", f.qIno.Ino, f.intFd(), err) + return 0, err + } + cipherSz := uint64(fi.Size()) + plainSz := uint64(f.contentEnc.CipherSizeToPlainSize(cipherSz)) + return plainSz, nil +} + +// truncateGrowFile extends a file using seeking or ftruncate performing RMW on +// the first and last block as necessary. New blocks in the middle become +// file holes unless they have been fallocate()'d beforehand. +func (f *File2) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) fuse.Status { + if newPlainSz <= oldPlainSz { + log.Panicf("BUG: newSize=%d <= oldSize=%d", newPlainSz, oldPlainSz) + } + newEOFOffset := newPlainSz - 1 + if oldPlainSz > 0 { + n1 := f.contentEnc.PlainOffToBlockNo(oldPlainSz - 1) + n2 := f.contentEnc.PlainOffToBlockNo(newEOFOffset) + // The file is grown within one block, no need to pad anything. + // Write a single zero to the last byte and let doWrite figure out the RMW. + if n1 == n2 { + buf := make([]byte, 1) + _, status := f.doWrite(buf, int64(newEOFOffset)) + return status + } + } + // The truncate creates at least one new block. + // + // Make sure the old last block is padded to the block boundary. This call + // is a no-op if it is already block-aligned. + status := f.zeroPad(oldPlainSz) + if !status.Ok() { + return status + } + // The new size is block-aligned. In this case we can do everything ourselves + // and avoid the call to doWrite. + if newPlainSz%f.contentEnc.PlainBS() == 0 { + // The file was empty, so it did not have a header. Create one. + if oldPlainSz == 0 { + id, err := f.createHeader() + if err != nil { + return fuse.ToStatus(err) + } + f.fileTableEntry.ID = id + } + cSz := int64(f.contentEnc.PlainSizeToCipherSize(newPlainSz)) + err := syscall.Ftruncate(f.intFd(), cSz) + if err != nil { + tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err) + } + return fuse.ToStatus(err) + } + // The new size is NOT aligned, so we need to write a partial block. + // Write a single zero to the last byte and let doWrite figure it out. + buf := make([]byte, 1) + _, status = f.doWrite(buf, int64(newEOFOffset)) + return status +} |