113 lines
3.4 KiB
Go
113 lines
3.4 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
//go:build linux
|
||
|
|
|
||
|
|
package nfsfs
|
||
|
|
|
||
|
|
import (
|
||
|
|
"runtime"
|
||
|
|
"syscall"
|
||
|
|
"unsafe"
|
||
|
|
)
|
||
|
|
|
||
|
|
// The reflink ioctl of linux/fs.h, FICLONERANGE, and the syscall number
|
||
|
|
// of copy_file_range(2), which the standard library does not export on
|
||
|
|
// linux. FICLONERANGE is _IOW(0x94, 13, struct file_clone_range) with a
|
||
|
|
// 32 byte struct: (1<<30)|(32<<16)|(0x94<<8)|13. The copy_file_range
|
||
|
|
// numbers follow the kernel's syscall tables: 319 on amd64 and 286 on
|
||
|
|
// the asm generic table of arm64, riscv64 and loong64. An architecture
|
||
|
|
// outside the table answers unshareable, and the caller keeps its
|
||
|
|
// userspace path.
|
||
|
|
const ioctlFICLONERANGE = 0x4020940D
|
||
|
|
|
||
|
|
// fileCloneRange mirrors struct file_clone_range of linux/fs.h, the
|
||
|
|
// argument of FICLONERANGE.
|
||
|
|
type fileCloneRange struct {
|
||
|
|
srcFD int64
|
||
|
|
srcOffset uint64
|
||
|
|
srcLength uint64
|
||
|
|
destOffset uint64
|
||
|
|
}
|
||
|
|
|
||
|
|
// CloneRange makes the destination carry the source's bytes through the
|
||
|
|
// reflink of the filesystem, XFS, btrfs and ZFS among them. The
|
||
|
|
// descriptor cache hands out both ends, so a range cloned through cached
|
||
|
|
// descriptors stays as verified as any other read or write.
|
||
|
|
func (l *Local) CloneRange(src Handle, srcOff int64, dst Handle, dstOff int64, length int64) error {
|
||
|
|
sf, releaseSrc, err := l.dataFD(src, false)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer releaseSrc()
|
||
|
|
df, releaseDst, err := l.dataFD(dst, true)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer releaseDst()
|
||
|
|
cr := fileCloneRange{
|
||
|
|
srcFD: int64(sf.Fd()),
|
||
|
|
srcOffset: uint64(srcOff),
|
||
|
|
srcLength: uint64(length),
|
||
|
|
destOffset: uint64(dstOff),
|
||
|
|
}
|
||
|
|
// SAFETY: ioctl takes the address of exactly the 32 byte struct the
|
||
|
|
// FICLONERANGE command name carries; the kernel reads it and writes
|
||
|
|
// nothing through it.
|
||
|
|
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, df.Fd(), ioctlFICLONERANGE,
|
||
|
|
uintptr(unsafe.Pointer(&cr))); errno != 0 {
|
||
|
|
return errno
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// CopyRange copies the bytes through copy_file_range(2), in chunks until
|
||
|
|
// the length is served. The syscall answers how much moved; a short move
|
||
|
|
// on the first call means the kernel refused for these files and the
|
||
|
|
// error travels to the caller's fallback.
|
||
|
|
func (l *Local) CopyRange(src Handle, srcOff int64, dst Handle, dstOff int64, length int64) error {
|
||
|
|
sf, releaseSrc, err := l.dataFD(src, false)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer releaseSrc()
|
||
|
|
df, releaseDst, err := l.dataFD(dst, true)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer releaseDst()
|
||
|
|
number := copyFileRangeSyscall()
|
||
|
|
if number == 0 {
|
||
|
|
return syscall.ENOSYS
|
||
|
|
}
|
||
|
|
var inOff, outOff int64 = srcOff, dstOff
|
||
|
|
for length > 0 {
|
||
|
|
chunk := min(length, 8<<20)
|
||
|
|
// SAFETY: the syscall copies from and to the file offsets behind
|
||
|
|
// the two pointers and updates them; both live across the call.
|
||
|
|
n, _, errno := syscall.Syscall6(number, sf.Fd(), uintptr(unsafe.Pointer(&inOff)),
|
||
|
|
df.Fd(), uintptr(unsafe.Pointer(&outOff)), uintptr(chunk), 0)
|
||
|
|
if errno != 0 {
|
||
|
|
return errno
|
||
|
|
}
|
||
|
|
if n == 0 {
|
||
|
|
return syscall.EINVAL
|
||
|
|
}
|
||
|
|
length -= int64(n)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// copyFileRangeSyscall answers the syscall number of copy_file_range on
|
||
|
|
// the architectures this project builds for linux, zero elsewhere.
|
||
|
|
func copyFileRangeSyscall() uintptr {
|
||
|
|
switch runtime.GOARCH {
|
||
|
|
case "amd64":
|
||
|
|
return 319
|
||
|
|
case "arm64", "riscv64", "loong64":
|
||
|
|
return 286
|
||
|
|
default:
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
}
|