@@ -0,0 +1,24 @@
|
||||
// Copyright (C) 2020 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type copyRangeImplementationBasicFile func(src, dst basicFile, srcOffset, dstOffset, size int64) error
|
||||
|
||||
func copyRangeImplementationForBasicFile(impl copyRangeImplementationBasicFile) copyRangeImplementation {
|
||||
return func(src, dst File, srcOffset, dstOffset, size int64) error {
|
||||
srcFile, srcOk := src.(basicFile)
|
||||
dstFile, dstOk := dst.(basicFile)
|
||||
if !srcOk || !dstOk {
|
||||
return syscall.ENOTSUP
|
||||
}
|
||||
return impl(srcFile, dstFile, srcOffset, dstOffset, size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2019 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build linux
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerCopyRangeImplementation(CopyRangeMethodCopyFileRange, copyRangeImplementationForBasicFile(copyRangeCopyFileRange))
|
||||
}
|
||||
|
||||
func copyRangeCopyFileRange(src, dst basicFile, srcOffset, dstOffset, size int64) error {
|
||||
for size > 0 {
|
||||
// From MAN page:
|
||||
//
|
||||
// If off_in is not NULL, then off_in must point to a buffer that
|
||||
// specifies the starting offset where bytes from fd_in will be read.
|
||||
// The file offset of fd_in is not changed, but off_in is adjusted
|
||||
// appropriately.
|
||||
//
|
||||
// Also, even if explicitly not stated, the same is true for dstOffset
|
||||
n, err := unix.CopyFileRange(int(src.Fd()), &srcOffset, int(dst.Fd()), &dstOffset, int(size), 0)
|
||||
if n == 0 && err == nil {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if err != nil && err != syscall.EAGAIN {
|
||||
return err
|
||||
}
|
||||
// Handle case where err == EAGAIN and n == -1 (it's not clear if that can happen)
|
||||
if n > 0 {
|
||||
size -= int64(n)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (C) 2019 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !windows,!solaris,!darwin
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerCopyRangeImplementation(CopyRangeMethodIoctl, copyRangeImplementationForBasicFile(copyRangeIoctl))
|
||||
}
|
||||
|
||||
const FICLONE = 0x40049409
|
||||
const FICLONERANGE = 0x4020940d
|
||||
|
||||
/*
|
||||
http://man7.org/linux/man-pages/man2/ioctl_ficlonerange.2.html
|
||||
|
||||
struct file_clone_range {
|
||||
__s64 src_fd;
|
||||
__u64 src_offset;
|
||||
__u64 src_length;
|
||||
__u64 dest_offset;
|
||||
};
|
||||
*/
|
||||
type fileCloneRange struct {
|
||||
srcFd int64
|
||||
srcOffset uint64
|
||||
srcLength uint64
|
||||
dstOffset uint64
|
||||
}
|
||||
|
||||
func copyRangeIoctl(src, dst basicFile, srcOffset, dstOffset, size int64) error {
|
||||
fi, err := src.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// https://www.man7.org/linux/man-pages/man2/ioctl_ficlonerange.2.html
|
||||
// If src_length is zero, the ioctl reflinks to the end of the source file.
|
||||
if srcOffset+size == fi.Size() {
|
||||
size = 0
|
||||
}
|
||||
|
||||
if srcOffset == 0 && dstOffset == 0 && size == 0 {
|
||||
// Optimization for whole file copies.
|
||||
_, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, dst.Fd(), FICLONE, src.Fd())
|
||||
if errNo != 0 {
|
||||
return errNo
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
params := fileCloneRange{
|
||||
srcFd: int64(src.Fd()),
|
||||
srcOffset: uint64(srcOffset),
|
||||
srcLength: uint64(size),
|
||||
dstOffset: uint64(dstOffset),
|
||||
}
|
||||
_, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, dst.Fd(), FICLONERANGE, uintptr(unsafe.Pointer(¶ms)))
|
||||
if errNo != 0 {
|
||||
return errNo
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (C) 2019 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !windows,!darwin
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerCopyRangeImplementation(CopyRangeMethodSendFile, copyRangeImplementationForBasicFile(copyRangeSendFile))
|
||||
}
|
||||
|
||||
func copyRangeSendFile(src, dst basicFile, srcOffset, dstOffset, size int64) error {
|
||||
// Check that the destination file has sufficient space
|
||||
if fi, err := dst.Stat(); err != nil {
|
||||
return err
|
||||
} else if fi.Size() < dstOffset+size {
|
||||
if err := dst.Truncate(dstOffset + size); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Record old dst offset.
|
||||
oldDstOffset, err := dst.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _, _ = dst.Seek(oldDstOffset, io.SeekStart) }()
|
||||
|
||||
// Seek to the offset we expect to write
|
||||
if oldDstOffset != dstOffset {
|
||||
if n, err := dst.Seek(dstOffset, io.SeekStart); err != nil {
|
||||
return err
|
||||
} else if n != dstOffset {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
}
|
||||
|
||||
for size > 0 {
|
||||
// From the MAN page:
|
||||
//
|
||||
// If offset is not NULL, then it points to a variable holding the file offset from which sendfile() will start
|
||||
// reading data from in_fd. When sendfile() returns, this variable will be set to the offset of the byte
|
||||
// following the last byte that was read. If offset is not NULL, then sendfile() does not modify the current
|
||||
// file offset of in_fd; otherwise the current file offset is adjusted to reflect the number of bytes read from
|
||||
// in_fd.
|
||||
n, err := syscall.Sendfile(int(dst.Fd()), int(src.Fd()), &srcOffset, int(size))
|
||||
if n == 0 && err == nil {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
if err != nil && err != syscall.EAGAIN {
|
||||
return err
|
||||
}
|
||||
// Handle case where err == EAGAIN and n == -1 (it's not clear if that can happen)
|
||||
if n > 0 {
|
||||
size -= int64(n)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dst.Seek(oldDstOffset, io.SeekStart)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2019 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
var (
|
||||
copyRangeMethods = make(map[CopyRangeMethod]copyRangeImplementation)
|
||||
mut = sync.NewMutex()
|
||||
)
|
||||
|
||||
type copyRangeImplementation func(src, dst File, srcOffset, dstOffset, size int64) error
|
||||
|
||||
func registerCopyRangeImplementation(copyMethod CopyRangeMethod, impl copyRangeImplementation) {
|
||||
mut.Lock()
|
||||
defer mut.Unlock()
|
||||
|
||||
l.Debugln("Registering " + copyMethod.String() + " copyRange method")
|
||||
|
||||
copyRangeMethods[copyMethod] = impl
|
||||
}
|
||||
|
||||
// CopyRange tries to use the specified method to copy data between two files.
|
||||
// Takes size bytes at offset srcOffset from the source file, and copies the data to destination file at offset
|
||||
// dstOffset. If required, adjusts the size of the destination file to fit that much data.
|
||||
//
|
||||
// On Linux/BSD you can ask it to use ioctl and copy_file_range system calls, which if the underlying filesystem supports
|
||||
// it tries referencing existing data in the source file, instead of making a copy and taking up additional space.
|
||||
//
|
||||
// CopyRange does its best to have no effect on src and dst file offsets (copy operation should not affect it).
|
||||
func CopyRange(copyMethod CopyRangeMethod, src, dst File, srcOffset, dstOffset, size int64) error {
|
||||
if impl, ok := copyRangeMethods[copyMethod]; ok {
|
||||
return impl(src, dst, srcOffset, dstOffset, size)
|
||||
}
|
||||
|
||||
return syscall.ENOTSUP
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (C) 2019 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
func init() {
|
||||
registerCopyRangeImplementation(CopyRangeMethodAllWithFallback, copyRangeAllWithFallback)
|
||||
}
|
||||
|
||||
func copyRangeAllWithFallback(src, dst File, srcOffset, dstOffset, size int64) error {
|
||||
var err error
|
||||
for _, method := range []CopyRangeMethod{CopyRangeMethodIoctl, CopyRangeMethodCopyFileRange, CopyRangeMethodSendFile, CopyRangeMethodStandard} {
|
||||
if err = CopyRange(method, src, dst, srcOffset, dstOffset, size); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (C) 2020 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
type CopyRangeMethod int
|
||||
|
||||
const (
|
||||
CopyRangeMethodStandard CopyRangeMethod = iota
|
||||
CopyRangeMethodIoctl
|
||||
CopyRangeMethodCopyFileRange
|
||||
CopyRangeMethodSendFile
|
||||
CopyRangeMethodAllWithFallback
|
||||
)
|
||||
|
||||
func (o CopyRangeMethod) String() string {
|
||||
switch o {
|
||||
case CopyRangeMethodStandard:
|
||||
return "standard"
|
||||
case CopyRangeMethodIoctl:
|
||||
return "ioctl"
|
||||
case CopyRangeMethodCopyFileRange:
|
||||
return "copy_file_range"
|
||||
case CopyRangeMethodSendFile:
|
||||
return "sendfile"
|
||||
case CopyRangeMethodAllWithFallback:
|
||||
return "all"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (o CopyRangeMethod) MarshalText() ([]byte, error) {
|
||||
return []byte(o.String()), nil
|
||||
}
|
||||
|
||||
func (o *CopyRangeMethod) UnmarshalText(bs []byte) error {
|
||||
switch string(bs) {
|
||||
case "standard":
|
||||
*o = CopyRangeMethodStandard
|
||||
case "ioctl":
|
||||
*o = CopyRangeMethodIoctl
|
||||
case "copy_file_range":
|
||||
*o = CopyRangeMethodCopyFileRange
|
||||
case "sendfile":
|
||||
*o = CopyRangeMethodSendFile
|
||||
case "all":
|
||||
*o = CopyRangeMethodAllWithFallback
|
||||
default:
|
||||
*o = CopyRangeMethodStandard
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *CopyRangeMethod) ParseDefault(str string) error {
|
||||
return o.UnmarshalText([]byte(str))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2019 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerCopyRangeImplementation(CopyRangeMethodStandard, copyRangeStandard)
|
||||
}
|
||||
|
||||
func copyRangeStandard(src, dst File, srcOffset, dstOffset, size int64) error {
|
||||
const bufSize = 4 << 20
|
||||
|
||||
buf := make([]byte, bufSize)
|
||||
|
||||
// TODO: In go 1.15, we should use file.ReadFrom that uses copy_file_range underneath.
|
||||
|
||||
// ReadAt and WriteAt does not modify the position of the file.
|
||||
for size > 0 {
|
||||
if size < bufSize {
|
||||
buf = buf[:size]
|
||||
}
|
||||
n, err := src.ReadAt(buf, srcOffset)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return err
|
||||
}
|
||||
if _, err = dst.WriteAt(buf[:n], dstOffset); err != nil {
|
||||
return err
|
||||
}
|
||||
srcOffset += int64(n)
|
||||
dstOffset += int64(n)
|
||||
size -= int64(n)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
// Copyright (C) 2019 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
generationSize int64 = 4 << 20
|
||||
defaultCopySize int64 = 1 << 20
|
||||
|
||||
testCases = []struct {
|
||||
name string
|
||||
// Starting size of files
|
||||
srcSize int64
|
||||
dstSize int64
|
||||
// Offset from which to read
|
||||
srcOffset int64
|
||||
dstOffset int64
|
||||
// Cursor position before the copy
|
||||
srcStartingPos int64
|
||||
dstStartingPos int64
|
||||
// Expected destination size
|
||||
expectedDstSizeAfterCopy int64
|
||||
// Custom copy size
|
||||
copySize int64
|
||||
// Expected failure
|
||||
expectedErrors map[CopyRangeMethod]error
|
||||
}{
|
||||
{
|
||||
name: "append to end",
|
||||
srcSize: generationSize,
|
||||
dstSize: generationSize,
|
||||
srcOffset: 0,
|
||||
dstOffset: generationSize,
|
||||
srcStartingPos: generationSize,
|
||||
dstStartingPos: generationSize,
|
||||
expectedDstSizeAfterCopy: generationSize + defaultCopySize,
|
||||
copySize: defaultCopySize,
|
||||
expectedErrors: nil,
|
||||
},
|
||||
{
|
||||
name: "append to end, offsets at start",
|
||||
srcSize: generationSize,
|
||||
dstSize: generationSize,
|
||||
srcOffset: 0,
|
||||
dstOffset: generationSize,
|
||||
srcStartingPos: 0, // We seek back to start, and expect src not to move after copy
|
||||
dstStartingPos: 0, // Seek back, but expect dst pos to not change
|
||||
expectedDstSizeAfterCopy: generationSize + defaultCopySize,
|
||||
copySize: defaultCopySize,
|
||||
expectedErrors: nil,
|
||||
},
|
||||
{
|
||||
name: "overwrite part of destination region",
|
||||
srcSize: generationSize,
|
||||
dstSize: generationSize,
|
||||
srcOffset: defaultCopySize,
|
||||
dstOffset: generationSize,
|
||||
srcStartingPos: generationSize,
|
||||
dstStartingPos: generationSize,
|
||||
expectedDstSizeAfterCopy: generationSize + defaultCopySize,
|
||||
copySize: defaultCopySize,
|
||||
expectedErrors: nil,
|
||||
},
|
||||
{
|
||||
name: "overwrite all of destination",
|
||||
srcSize: generationSize,
|
||||
dstSize: generationSize,
|
||||
srcOffset: 0,
|
||||
dstOffset: 0,
|
||||
srcStartingPos: generationSize,
|
||||
dstStartingPos: generationSize,
|
||||
expectedDstSizeAfterCopy: generationSize,
|
||||
copySize: defaultCopySize,
|
||||
expectedErrors: nil,
|
||||
},
|
||||
{
|
||||
name: "overwrite part of destination",
|
||||
srcSize: generationSize,
|
||||
dstSize: generationSize,
|
||||
srcOffset: defaultCopySize,
|
||||
dstOffset: 0,
|
||||
srcStartingPos: generationSize,
|
||||
dstStartingPos: generationSize,
|
||||
expectedDstSizeAfterCopy: generationSize,
|
||||
copySize: defaultCopySize,
|
||||
expectedErrors: nil,
|
||||
},
|
||||
// Write way past the end of the file
|
||||
{
|
||||
name: "destination gets expanded as it is being written to",
|
||||
srcSize: generationSize,
|
||||
dstSize: generationSize,
|
||||
srcOffset: 0,
|
||||
dstOffset: generationSize * 2,
|
||||
srcStartingPos: generationSize,
|
||||
dstStartingPos: generationSize,
|
||||
expectedDstSizeAfterCopy: generationSize*2 + defaultCopySize,
|
||||
copySize: defaultCopySize,
|
||||
expectedErrors: nil,
|
||||
},
|
||||
// Source file does not have enough bytes to copy in that range, should result in an unexpected eof.
|
||||
{
|
||||
name: "source file too small",
|
||||
srcSize: generationSize,
|
||||
dstSize: generationSize,
|
||||
srcOffset: 0,
|
||||
dstOffset: 0,
|
||||
srcStartingPos: 0,
|
||||
dstStartingPos: 0,
|
||||
expectedDstSizeAfterCopy: -11, // Does not matter, should fail.
|
||||
copySize: defaultCopySize * 10,
|
||||
// ioctl returns syscall.EINVAL, rest are wrapped
|
||||
expectedErrors: map[CopyRangeMethod]error{
|
||||
CopyRangeMethodIoctl: syscall.EINVAL,
|
||||
CopyRangeMethodStandard: io.ErrUnexpectedEOF,
|
||||
CopyRangeMethodCopyFileRange: io.ErrUnexpectedEOF,
|
||||
CopyRangeMethodSendFile: io.ErrUnexpectedEOF,
|
||||
CopyRangeMethodAllWithFallback: io.ErrUnexpectedEOF,
|
||||
},
|
||||
},
|
||||
// Non block sized file
|
||||
{
|
||||
name: "not block aligned write",
|
||||
srcSize: generationSize + 2,
|
||||
dstSize: 0,
|
||||
srcOffset: 1,
|
||||
dstOffset: 0,
|
||||
srcStartingPos: 0,
|
||||
dstStartingPos: 0,
|
||||
expectedDstSizeAfterCopy: generationSize + 1,
|
||||
copySize: generationSize + 1,
|
||||
// Only fails for ioctl
|
||||
expectedErrors: map[CopyRangeMethod]error{
|
||||
CopyRangeMethodIoctl: syscall.EINVAL,
|
||||
},
|
||||
},
|
||||
// Last block that starts on a nice boundary
|
||||
{
|
||||
name: "last block",
|
||||
srcSize: generationSize + 2,
|
||||
dstSize: 0,
|
||||
srcOffset: generationSize,
|
||||
dstOffset: 0,
|
||||
srcStartingPos: 0,
|
||||
dstStartingPos: 0,
|
||||
expectedDstSizeAfterCopy: 2,
|
||||
copySize: 2,
|
||||
// Succeeds on all, as long as the offset is file-system block aligned.
|
||||
expectedErrors: nil,
|
||||
},
|
||||
// Copy whole file
|
||||
{
|
||||
name: "whole file copy block aligned",
|
||||
srcSize: generationSize,
|
||||
dstSize: 0,
|
||||
srcOffset: 0,
|
||||
dstOffset: 0,
|
||||
srcStartingPos: 0,
|
||||
dstStartingPos: 0,
|
||||
expectedDstSizeAfterCopy: generationSize,
|
||||
copySize: generationSize,
|
||||
expectedErrors: nil,
|
||||
},
|
||||
{
|
||||
name: "whole file copy not block aligned",
|
||||
srcSize: generationSize + 1,
|
||||
dstSize: 0,
|
||||
srcOffset: 0,
|
||||
dstOffset: 0,
|
||||
srcStartingPos: 0,
|
||||
dstStartingPos: 0,
|
||||
expectedDstSizeAfterCopy: generationSize + 1,
|
||||
copySize: generationSize + 1,
|
||||
expectedErrors: nil,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestCopyRange(ttt *testing.T) {
|
||||
randSrc := rand.New(rand.NewSource(rand.Int63()))
|
||||
for copyMethod, impl := range copyRangeMethods {
|
||||
ttt.Run(copyMethod.String(), func(tt *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
tt.Run(testCase.name, func(t *testing.T) {
|
||||
srcBuf := make([]byte, testCase.srcSize)
|
||||
dstBuf := make([]byte, testCase.dstSize)
|
||||
td, err := ioutil.TempDir(os.Getenv("STFSTESTPATH"), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(td) }()
|
||||
fs := NewFilesystem(FilesystemTypeBasic, td)
|
||||
|
||||
if _, err := io.ReadFull(randSrc, srcBuf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(randSrc, dstBuf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
src, err := fs.Create("src")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = src.Close() }()
|
||||
|
||||
dst, err := fs.Create("dst")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = dst.Close() }()
|
||||
|
||||
// Write some data
|
||||
|
||||
if _, err := src.Write(srcBuf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := dst.Write(dstBuf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set the offsets
|
||||
|
||||
if n, err := src.Seek(testCase.srcStartingPos, io.SeekStart); err != nil || n != testCase.srcStartingPos {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if n, err := dst.Seek(testCase.dstStartingPos, io.SeekStart); err != nil || n != testCase.dstStartingPos {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := impl(src.(basicFile), dst.(basicFile), testCase.srcOffset, testCase.dstOffset, testCase.copySize); err != nil {
|
||||
if err == syscall.ENOTSUP {
|
||||
// Test runner can adjust directory in which to run the tests, that allow broader tests.
|
||||
t.Skip("Not supported on the current filesystem, set STFSTESTPATH env var.")
|
||||
}
|
||||
if testCase.expectedErrors[copyMethod] == err {
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
} else if testCase.expectedErrors[copyMethod] != nil {
|
||||
t.Fatal("did not get expected error")
|
||||
}
|
||||
|
||||
// Check offsets where we expect them
|
||||
|
||||
if srcCurPos, err := src.Seek(0, io.SeekCurrent); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if srcCurPos != testCase.srcStartingPos {
|
||||
t.Errorf("src pos expected %d got %d", testCase.srcStartingPos, srcCurPos)
|
||||
}
|
||||
|
||||
if dstCurPos, err := dst.Seek(0, io.SeekCurrent); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if dstCurPos != testCase.dstStartingPos {
|
||||
t.Errorf("dst pos expected %d got %d", testCase.dstStartingPos, dstCurPos)
|
||||
}
|
||||
|
||||
// Check dst size
|
||||
|
||||
if fi, err := dst.Stat(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if fi.Size() != testCase.expectedDstSizeAfterCopy {
|
||||
t.Errorf("expected %d size, got %d", testCase.expectedDstSizeAfterCopy, fi.Size())
|
||||
}
|
||||
|
||||
// Check the data is as expected
|
||||
|
||||
if _, err := dst.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resultBuf := make([]byte, testCase.expectedDstSizeAfterCopy)
|
||||
if _, err := io.ReadFull(dst, resultBuf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(srcBuf[testCase.srcOffset:testCase.srcOffset+testCase.copySize], resultBuf[testCase.dstOffset:testCase.dstOffset+testCase.copySize]) {
|
||||
t.Errorf("Not equal")
|
||||
}
|
||||
|
||||
// Check not copied content does not get corrupted
|
||||
|
||||
if testCase.dstOffset > testCase.dstSize {
|
||||
if !bytes.Equal(dstBuf[:testCase.dstSize], resultBuf[:testCase.dstSize]) {
|
||||
t.Error("region before copy region not equals")
|
||||
}
|
||||
if !bytes.Equal(resultBuf[testCase.dstSize:testCase.dstOffset], make([]byte, testCase.dstOffset-testCase.dstSize)) {
|
||||
t.Error("found non zeroes in expected zero region")
|
||||
}
|
||||
} else {
|
||||
if !bytes.Equal(dstBuf[:testCase.dstOffset], resultBuf[:testCase.dstOffset]) {
|
||||
t.Error("region before copy region not equals")
|
||||
}
|
||||
afterCopyStart := testCase.dstOffset + testCase.copySize
|
||||
|
||||
if afterCopyStart < testCase.dstSize {
|
||||
if !bytes.Equal(dstBuf[afterCopyStart:], resultBuf[afterCopyStart:len(dstBuf)]) {
|
||||
t.Error("region after copy region not equals")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user