diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/fusefrontend/file.go | 2 | ||||
| -rw-r--r-- | internal/fusefrontend/file_allocate_truncate.go | 159 | 
2 files changed, 122 insertions, 39 deletions
| diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 1835b53..ead253f 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -208,8 +208,6 @@ func (f *file) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fus  	return fuse.ReadResultData(out), status  } -const FALLOC_FL_KEEP_SIZE = 0x01 -  // doWrite - encrypt "data" and write it to plaintext offset "off"  //  // Arguments do not have to be block-aligned, read-modify-write is diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go index 5be7df4..65d6df6 100644 --- a/internal/fusefrontend/file_allocate_truncate.go +++ b/internal/fusefrontend/file_allocate_truncate.go @@ -4,6 +4,7 @@ package fusefrontend  // i.e. ftruncate and fallocate  import ( +	"log"  	"sync"  	"syscall" @@ -12,17 +13,78 @@ import (  	"github.com/rfjakob/gocryptfs/internal/tlog"  ) +const FALLOC_DEFAULT = 0x00 +const FALLOC_FL_KEEP_SIZE = 0x01 +  // Only warn once  var allocateWarnOnce sync.Once -// Allocate - FUSE call, fallocate(2) -// This is not implemented yet in gocryptfs, but it is neither in EncFS. This -// suggests that the user demand is low. +// 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 *file) Allocate(off uint64, sz uint64, mode uint32) fuse.Status { -	allocateWarnOnce.Do(func() { -		tlog.Warn.Printf("fallocate(2) is not supported, returning ENOSYS - see https://github.com/rfjakob/gocryptfs/issues/1") -	}) -	return fuse.ENOSYS +	if mode != FALLOC_DEFAULT && mode != FALLOC_FL_KEEP_SIZE { +		f := func() { +			tlog.Warn.Print("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 +	} +	wlock.lock(f.ino) +	defer wlock.unlock(f.ino) + +	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.PlainSizeToCipherSize(lastBlock.Skip+lastBlock.Length) +	err := syscall.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 @@ -50,13 +112,10 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  	}  	// We need the old file size to determine if we are growing or shrinking  	// the file -	fi, err := f.fd.Stat() +	oldSize, err := f.statPlainSize()  	if err != nil { -		tlog.Warn.Printf("ino%d fh%d: Truncate: Fstat failed: %v", f.ino, f.intFd(), err)  		return fuse.ToStatus(err) -	} -	oldSize := f.contentEnc.CipherSizeToPlainSize(uint64(fi.Size())) -	{ +	} else {  		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.ino, oldB, newB, oldSize, newSize) @@ -67,31 +126,7 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  	}  	// File grows  	if newSize > oldSize { -		// File was empty, create new header -		if oldSize == 0 { -			err = f.createHeader() -			if err != nil { -				return fuse.ToStatus(err) -			} -		} -		// New blocks to add -		addBlocks := f.contentEnc.ExplodePlainRange(oldSize, newSize-oldSize) -		if len(addBlocks) >= 2 { -			f.zeroPad(oldSize) -		} -		lastBlock := addBlocks[len(addBlocks)-1] -		if lastBlock.IsPartial() { -			off := lastBlock.BlockPlainOff() -			_, status := f.doWrite(make([]byte, lastBlock.Length), int64(off+lastBlock.Skip)) -			return status -		} else { -			off := lastBlock.BlockCipherOff() -			err = syscall.Ftruncate(f.intFd(), int64(off+f.contentEnc.CipherBS())) -			if err != nil { -				tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err) -			} -			return fuse.ToStatus(err) -		} +		return f.truncateGrowFile(oldSize, newSize)  	} else {  		// File shrinks  		blockNo := f.contentEnc.PlainOffToBlockNo(newSize) @@ -121,3 +156,53 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  		return fuse.OK  	}  } + +// statPlainSize stats the file and returns the plaintext size +func (f *file) statPlainSize() (uint64, error) { +	fi, err := f.fd.Stat() +	if err != nil { +		tlog.Warn.Printf("ino%d fh%d: statPlainSize: %v", f.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 neccessary. New blocks in the middle become +// file holes unless they have been fallocate()'d beforehand. +func (f *file) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) fuse.Status { +	if newPlainSz <= oldPlainSz { +		log.Panicf("BUG: newSize=%d <= oldSize=%d", newPlainSz, oldPlainSz) +	} +	var err error +	// File was empty, create new header +	if oldPlainSz == 0 { +		err = f.createHeader() +		if err != nil { +			return fuse.ToStatus(err) +		} +	} +	// New blocks to add +	addBlocks := f.contentEnc.ExplodePlainRange(oldPlainSz, newPlainSz-oldPlainSz) +	if oldPlainSz > 0 && len(addBlocks) >= 2 { +		// Zero-pad the first block (unless the first block is also the last block) +		f.zeroPad(oldPlainSz) +	} +	lastBlock := addBlocks[len(addBlocks)-1] +	if lastBlock.IsPartial() { +		// Write at the new end of the file. The seek implicitly grows the file +		// (creates a file hole) and doWrite() takes care of RMW. +		off := lastBlock.BlockPlainOff() +		_, status := f.doWrite(make([]byte, lastBlock.Length), int64(off+lastBlock.Skip)) +		return status +	} else { +		off := lastBlock.BlockCipherOff() +		err = syscall.Ftruncate(f.intFd(), int64(off+f.contentEnc.CipherBS())) +		if err != nil { +			tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err) +		} +		return fuse.ToStatus(err) +	} +} | 
