unix: fix and test the FIDEDUPERANGE Linux ioctl

The previous implementation didn't match the ioctl spec.

Fixes golang/go#43678

Change-Id: Ia1c292c2dbd4913fb1d7e8331d9f18e23169d64a
GitHub-Last-Rev: 5331c424ef5dd0384afda9effbb76afd7705ecfd
GitHub-Pull-Request: golang/sys#97
Reviewed-on: https://go-review.googlesource.com/c/sys/+/284352
Run-TryBot: Tobias Klauser <tobias.klauser@gmail.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Tobias Klauser <tobias.klauser@gmail.com>
Trust: Tobias Klauser <tobias.klauser@gmail.com>
Trust: Ian Lance Taylor <iant@golang.org>
diff --git a/unix/linux/types.go b/unix/linux/types.go
index ab2be60..ef138cf 100644
--- a/unix/linux/types.go
+++ b/unix/linux/types.go
@@ -474,7 +474,16 @@
 
 type FileCloneRange C.struct_file_clone_range
 
-type FileDedupeRange C.struct_file_dedupe_range
+type RawFileDedupeRange C.struct_file_dedupe_range
+
+type RawFileDedupeRangeInfo C.struct_file_dedupe_range_info
+
+const (
+	SizeofRawFileDedupeRange     = C.sizeof_struct_file_dedupe_range
+	SizeofRawFileDedupeRangeInfo = C.sizeof_struct_file_dedupe_range_info
+	FILE_DEDUPE_RANGE_SAME       = C.FILE_DEDUPE_RANGE_SAME
+	FILE_DEDUPE_RANGE_DIFFERS    = C.FILE_DEDUPE_RANGE_DIFFERS
+)
 
 // Filesystem Encryption
 
diff --git a/unix/syscall_linux.go b/unix/syscall_linux.go
index ad49b27..0a48548 100644
--- a/unix/syscall_linux.go
+++ b/unix/syscall_linux.go
@@ -137,12 +137,61 @@
 	return ioctl(destFd, FICLONE, uintptr(srcFd))
 }
 
+type FileDedupeRange struct {
+	Src_offset uint64
+	Src_length uint64
+	Reserved1  uint16
+	Reserved2  uint32
+	Info       []FileDedupeRangeInfo
+}
+
+type FileDedupeRangeInfo struct {
+	Dest_fd       int64
+	Dest_offset   uint64
+	Bytes_deduped uint64
+	Status        int32
+	Reserved      uint32
+}
+
 // IoctlFileDedupeRange performs an FIDEDUPERANGE ioctl operation to share the
-// range of data conveyed in value with the file associated with the file
-// descriptor destFd. See the ioctl_fideduperange(2) man page for details.
-func IoctlFileDedupeRange(destFd int, value *FileDedupeRange) error {
-	err := ioctl(destFd, FIDEDUPERANGE, uintptr(unsafe.Pointer(value)))
-	runtime.KeepAlive(value)
+// range of data conveyed in value from the file associated with the file
+// descriptor srcFd to the value.Info destinations. See the
+// ioctl_fideduperange(2) man page for details.
+func IoctlFileDedupeRange(srcFd int, value *FileDedupeRange) error {
+	buf := make([]byte, SizeofRawFileDedupeRange+
+		len(value.Info)*SizeofRawFileDedupeRangeInfo)
+	rawrange := (*RawFileDedupeRange)(unsafe.Pointer(&buf[0]))
+	rawrange.Src_offset = value.Src_offset
+	rawrange.Src_length = value.Src_length
+	rawrange.Dest_count = uint16(len(value.Info))
+	rawrange.Reserved1 = value.Reserved1
+	rawrange.Reserved2 = value.Reserved2
+
+	for i := range value.Info {
+		rawinfo := (*RawFileDedupeRangeInfo)(unsafe.Pointer(
+			uintptr(unsafe.Pointer(&buf[0])) + uintptr(SizeofRawFileDedupeRange) +
+				uintptr(i*SizeofRawFileDedupeRangeInfo)))
+		rawinfo.Dest_fd = value.Info[i].Dest_fd
+		rawinfo.Dest_offset = value.Info[i].Dest_offset
+		rawinfo.Bytes_deduped = value.Info[i].Bytes_deduped
+		rawinfo.Status = value.Info[i].Status
+		rawinfo.Reserved = value.Info[i].Reserved
+	}
+
+	err := ioctl(srcFd, FIDEDUPERANGE, uintptr(unsafe.Pointer(&buf[0])))
+
+	// Output
+	for i := range value.Info {
+		rawinfo := (*RawFileDedupeRangeInfo)(unsafe.Pointer(
+			uintptr(unsafe.Pointer(&buf[0])) + uintptr(SizeofRawFileDedupeRange) +
+				uintptr(i*SizeofRawFileDedupeRangeInfo)))
+		value.Info[i].Dest_fd = rawinfo.Dest_fd
+		value.Info[i].Dest_offset = rawinfo.Dest_offset
+		value.Info[i].Bytes_deduped = rawinfo.Bytes_deduped
+		value.Info[i].Status = rawinfo.Status
+		value.Info[i].Reserved = rawinfo.Reserved
+	}
+
 	return err
 }
 
diff --git a/unix/syscall_linux_test.go b/unix/syscall_linux_test.go
index db1a960..699e56f 100644
--- a/unix/syscall_linux_test.go
+++ b/unix/syscall_linux_test.go
@@ -796,3 +796,94 @@
 		t.Errorf("Openat2 should fail with EXDEV, got %v", err)
 	}
 }
