copy_stubs.c (12912B)
1 /* Unison file synchronizer: src/copy_stubs.c */ 2 /* Copyright 2021-2025, Tõivo Leedjärv 3 4 This program is free software: you can redistribute it and/or modify 5 it under the terms of the GNU General Public License as published by 6 the Free Software Foundation, either version 3 of the License, or 7 (at your option) any later version. 8 9 This program is distributed in the hope that it will be useful, 10 but WITHOUT ANY WARRANTY; without even the implied warranty of 11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 GNU General Public License for more details. 13 14 You should have received a copy of the GNU General Public License 15 along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #include <caml/memory.h> 19 #include <caml/threads.h> 20 #include <caml/unixsupport.h> 21 #include <caml/version.h> 22 23 #if OCAML_VERSION_MAJOR < 5 24 #define caml_unix_error unix_error 25 #define caml_uerror uerror 26 #define caml_win32_maperr win32_maperr 27 #endif 28 29 30 #include <errno.h> 31 32 33 /* ----------------------------------------------- */ 34 /* Clone a file given source and destination paths */ 35 /* It must fully complete or fully fail. 36 37 The function must not raise any exceptions. 38 39 Return true for success and false for failure 40 or if the operation is not supported. */ 41 42 #if defined(__APPLE__) 43 44 45 #include <AvailabilityMacros.h> 46 47 #if defined(MAC_OS_X_VERSION_10_12) 48 #include <string.h> 49 #include <sys/attr.h> 50 #include <sys/clonefile.h> 51 52 CAMLprim value unison_clone_path(value src, value dst) 53 { 54 CAMLparam2(src, dst); 55 char *srcn, *dstn; 56 int status; 57 58 srcn = strdup(String_val(src)); 59 dstn = strdup(String_val(dst)); 60 caml_release_runtime_system(); 61 status = clonefile(srcn, dstn, CLONE_NOFOLLOW | CLONE_NOOWNERCOPY); 62 free(srcn); 63 free(dstn); 64 caml_acquire_runtime_system(); 65 66 /* Don't raise an exception, just return false in case of errors */ 67 CAMLreturn(Val_bool(status == 0)); 68 } 69 #else /* MAC_OS_X_VERSION_10_12 */ 70 CAMLprim value unison_clone_path(value src, value dst) 71 { 72 CAMLparam2(src, dst); 73 CAMLreturn(Val_false); 74 } 75 #endif /* MAC_OS_X_VERSION_10_12 */ 76 77 78 #else /* defined(__APPLE__) */ 79 80 81 CAMLprim value unison_clone_path(value src, value dst) 82 { 83 CAMLparam2(src, dst); 84 CAMLreturn(Val_false); 85 } 86 87 /* Regarding Windows API: The CopyFile function in Windows API can do file 88 * clones under right conditions. Nevertheless, we can't use that function 89 * since it is not possible to explicitly request cloning (it is possible to 90 * check whether block cloning is supported (see [unison_copy_file]) but it is 91 * not a guarantee that the CopyFile function will actually result in a 92 * cloning operation) and the function will fall back to a normal copy, which 93 * we do not want in this case (even though performance-wise it would be more 94 * like sendfile). */ 95 96 #endif /* defined(__APPLE__) */ 97 98 99 /* ----------------------------------------------- */ 100 /* Clone a file given input and output fd */ 101 /* It must fully complete or fully fail. 102 103 The function must not raise any exceptions. 104 105 Return true for success and false for failure 106 or if the operation is not supported. */ 107 108 #if defined(__linux__) || defined(__linux) 109 110 111 #include <sys/ioctl.h> 112 113 #if !defined(FICLONE) && defined(_IOW) 114 #define FICLONE _IOW(0x94, 9, int) 115 #endif 116 117 CAMLprim value unison_clone_file(value in_fd, value out_fd) 118 { 119 CAMLparam2(in_fd, out_fd); 120 121 #ifdef FICLONE 122 caml_release_runtime_system(); 123 int status = ioctl(Int_val(out_fd), FICLONE, Int_val(in_fd)); 124 caml_acquire_runtime_system(); 125 126 /* Don't raise an exception, just return false in case of errors */ 127 CAMLreturn(Val_bool(status == 0)); 128 #else /* defined(FICLONE) */ 129 CAMLreturn(Val_false); 130 #endif 131 } 132 133 134 #else /* defined(__linux__) */ 135 136 137 CAMLprim value unison_clone_file(value in_fd, value out_fd) 138 { 139 CAMLparam2(in_fd, out_fd); 140 CAMLreturn(Val_false); 141 } 142 143 144 #endif /* defined(__linux__) */ 145 146 147 /* --------------------------------------------------------- */ 148 /* Copy, or possibly clone, a file given input and output fd */ 149 /* If operation is not supported by the OS or the filesystem 150 then file offsets must not have been changed at failure. 151 Output file offset must be changed on success. 152 153 The function must return the number of bytes copied. 154 155 On any error, raise a Unix exception based on errno. 156 Raise ENOSYS if the operation is not supported. */ 157 158 #if defined(__linux__) || defined(__linux) 159 160 161 #include <unistd.h> 162 #include <sys/syscall.h> 163 #include <sys/sendfile.h> 164 165 CAMLprim value unison_copy_file(value in_fd, value out_fd, value in_offs, value len) 166 { 167 CAMLparam4(in_fd, out_fd, in_offs, len); 168 off_t off_i = Int64_val(in_offs); 169 ssize_t ret; 170 171 caml_release_runtime_system(); 172 #ifdef __NR_copy_file_range 173 /* First, try copy_file_range() */ 174 /* Using off_i prevents changing in_fd file offset */ 175 ret = syscall(__NR_copy_file_range, Int_val(in_fd), &off_i, Int_val(out_fd), NULL, Long_val(len), 0); 176 if (ret == -1 && (errno == ENOSYS || errno == EBADF || errno == EXDEV)) 177 #endif /* defined(__NR_copy_file_range) */ 178 { 179 /* Second, try sendfile(); this one changes out_fd file offset */ 180 ret = sendfile(Int_val(out_fd), Int_val(in_fd), &off_i, Long_val(len)); 181 } 182 caml_acquire_runtime_system(); 183 if (ret == -1) caml_uerror("copy_file", Nothing); 184 185 CAMLreturn(Val_long(ret)); 186 } 187 188 189 #elif defined(__FreeBSD__) 190 191 #include <sys/param.h> 192 #include <sys/types.h> 193 #include <unistd.h> 194 195 CAMLprim value unison_copy_file(value in_fd, value out_fd, value in_offs, value len) 196 { 197 CAMLparam4(in_fd, out_fd, in_offs, len); 198 #if __FreeBSD_version >= 1300037 199 off_t off_i = Int64_val(in_offs); 200 ssize_t ret; 201 202 caml_release_runtime_system(); 203 /* Using off_i prevents changing in_fd file offset */ 204 ret = copy_file_range(Int_val(in_fd), &off_i, Int_val(out_fd), NULL, Long_val(len), 0); 205 caml_acquire_runtime_system(); 206 if (ret == -1) caml_uerror("copy_file", Nothing); 207 208 CAMLreturn(Val_long(ret)); 209 #else /* __FreeBSD_version >= 1300037 */ 210 caml_unix_error(ENOSYS, "copy_file", Nothing); 211 CAMLreturn(Val_long(0)); 212 #endif /* __FreeBSD_version >= 1300037 */ 213 } 214 215 216 #elif defined(__sun) || defined(sun) 217 218 219 #include <sys/sendfile.h> 220 221 CAMLprim value unison_copy_file(value in_fd, value out_fd, value in_offs, value len) 222 { 223 CAMLparam4(in_fd, out_fd, in_offs, len); 224 off_t off_orig; 225 off_t off = off_orig = Int64_val(in_offs); 226 ssize_t ret; 227 228 caml_release_runtime_system(); 229 /* This one changes out_fd file offset */ 230 ret = sendfile(Int_val(out_fd), Int_val(in_fd), &off, Long_val(len)); 231 caml_acquire_runtime_system(); 232 if (ret == -1) { 233 if (off > off_orig) { 234 ret = off - off_orig; 235 } else { 236 caml_uerror("copy_file", Nothing); 237 } 238 } 239 240 CAMLreturn(Val_long(ret)); 241 } 242 243 244 #elif defined(_WIN32) 245 246 247 #ifndef UNICODE 248 #define UNICODE 249 #endif 250 #ifndef _UNICODE 251 #define _UNICODE 252 #endif 253 254 #include <windows.h> 255 #include <winioctl.h> 256 257 #if defined(__MINGW32__) 258 259 /* FIXME: As of January 2025, mingw has does not include type definitions for 260 * FSCTL_GET_INTEGRITY_INFORMATION_BUFFER and DUPLICATE_EXTENTS_DATA (but does 261 * include the definitions of FSCTL constants). If you get a compilation error 262 * about "type redefinition" or similar, just remove the typedefs below. */ 263 264 #ifndef FILE_SUPPORTS_BLOCK_REFCOUNTING 265 #define FILE_SUPPORTS_BLOCK_REFCOUNTING 0x08000000 266 #endif 267 268 #ifndef FSCTL_GET_INTEGRITY_INFORMATION 269 #define FSCTL_GET_INTEGRITY_INFORMATION 0x9027c 270 #endif 271 272 typedef struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { 273 WORD ChecksumAlgorithm; 274 WORD Reserved; 275 DWORD Flags; 276 DWORD ChecksumChunkSizeInBytes; 277 DWORD ClusterSizeInBytes; 278 } FSCTL_GET_INTEGRITY_INFORMATION_BUFFER; 279 280 #ifndef FSCTL_DUPLICATE_EXTENTS_TO_FILE 281 #define FSCTL_DUPLICATE_EXTENTS_TO_FILE 0x98344 282 #endif 283 284 #if !defined(__MINGW64_VERSION_MAJOR) || __MINGW64_VERSION_MAJOR < 13 285 286 /* This typedef is included in mingw since version 13.0.0 */ 287 typedef struct _DUPLICATE_EXTENTS_DATA { 288 HANDLE FileHandle; 289 LARGE_INTEGER SourceFileOffset; 290 LARGE_INTEGER TargetFileOffset; 291 LARGE_INTEGER ByteCount; 292 } DUPLICATE_EXTENTS_DATA; 293 294 #endif /* __MINGW64_VERSION_MAJOR < 13 */ 295 296 #endif /* defined(__MINGW32__) */ 297 298 void unsn_copy_file_error(void) 299 { 300 caml_win32_maperr(GetLastError()); 301 caml_uerror("copy_file", Nothing); 302 } 303 304 BOOL unsn_copy_file_supports_cloning(HANDLE h) 305 { 306 DWORD flags = 0; 307 return 308 (GetVolumeInformationByHandleW(h, NULL, 0, NULL, NULL, &flags, NULL, 0) 309 && (flags & FILE_SUPPORTS_BLOCK_REFCOUNTING)); 310 } 311 312 void unsn_copy_file_set_sparse(HANDLE h, BOOL sparse) 313 { 314 FILE_SET_SPARSE_BUFFER sp; 315 sp.SetSparse = sparse; 316 if (!DeviceIoControl(h, FSCTL_SET_SPARSE, &sp, sizeof(sp), 317 NULL, 0, NULL, NULL)) { 318 unsn_copy_file_error(); 319 } 320 } 321 322 BOOL unsn_copy_file_get_sparse(HANDLE h) 323 { 324 FILE_BASIC_INFO info; 325 if (!GetFileInformationByHandleEx(h, FileBasicInfo, &info, sizeof(info))) { 326 unsn_copy_file_error(); 327 } 328 return (info.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) != 0; 329 } 330 331 CAMLprim value unison_copy_file(value in_fd, value out_fd, value in_offs, value len) 332 { 333 CAMLparam4(in_fd, out_fd, in_offs, len); 334 335 HANDLE hin = Handle_val(in_fd); 336 HANDLE hout = Handle_val(out_fd); 337 338 if (!unsn_copy_file_supports_cloning(hout) 339 || !unsn_copy_file_supports_cloning(hin)) { 340 caml_unix_error(ENOSYS, "copy_file", Nothing); 341 } 342 343 /* INTEGRITY_INFORMATION contains information about fs cluster size. We make 344 * the assumption that both the source and destination have the same cluster 345 * size (From MSDN: The source and destination files must be on the same ReFS 346 * volume.). */ 347 FSCTL_GET_INTEGRITY_INFORMATION_BUFFER intgrty; 348 if (!DeviceIoControl(hin, FSCTL_GET_INTEGRITY_INFORMATION, 349 NULL, 0, &intgrty, sizeof(intgrty), NULL, NULL)) { 350 unsn_copy_file_error(); 351 } 352 353 DUPLICATE_EXTENTS_DATA dupl; 354 dupl.FileHandle = hin; 355 dupl.SourceFileOffset.QuadPart = Int64_val(in_offs); 356 if (!SetFilePointerEx(hout, (LARGE_INTEGER){0}, &dupl.TargetFileOffset, 357 FILE_CURRENT)) { 358 unsn_copy_file_error(); 359 } 360 361 /* From MSDN: The source and destination regions must begin and end at 362 * a cluster boundary. */ 363 if ((dupl.SourceFileOffset.QuadPart % intgrty.ClusterSizeInBytes != 0) 364 || (dupl.TargetFileOffset.QuadPart % intgrty.ClusterSizeInBytes != 0)) { 365 /* FSCTL_DUPLICATE_EXTENTS_TO_FILE would fail anyway if this didn't hold, 366 * but this way we can bail out earlier. */ 367 caml_unix_error(EINVAL, "copy_file", Nothing); 368 } 369 dupl.ByteCount.QuadPart = Long_val(len) + 370 intgrty.ClusterSizeInBytes - (Long_val(len) % intgrty.ClusterSizeInBytes); 371 372 /* From MSDN: If the source file is sparse, the destination file must also be 373 * sparse. */ 374 BOOL in_is_sparse = unsn_copy_file_get_sparse(hin); 375 BOOL out_is_sparse = unsn_copy_file_get_sparse(hout); 376 /* Based on the wording in MSDN, it is ok if the destination file is sparse 377 * and the source is not, the opposite is not. */ 378 if (in_is_sparse && !out_is_sparse) { 379 unsn_copy_file_set_sparse(hout, TRUE); 380 } 381 382 /* From MSDN: The destination region must not extend past the end of file. */ 383 LARGE_INTEGER newTargetLen; 384 newTargetLen.QuadPart = dupl.TargetFileOffset.QuadPart + Long_val(len); 385 FILE_STANDARD_INFO fi; 386 if (!GetFileInformationByHandleEx(hout, FileStandardInfo, &fi, sizeof(fi))) { 387 unsn_copy_file_error(); 388 } 389 if (fi.EndOfFile.QuadPart < newTargetLen.QuadPart) { 390 /* We want to avoid allocating on disk when extending the file. Make the 391 * destination file temporarily sparse, if it's not already. */ 392 if (!out_is_sparse && !in_is_sparse) { 393 unsn_copy_file_set_sparse(hout, TRUE); 394 } 395 FILE_END_OF_FILE_INFO eofi; 396 eofi.EndOfFile = newTargetLen; 397 if (!SetFileInformationByHandle(hout, FileEndOfFileInfo, &eofi, sizeof(eofi))) { 398 unsn_copy_file_error(); 399 } 400 /* The destination file was set to sparse when extending it; revert the 401 * sparse status. */ 402 if (!out_is_sparse && !in_is_sparse) { 403 unsn_copy_file_set_sparse(hout, FALSE); 404 } 405 } 406 407 caml_release_runtime_system(); 408 BOOL res = DeviceIoControl(hout, FSCTL_DUPLICATE_EXTENTS_TO_FILE, 409 &dupl, sizeof(dupl), NULL, 0, NULL, NULL); 410 caml_acquire_runtime_system(); 411 if (!res) { 412 unsn_copy_file_error(); 413 } else { 414 /* FSCTL_DUPLICATE_EXTENTS_TO_FILE does not advance the destination file 415 * offset. */ 416 LARGE_INTEGER copied; 417 copied.QuadPart = Long_val(len); 418 if (!SetFilePointerEx(hout, copied, NULL, FILE_CURRENT)) { 419 unsn_copy_file_error(); 420 } 421 } 422 423 CAMLreturn(len); 424 } 425 426 #else /* defined(__linux__) || defined(__FreeBSD__) || defined(__sun) || defined(_WIN32) */ 427 428 429 CAMLprim value unison_copy_file(value in_fd, value out_fd, value in_offs, value len) 430 { 431 CAMLparam4(in_fd, out_fd, in_offs, len); 432 caml_unix_error(ENOSYS, "copy_file", Nothing); 433 CAMLreturn(Val_long(0)); 434 } 435 436 437 #endif /* defined(__linux__) || defined(__FreeBSD__) || defined (__sun) || defined(_WIN32) */