// 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/. //go:build windows // +build windows package fs import ( "io" "syscall" "unsafe" "golang.org/x/sys/windows" ) func init() { registerCopyRangeImplementation(CopyRangeMethodDuplicateExtents, copyRangeImplementationForBasicFile(copyRangeDuplicateExtents)) } // Inspired by https://github.com/git-lfs/git-lfs/blob/master/tools/util_windows.go var ( availableClusterSize = []int64{64 * 1024, 4 * 1024} // ReFS only supports 64KiB and 4KiB cluster. GiB = int64(1024 * 1024 * 1024) ) // fsctlDuplicateExtentsToFile = FSCTL_DUPLICATE_EXTENTS_TO_FILE IOCTL // Instructs the file system to copy a range of file bytes on behalf of an application. // // https://docs.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file const fsctlDuplicateExtentsToFile = 623428 // duplicateExtentsData = DUPLICATE_EXTENTS_DATA structure // Contains parameters for the FSCTL_DUPLICATE_EXTENTS control code that performs the Block Cloning operation. // // https://docs.microsoft.com/windows/win32/api/winioctl/ns-winioctl-duplicate_extents_data type duplicateExtentsData struct { FileHandle windows.Handle SourceFileOffset int64 TargetFileOffset int64 ByteCount int64 } func copyRangeDuplicateExtents(src, dst basicFile, srcOffset, dstOffset, size int64) error { var err error // Check that the destination file has sufficient space dstFi, err := dst.Stat() if err != nil { return err } dstSize := dstFi.Size() if dstSize < dstOffset+size { // set file size. There is a requirements "The destination region must not extend past the end of file." if err = dst.Truncate(dstOffset + size); err != nil { return err } dstSize = dstOffset + size } // The source file has to be big enough if fi, err := src.Stat(); err != nil { return err } else if fi.Size() < srcOffset+size { return io.ErrUnexpectedEOF } // Requirement // * The source and destination regions must begin and end at a cluster boundary. (4KiB or 64KiB) // * cloneRegionSize less than 4GiB. // see https://docs.microsoft.com/windows/win32/fileio/block-cloning smallestClusterSize := availableClusterSize[len(availableClusterSize)-1] if srcOffset%smallestClusterSize != 0 || dstOffset%smallestClusterSize != 0 { return syscall.EINVAL } // Each file gets allocated multiple of "clusterSize" blocks, yet file size determines how much of the last block // is readable/visible. // Copies only happen in block sized chunks, hence you can copy non block sized regions of data to a file, as long // as the regions are copied at the end of the file where the block visibility is adjusted by the file size. if size%smallestClusterSize != 0 && dstOffset+size != dstSize { return syscall.EINVAL } // Clone first xGiB region. for size > GiB { _, err = withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) { return 0, callDuplicateExtentsToFile(srcFd, dstFd, srcOffset, dstOffset, GiB) }) if err != nil { return wrapError(err) } size -= GiB srcOffset += GiB dstOffset += GiB } // Clone tail. First try with 64KiB round up, then fallback to 4KiB. for _, cloneRegionSize := range availableClusterSize { _, err = withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) { return 0, callDuplicateExtentsToFile(srcFd, dstFd, srcOffset, dstOffset, roundUp(size, cloneRegionSize)) }) if err != nil { continue } break } return wrapError(err) } func wrapError(err error) error { if err == windows.SEVERITY_ERROR { return syscall.ENOTSUP } return err } // call FSCTL_DUPLICATE_EXTENTS_TO_FILE IOCTL // see https://docs.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file // // memo: Overflow (cloneRegionSize is greater than file ends) is safe and just ignored by windows. func callDuplicateExtentsToFile(src, dst uintptr, srcOffset, dstOffset int64, cloneRegionSize int64) error { var ( bytesReturned uint32 overlapped windows.Overlapped ) request := duplicateExtentsData{ FileHandle: windows.Handle(src), SourceFileOffset: srcOffset, TargetFileOffset: dstOffset, ByteCount: cloneRegionSize, } return windows.DeviceIoControl( windows.Handle(dst), fsctlDuplicateExtentsToFile, (*byte)(unsafe.Pointer(&request)), uint32(unsafe.Sizeof(request)), (*byte)(unsafe.Pointer(nil)), // = nullptr 0, &bytesReturned, &overlapped) } func roundUp(value, base int64) int64 { mod := value % base if mod == 0 { return value } return value - mod + base }