+
+func TestFideduperange(t *testing.T) {
+	if runtime.GOOS == "android" {
+		// The ioctl in the build robot android-amd64 returned ENOTTY,
+		// an error not documented for that syscall.
+		t.Skip("FIDEDUPERANGE ioctl doesn't work on android, skipping test")
+	}
+
+	f1, err := ioutil.TempFile("", t.Name())
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f1.Close()
+	defer os.Remove(f1.Name())
+
+	// Test deduplication with two blocks of zeros
+	data := make([]byte, 4096)
+
+	for i := 0; i < 2; i += 1 {
+		_, err = f1.Write(data)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	f2, err := ioutil.TempFile("", t.Name())
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f2.Close()
+	defer os.Remove(f2.Name())
+
+	for i := 0; i < 2; i += 1 {
+		// Make the 2nd block different
+		if i == 1 {
+			data[1] = 1
+		}
+
+		_, err = f2.Write(data)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	dedupe := unix.FileDedupeRange{
+		Src_offset: uint64(0),
+		Src_length: uint64(4096),
+		Info: []unix.FileDedupeRangeInfo{
+			unix.FileDedupeRangeInfo{
+				Dest_fd:     int64(f2.Fd()),
+				Dest_offset: uint64(0),
+			},
+			unix.FileDedupeRangeInfo{
+				Dest_fd:     int64(f2.Fd()),
+				Dest_offset: uint64(4096),
+			},
+		}}
+
+	err = unix.IoctlFileDedupeRange(int(f1.Fd()), &dedupe)
+	if err == unix.EOPNOTSUPP || err == unix.EINVAL {
+		t.Skip("deduplication not supported on this filesystem")
+	} else if err != nil {
+		t.Fatal(err)
+	}
+
+	// The first Info should be equal
+	if dedupe.Info[0].Status < 0 {
+		// We expect status to be a negated errno
+		t.Errorf("Unexpected error in FileDedupeRange: %s",
+			unix.ErrnoName(unix.Errno(-dedupe.Info[0].Status)))
+	} else if dedupe.Info[0].Status == unix.FILE_DEDUPE_RANGE_DIFFERS {
+		t.Errorf("Unexpected different bytes in FileDedupeRange")
+	}
+	if dedupe.Info[0].Bytes_deduped != 4096 {
+		t.Errorf("Unexpected amount of bytes deduped %v != %v",
+			dedupe.Info[0].Bytes_deduped, 4096)
+	}
+
+	// The second Info should be different
+	if dedupe.Info[1].Status < 0 {
+		// We expect status to be a negated errno
+		t.Errorf("Unexpected error in FileDedupeRange: %s",
+			unix.ErrnoName(unix.Errno(-dedupe.Info[1].Status)))
+	} else if dedupe.Info[1].Status == unix.FILE_DEDUPE_RANGE_SAME {
+		t.Errorf("Unexpected equal bytes in FileDedupeRange")
+	}
+	if dedupe.Info[1].Bytes_deduped != 0 {
+		t.Errorf("Unexpected amount of bytes deduped %v != %v",
+			dedupe.Info[1].Bytes_deduped, 0)
+	}
+}
diff --git a/unix/ztypes_linux.go b/unix/ztypes_linux.go
index 3f922db..d3a1871 100644
--- a/unix/ztypes_linux.go
+++ b/unix/ztypes_linux.go
@@ -84,7 +84,7 @@
 	Dest_offset uint64
 }
 
-type FileDedupeRange struct {
+type RawFileDedupeRange struct {
 	Src_offset uint64
 	Src_length uint64
 	Dest_count uint16
@@ -92,6 +92,21 @@
 	Reserved2  uint32
 }
 
+type RawFileDedupeRangeInfo struct {
+	Dest_fd       int64
+	Dest_offset   uint64
+	Bytes_deduped uint64
+	Status        int32
+	Reserved      uint32
+}
+
+const (
+	SizeofRawFileDedupeRange     = 0x18
+	SizeofRawFileDedupeRangeInfo = 0x20
+	FILE_DEDUPE_RANGE_SAME       = 0x0
+	FILE_DEDUPE_RANGE_DIFFERS    = 0x1
+)
+
 type FscryptPolicy struct {
 	Version                   uint8
 	Contents_encryption_mode  uint8