]> git.saurik.com Git - apple/copyfile.git/commitdiff
copyfile-146.50.5.tar.gz macos-10134 macos-10135 macos-10136 v146.50.5
authorApple <opensource@apple.com>
Wed, 15 Nov 2017 06:18:32 +0000 (06:18 +0000)
committerApple <opensource@apple.com>
Wed, 15 Nov 2017 06:18:32 +0000 (06:18 +0000)
copyfile.3
copyfile.c
copyfile.h
copyfile.xcodeproj/project.pbxproj
copyfile_test/main.c [new file with mode: 0644]
copyfile_test/sparse_test.c [new file with mode: 0644]
copyfile_test/sparse_test.h [new file with mode: 0644]
copyfile_test/systemx.c [new file with mode: 0644]
copyfile_test/systemx.h [new file with mode: 0644]
copyfile_test/test_utils.c [new file with mode: 0644]
copyfile_test/test_utils.h [new file with mode: 0644]

index 4233ce9bcfdddf9d5864436ba4a4c7a2f8d597ee..ceb898b037909a03d071563293ef4c2a92d5e05d 100644 (file)
@@ -201,6 +201,15 @@ supported, see below for more information.
 (This is only applicable for the
 .Fn copyfile
 function.)
+.It Dv COPYFILE_DATA_SPARSE
+Copy a file sparsely.
+This requires that the source and destination file systems support sparse files with hole sizes
+at least as large as their block sizes.
+This also requires that the source file is sparse, and for
+.Fn fcopyfile
+the source file descriptor's offset be a multiple of the minimum hole size.
+If COPYFILE_DATA is also specified, this will fall back to a full copy
+if sparse copying cannot be performed for any reason; otherwise, an error is returned.
 .It Dv COPYFILE_NOFOLLOW
 This is a convenience macro, equivalent to
 .Dv (COPYFILE_NOFOLLOW_DST | COPYFILE_NOFOLLOW_SRC) .
@@ -610,6 +619,11 @@ was a negative number.
 A memory allocation failed.
 .It Bq Er ENOTSUP
 The source file was not a directory, symbolic link, or regular file.
+.It Bq Er ENOTSUP
+COPYFILE_CLONE_FORCE was specified and file cloning is not supported.
+.It Bq Er ENOTSUP
+COPYFILE_DATA_SPARSE was specified, sparse copying is not supported,
+and COPYFILE_DATA was not specified.
 .It Bq Er ECANCELED
 The copy was cancelled by callback.
 .It Bq Er EEXIST
index 91e4d9ae59936994fec751dcab61e4d92245bc1c..f07b86d4b03bd21e2f08e544b503558db35155aa 100644 (file)
@@ -649,7 +649,7 @@ copytree(copyfile_state_t s)
                goto done;
        }
 
-       flags = s->flags & (COPYFILE_ALL | COPYFILE_NOFOLLOW | COPYFILE_VERBOSE | COPYFILE_EXCL | COPYFILE_CLONE);
+       flags = s->flags & (COPYFILE_ALL | COPYFILE_NOFOLLOW | COPYFILE_VERBOSE | COPYFILE_EXCL | COPYFILE_CLONE | COPYFILE_DATA_SPARSE);
 
        paths[0] = src = s->src;
        dst = s->dst;
@@ -1422,7 +1422,7 @@ static int copyfile_internal(copyfile_state_t s, copyfile_flags_t flags)
         * the non-meta data portion of the file.  We attempt to
         * remove (via unlink) the destination file if we fail.
         */
-       if (COPYFILE_DATA & flags)
+       if ((COPYFILE_DATA|COPYFILE_DATA_SPARSE) & flags)
        {
                if ((ret = copyfile_data(s)) < 0)
                {
@@ -2025,6 +2025,297 @@ static copyfile_flags_t copyfile_check(copyfile_state_t s)
        return ret;
 }
 
+/*
+ * Attempt to copy the data section of a file sparsely.
+ * Requires that the source and destination file systems support sparse files.
+ * Also requires that the source file descriptor's offset is a multiple of the smaller of the
+ * source and destination file systems' block size.
+ * In practice, this means that we refuse to perform copies that are only partially sparse.
+ * Returns 0 if the source sparse file was copied, -1 on an unrecoverable error that
+ * callers should propagate, and ENOTSUP where this routine refuses to copy the source file.
+ * In this final case, callers are free to attempt a full copy.
+ */
+static int copyfile_data_sparse(copyfile_state_t s, size_t input_blk_size, size_t output_blk_size)
+{
+       int src_fd = s->src_fd, dst_fd = s->dst_fd, rc = 0;
+       off_t src_start, dst_start, src_size = s->sb.st_size;
+       off_t first_hole_offset, next_hole_offset, current_src_offset, next_src_offset;
+       ssize_t nread;
+       size_t iosize = MIN(input_blk_size, output_blk_size);
+       copyfile_callback_t status = s->statuscb;
+       char *bp = NULL;
+       bool use_punchhole = true;
+       errno = 0;
+
+       // Sanity checks.
+       if (!(s->flags & COPYFILE_DATA_SPARSE)) {
+               // Don't attempt this unless the right flags are passed.
+               return ENOTSUP;
+       } else if (src_size <= 0) {
+               // The file size of our source is invalid; there's nothing to copy.
+               errno = EINVAL;
+               goto error_exit;
+       }
+
+       // Since a major underlying filesystem requires that holes are block-aligned,
+       // we only punch holes if we can guarantee that all holes from the source can
+       // be holes in the destination, which requires that the source filesystem's block size
+       // be an integral multiple of the destination filesystem's block size.
+       if (input_blk_size % output_blk_size != 0) {
+               use_punchhole = false;
+       }
+
+       // Get the starting src/dest file descriptor offsets.
+       src_start = lseek(src_fd, 0, SEEK_CUR);
+       dst_start = lseek(dst_fd, 0, SEEK_CUR);
+       if (src_start < 0 || src_start >= src_size || dst_start < 0) {
+               /*
+                * Invalid starting source/destination offset:
+                * Either < 0 which is plainly invalid (lseek may have failed),
+                * or > EOF which means that the copy operation is undefined,
+                * as by definition there is no data past EOF.
+                */
+               if (errno == 0) {
+                       errno = EINVAL;
+               }
+               copyfile_warn("Invalid file descriptor offset, cannot perform a sparse copy");
+               goto error_exit;
+       } else if (src_start != (off_t) roundup(src_start, iosize) ||
+                          dst_start != (off_t) roundup(dst_start, iosize)) {
+               // If the starting offset isn't a multiple of the iosize, we can't do an entire sparse copy.
+               // Fall back to copyfile_data(), which will perform a full copy from the starting position.
+               return ENOTSUP;
+       }
+
+       // Make sure that there is at least one hole in this [part of the] file.
+       first_hole_offset = lseek(src_fd, src_start, SEEK_HOLE);
+       if (first_hole_offset == -1 || first_hole_offset == src_size) {
+               /*
+                * Either an error occurred, the src starting position is EOF, or there are no
+                * holes in this [portion of the] source file. Regardless, we rewind the source file
+                * and return ENOTSUP so copyfile_data() can attempt a full copy.
+                */
+               if (lseek(src_fd, src_start, SEEK_SET) == -1) {
+                       goto error_exit;
+               }
+               return ENOTSUP;
+       }
+
+       // We are ready to begin copying.
+       // First, truncate the destination file to zero out any existing contents.
+       // Then, truncate it again to its eventual size.
+       if (ftruncate(dst_fd, dst_start) == -1) {
+               copyfile_warn("Could not zero destination file before copy");
+               goto error_exit;
+       } else if (ftruncate(dst_fd, dst_start + src_size - src_start) == -1) {
+               copyfile_warn("Could not set destination file size before copy");
+               goto error_exit;
+       }
+
+       // Set the source's offset to the first data section.
+       current_src_offset = lseek(src_fd, src_start, SEEK_DATA);
+       if (current_src_offset == -1) {
+               if (errno == ENXIO) {
+                       // There are no more data sections in the file, so there's nothing to copy.
+                       goto set_total_copied;
+               }
+               goto error_exit;
+       }
+
+       // Now, current_src_offset points at the start of src's first data region.
+       // Update dst_fd to point to the same offset (respecting its start).
+       if (lseek(dst_fd, dst_start + current_src_offset - src_start, SEEK_SET) == -1) {
+               copyfile_warn("failed to set dst to first data section");
+               goto error_exit;
+       }
+
+       // Allocate a temporary buffer to copy data sections into.
+       bp = malloc(iosize);
+       if (bp == NULL) {
+               copyfile_warn("No memory for copy buffer");
+               goto error_exit;
+       }
+
+       /*
+        * Performing a sparse copy:
+        * While our source fd points to a data section (and is < EOF), read iosize bytes in.
+        * Then, write those bytes to the dest fd, using the same iosize.
+        * Finally, update our source and dest fds to point to the next data section.
+        */
+       while ((nread = read(src_fd, bp, iosize)) > 0) {
+               ssize_t nwritten;
+               size_t left = nread;
+               void *ptr = bp;
+               int loop = 0;
+
+               while (left > 0) {
+                       nwritten = write(dst_fd, ptr, left);
+                       switch (nwritten) {
+                               case 0:
+                                       if (++loop > 5) {
+                                               copyfile_warn("writing to output %d times resulted in 0 bytes written", loop);
+                                               errno = EAGAIN;
+                                               goto error_exit;
+                                       }
+                                       break;
+                               case -1:
+                                       copyfile_warn("writing to output file failed");
+                                       if (status) {
+                                               int rv = (*status)(COPYFILE_COPY_DATA, COPYFILE_ERR, s, s->src, s->dst, s->ctx);
+                                               if (rv == COPYFILE_SKIP) {      // Skip the data copy
+                                                       errno = 0;
+                                                       goto exit;
+                                               } else if (rv == COPYFILE_CONTINUE) {   // Retry the write
+                                                       errno = 0;
+                                                       continue;
+                                               }
+                                       }
+                                       // If we get here, we either have no callback or it didn't tell us to continue.
+                                       goto error_exit;
+                                       break;
+                               default:
+                                       left -= nwritten;
+                                       ptr = ((char*)ptr) + nwritten;
+                                       loop = 0;
+                                       break;
+                       }
+                       s->totalCopied += nwritten;
+                       if (status) {
+                               int rv = (*status)(COPYFILE_COPY_DATA, COPYFILE_PROGRESS, s, s->src, s->dst, s->ctx);
+                               if (rv == COPYFILE_QUIT) {
+                                       errno = ECANCELED;
+                                       goto error_exit;
+                               }
+                       }
+               }
+               current_src_offset += nread;
+
+               // Find the next area of src_fd to copy.
+               // Since data sections can be any length, we need see if current_src_offset points
+               // at a hole.
+               // If we get ENXIO, we're done copying (the last part of the file was a data section).
+               // If this is not a hole, we do not need to alter current_src_offset yet.
+               // If this is a hole, then we need to look for the next data section.
+               next_hole_offset = lseek(src_fd, current_src_offset, SEEK_HOLE);
+               if (next_hole_offset == -1) {
+                       if (errno == ENXIO) {
+                               break; // We're done copying data sections.
+                       }
+                       copyfile_warn("unable to find next hole in file during copy");
+                       goto error_exit;
+               } else if (next_hole_offset != current_src_offset) {
+                       // Keep copying this data section (we must rewind src_fd to current_src_offset).
+                       if (lseek(src_fd, current_src_offset, SEEK_SET) == -1) {
+                               goto error_exit;
+                       }
+                       continue;
+               }
+
+               // If we get here, we need to find the next data section to copy.
+               next_src_offset = lseek(src_fd, current_src_offset, SEEK_DATA);
+               if (next_src_offset == -1) {
+                       if (errno == ENXIO) {
+                               // There are no more data sections in this file, so we're done with the copy.
+                               break;
+                       }
+
+                       copyfile_warn("unable to advance src to next data section");
+                       goto error_exit;
+               }
+
+               // Advance the dst_fd to match (taking into account where it started).
+               if (lseek(dst_fd, dst_start + (next_src_offset - src_start), SEEK_SET) == -1) {
+                       copyfile_warn("unable to advance dst to next data section");
+                       goto error_exit;
+               }
+
+               current_src_offset = next_src_offset;
+       }
+       if (nread < 0) {
+               copyfile_warn("error %d reading from %s", errno, s->src ? s->src : "(null src)");
+               goto error_exit;
+       }
+
+       // Punch holes where possible if needed.
+       if (use_punchhole) {
+               struct fpunchhole punchhole_args;
+               off_t hole_start = first_hole_offset, hole_end;
+               bool trailing_hole = true;
+
+               // First, reset the source and destination file descriptors.
+               if (lseek(src_fd, src_start, SEEK_SET) == -1 || lseek(dst_fd, dst_start, SEEK_SET) == -1) {
+                       copyfile_warn("unable to reset file descriptors to punch holes");
+                       // We have still copied the data, so there's no need to return an error here.
+                       goto set_total_copied;
+               }
+
+               // Now, find holes in the source (first_hole_offset already points to a source hole),
+               // determining their length by the presence of a data section.
+               while ((hole_end = lseek(src_fd, hole_start + (off_t) iosize, SEEK_DATA)) != -1) {
+                       memset(&punchhole_args, 0, sizeof(punchhole_args));
+
+                       // Fix up the offset and length for the destination file.
+                       punchhole_args.fp_offset = hole_start - src_start + dst_start;
+                       punchhole_args.fp_length = hole_end - hole_start;
+                       if (fcntl(dst_fd, F_PUNCHHOLE, &punchhole_args) == -1) {
+                               copyfile_warn("unable to punch hole in destination file, offset %lld length %lld",
+                                                         hole_start - src_start + dst_start, hole_end - hole_start);
+                               goto set_total_copied;
+                       }
+
+                       // Now, find the start of the next hole.
+                       hole_start = lseek(src_fd, hole_end, SEEK_HOLE);
+                       if (hole_start == -1 || hole_start == src_size) {
+                               // No more holes (or lseek failed), so break.
+                               trailing_hole = false;
+                               break;
+                       }
+               }
+
+               if ((hole_end == -1 || hole_start == -1) && errno != ENXIO) {
+                       // A call to lseek() failed. Hole punching is best effort, so exit.
+                       copyfile_warn("lseek during hole punching failed");
+                       goto set_total_copied;
+               }
+
+               // We will still have a trailing hole to punch if the last lseek(SEEK_HOLE) succeeded.
+               if (trailing_hole) {
+                       // Since we can only punch iosize-aligned holes, we must make sure the last hole
+                       // is iosize-aligned. Unfortunately, no good truncate macros are in scope here,
+                       // so we must round down the end of the trailing hole to an iosize boundary ourselves.
+                       hole_end = (src_size % iosize == 0) ? src_size : roundup(src_size, iosize) - iosize;
+
+                       memset(&punchhole_args, 0, sizeof(punchhole_args));
+                       punchhole_args.fp_offset = hole_start - src_start + dst_start;
+                       punchhole_args.fp_length = hole_end - hole_start;
+                       if (fcntl(dst_fd, F_PUNCHHOLE, &punchhole_args) == -1) {
+                               copyfile_warn("unable to punch trailing hole in destination file, offset %lld",
+                                                         hole_start - src_start + dst_start);
+                               goto set_total_copied;
+                       }
+               }
+       }
+
+set_total_copied:
+       // Since we don't know in advance how many bytes we're copying, we advance this number
+       // as we copy, but to match copyfile_data() we set it here to the amount of bytes that would
+       // have been transferred in a full copy.
+       s->totalCopied = src_size - src_start;
+
+exit:
+       if (bp) {
+               free(bp);
+               bp = NULL;
+       }
+
+       return rc;
+
+error_exit:
+       s->err = errno;
+       rc = -1;
+       goto exit;
+}
+
 /*
  * Attempt to copy the data section of a file.  Using blockisize
  * is not necessarily the fastest -- it might be desirable to
@@ -2037,8 +2328,8 @@ static int copyfile_data(copyfile_state_t s)
        char *bp = 0;
        ssize_t nread;
        int ret = 0;
-       size_t iBlocksize = 0;
-       size_t oBlocksize = 0;
+       size_t iBlocksize = 0, iMinblocksize = 0;
+       size_t oBlocksize = 0, oMinblocksize = 0; // If 0, we don't support sparse copying.
        const size_t onegig = 1 << 30;
        struct statfs sfs;
        copyfile_callback_t status = s->statuscb;
@@ -2063,6 +2354,7 @@ static int copyfile_data(copyfile_state_t s)
                iBlocksize = s->sb.st_blksize;
        } else {
                iBlocksize = sfs.f_iosize;
+               iMinblocksize = sfs.f_bsize;
        }
 
        /* Work-around for 6453525, limit blocksize to 1G */
@@ -2070,21 +2362,58 @@ static int copyfile_data(copyfile_state_t s)
                iBlocksize = onegig;
        }
 
-       if ((bp = malloc(iBlocksize)) == NULL)
-               return -1;
-
-       if (fstatfs(s->dst_fd, &sfs) == -1 || sfs.f_iosize == 0) {
+       if (fstatfs(s->dst_fd, &sfs) == -1) {
+               oBlocksize = iBlocksize;
+       } else if (sfs.f_iosize == 0) {
                oBlocksize = iBlocksize;
+               oMinblocksize = sfs.f_bsize;
        } else {
+               oMinblocksize = sfs.f_bsize;
                oBlocksize = sfs.f_iosize;
                if (oBlocksize > onegig)
                        oBlocksize = onegig;
        }
 
+       s->totalCopied = 0;
+
+       // If requested, attempt a sparse copy.
+       if (s->flags & COPYFILE_DATA_SPARSE) {
+               // Check if the source & destination volumes both support sparse files.
+               long min_hole_size = MIN(fpathconf(s->src_fd, _PC_MIN_HOLE_SIZE),
+                                                                fpathconf(s->dst_fd, _PC_MIN_HOLE_SIZE));
+
+               // If holes are supported on both the source and dest volumes, make sure our min_hole_size
+               // is reasonable: if it's smaller than the source/dest block size,
+               // our copy performance will suffer (and we may not create sparse files).
+               if (iMinblocksize > 0 && oMinblocksize > 0 && (size_t) min_hole_size >= iMinblocksize
+                       && (size_t) min_hole_size >= oMinblocksize) {
+                       // Do the copy.
+                       ret = copyfile_data_sparse(s, iMinblocksize, oMinblocksize);
+
+                       // If we returned an error, exit gracefully.
+                       // If sparse copying is not supported, we try full copying if allowed by our caller.
+                       if (ret == 0) {
+                               goto exit;
+                       } else if (ret != ENOTSUP) {
+                               goto exit;
+                       }
+                       ret = 0;
+               }
+
+               // Make sure we're allowed to perform non-sparse copying.
+               if (!(s->flags & COPYFILE_DATA)) {
+                       ret = -1;
+                       errno = ENOTSUP;
+                       goto exit;
+               }
+       }
+
+       if ((bp = malloc(iBlocksize)) == NULL)
+               return -1;
+
        blen = iBlocksize;
 
-       s->totalCopied = 0;
-       /* If supported, do preallocation for Xsan / HFS volumes */
+       /* If supported, do preallocation for Xsan / HFS / apfs volumes */
 #ifdef F_PREALLOCATE
        {
                fstore_t fst;
@@ -2846,6 +3175,9 @@ struct {char *s; int v;} opts[] = {
        COPYFILE_OPTION(VERBOSE)
        COPYFILE_OPTION(RECURSIVE)
        COPYFILE_OPTION(DEBUG)
+       COPYFILE_OPTION(CLONE)
+       COPYFILE_OPTION(CLONE_FORCE)
+       COPYFILE_OPTION(DATA_SPARSE)
        {NULL, 0}
 };
 
@@ -2892,7 +3224,7 @@ int main(int c, char *v[])
  */
 
 
-#define offsetof(type, member) ((size_t)(&((type *)0)->member))
+#define offsetof(type, member) __builtin_offsetof(type, member)
 
 #define        XATTR_MAXATTRLEN   (16*1024*1024)
 
index e071fef09b93652789b9257d2cfd49e5694b4bc5..efea9dd5445ea19f607ac038067d2b7cc1815627 100644 (file)
@@ -106,6 +106,8 @@ typedef int (*copyfile_callback_t)(int, int, copyfile_state_t, const char *, con
 
 #define COPYFILE_RUN_IN_PLACE  (1<<26)
 
+#define COPYFILE_DATA_SPARSE   (1<<27)
+
 #define COPYFILE_VERBOSE       (1<<30)
 
 #define        COPYFILE_RECURSE_ERROR  0
index 87bc6380aa002fb4b592a73aea64e23bce3ee260..37eb53f69a51d5974c20c80e399f8d6a3cee5293 100644 (file)
@@ -7,7 +7,13 @@
        objects = {
 
 /* Begin PBXBuildFile section */
+               721D4F071EA95283000F0555 /* copyfile.c in Sources */ = {isa = PBXBuildFile; fileRef = FCCE17C1135A658F002CEE6D /* copyfile.c */; };
+               721D4F081EA95290000F0555 /* xattr_flags.c in Sources */ = {isa = PBXBuildFile; fileRef = 72406E621676C3C80099568B /* xattr_flags.c */; };
                72406E631676C3C80099568B /* xattr_flags.c in Sources */ = {isa = PBXBuildFile; fileRef = 72406E621676C3C80099568B /* xattr_flags.c */; };
+               726EE9DB1E9423E50017A5B9 /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = 726EE9DA1E9423E50017A5B9 /* main.c */; };
+               726EE9E01E9425160017A5B9 /* sparse_test.c in Sources */ = {isa = PBXBuildFile; fileRef = 726EE9DE1E9425160017A5B9 /* sparse_test.c */; };
+               726EE9E41E946B320017A5B9 /* systemx.c in Sources */ = {isa = PBXBuildFile; fileRef = 726EE9E21E946B320017A5B9 /* systemx.c */; };
+               726EE9E61E946D590017A5B9 /* test_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 726EE9E51E946D590017A5B9 /* test_utils.c */; };
                72B4C0F41676C47D00C13E05 /* copyfile_private.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B4C0F31676C47D00C13E05 /* copyfile_private.h */; settings = {ATTRIBUTES = (Private, ); }; };
                72EAA3B016A72F4500833E98 /* xattr_flags.h in Headers */ = {isa = PBXBuildFile; fileRef = 72EAA3AF16A72F4500833E98 /* xattr_flags.h */; settings = {ATTRIBUTES = (Public, ); }; };
                86EF9F0A1834018C00AAB3F3 /* xattr_properties.h in Headers */ = {isa = PBXBuildFile; fileRef = 86EF9F091834018C00AAB3F3 /* xattr_properties.h */; settings = {ATTRIBUTES = (Private, ); }; };
                FCCE17C4135A658F002CEE6D /* copyfile.h in Headers */ = {isa = PBXBuildFile; fileRef = FCCE17C2135A658F002CEE6D /* copyfile.h */; settings = {ATTRIBUTES = (Public, ); }; };
 /* End PBXBuildFile section */
 
+/* Begin PBXCopyFilesBuildPhase section */
+               726EE9D61E9423E50017A5B9 /* CopyFiles */ = {
+                       isa = PBXCopyFilesBuildPhase;
+                       buildActionMask = 2147483647;
+                       dstPath = /usr/share/man/man1/;
+                       dstSubfolderSpec = 0;
+                       files = (
+                       );
+                       runOnlyForDeploymentPostprocessing = 1;
+               };
+/* End PBXCopyFilesBuildPhase section */
+
 /* Begin PBXFileReference section */
                3F1EFD4C185C4EB400D1C970 /* copyfile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = copyfile.xcconfig; path = xcodescripts/copyfile.xcconfig; sourceTree = "<group>"; };
+               721D4F051EA95008000F0555 /* libcopyfile.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcopyfile.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/lib/system/libcopyfile.tbd; sourceTree = DEVELOPER_DIR; };
                72406E621676C3C80099568B /* xattr_flags.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = xattr_flags.c; sourceTree = "<group>"; };
+               726EE9D81E9423E50017A5B9 /* copyfile_test */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = copyfile_test; sourceTree = BUILT_PRODUCTS_DIR; };
+               726EE9DA1E9423E50017A5B9 /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = "<group>"; };
+               726EE9DE1E9425160017A5B9 /* sparse_test.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = sparse_test.c; sourceTree = "<group>"; };
+               726EE9DF1E9425160017A5B9 /* sparse_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sparse_test.h; sourceTree = "<group>"; };
+               726EE9E11E9427B40017A5B9 /* test_utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = test_utils.h; sourceTree = "<group>"; };
+               726EE9E21E946B320017A5B9 /* systemx.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = systemx.c; sourceTree = "<group>"; };
+               726EE9E31E946B320017A5B9 /* systemx.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = systemx.h; sourceTree = "<group>"; };
+               726EE9E51E946D590017A5B9 /* test_utils.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = test_utils.c; sourceTree = "<group>"; };
                72B4C0F31676C47D00C13E05 /* copyfile_private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = copyfile_private.h; sourceTree = "<group>"; };
                72EAA3AF16A72F4500833E98 /* xattr_flags.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = xattr_flags.h; sourceTree = "<group>"; };
                861E1C14180F0AF900E65B9A /* xattr_name_with_flags.3 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = xattr_name_with_flags.3; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
+               726EE9D51E9423E50017A5B9 /* Frameworks */ = {
+                       isa = PBXFrameworksBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                FCCE17B8135A6444002CEE6D /* Frameworks */ = {
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
+               721D4F041EA95007000F0555 /* Frameworks */ = {
+                       isa = PBXGroup;
+                       children = (
+                               721D4F051EA95008000F0555 /* libcopyfile.tbd */,
+                       );
+                       name = Frameworks;
+                       sourceTree = "<group>";
+               };
+               726EE9D91E9423E50017A5B9 /* copyfile_test */ = {
+                       isa = PBXGroup;
+                       children = (
+                               726EE9DA1E9423E50017A5B9 /* main.c */,
+                               726EE9DE1E9425160017A5B9 /* sparse_test.c */,
+                               726EE9DF1E9425160017A5B9 /* sparse_test.h */,
+                               726EE9E51E946D590017A5B9 /* test_utils.c */,
+                               726EE9E11E9427B40017A5B9 /* test_utils.h */,
+                               726EE9E21E946B320017A5B9 /* systemx.c */,
+                               726EE9E31E946B320017A5B9 /* systemx.h */,
+                       );
+                       path = copyfile_test;
+                       sourceTree = "<group>";
+               };
                FCCE17AB135A5FFB002CEE6D = {
                        isa = PBXGroup;
                        children = (
                                FCCE17C1135A658F002CEE6D /* copyfile.c */,
                                86EF9F091834018C00AAB3F3 /* xattr_properties.h */,
                                FCCE17C2135A658F002CEE6D /* copyfile.h */,
+                               726EE9D91E9423E50017A5B9 /* copyfile_test */,
                                FCCE17BC135A6444002CEE6D /* Products */,
+                               721D4F041EA95007000F0555 /* Frameworks */,
                        );
                        sourceTree = "<group>";
                        usesTabs = 1;
                        isa = PBXGroup;
                        children = (
                                FCCE17BB135A6444002CEE6D /* libcopyfile.dylib */,
+                               726EE9D81E9423E50017A5B9 /* copyfile_test */,
                        );
                        name = Products;
                        sourceTree = "<group>";
 /* End PBXHeadersBuildPhase section */
 
 /* Begin PBXNativeTarget section */
+               726EE9D71E9423E50017A5B9 /* copyfile_test */ = {
+                       isa = PBXNativeTarget;
+                       buildConfigurationList = 726EE9DD1E9423E50017A5B9 /* Build configuration list for PBXNativeTarget "copyfile_test" */;
+                       buildPhases = (
+                               726EE9D41E9423E50017A5B9 /* Sources */,
+                               726EE9D51E9423E50017A5B9 /* Frameworks */,
+                               726EE9D61E9423E50017A5B9 /* CopyFiles */,
+                       );
+                       buildRules = (
+                       );
+                       dependencies = (
+                       );
+                       name = copyfile_test;
+                       productName = copyfile_test;
+                       productReference = 726EE9D81E9423E50017A5B9 /* copyfile_test */;
+                       productType = "com.apple.product-type.tool";
+               };
                FCCE17BA135A6444002CEE6D /* copyfile */ = {
                        isa = PBXNativeTarget;
                        buildConfigurationList = FCCE17BE135A6444002CEE6D /* Build configuration list for PBXNativeTarget "copyfile" */;
                        isa = PBXProject;
                        attributes = {
                                ORGANIZATIONNAME = "Apple Inc.";
+                               TargetAttributes = {
+                                       726EE9D71E9423E50017A5B9 = {
+                                               CreatedOnToolsVersion = 9.0;
+                                               ProvisioningStyle = Automatic;
+                                       };
+                               };
                        };
                        buildConfigurationList = FCCE17B0135A5FFB002CEE6D /* Build configuration list for PBXProject "copyfile" */;
                        compatibilityVersion = "Xcode 3.2";
                        projectRoot = "";
                        targets = (
                                FCCE17BA135A6444002CEE6D /* copyfile */,
+                               726EE9D71E9423E50017A5B9 /* copyfile_test */,
                        );
                };
 /* End PBXProject section */
 /* End PBXShellScriptBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
+               726EE9D41E9423E50017A5B9 /* Sources */ = {
+                       isa = PBXSourcesBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               721D4F081EA95290000F0555 /* xattr_flags.c in Sources */,
+                               721D4F071EA95283000F0555 /* copyfile.c in Sources */,
+                               726EE9DB1E9423E50017A5B9 /* main.c in Sources */,
+                               726EE9E61E946D590017A5B9 /* test_utils.c in Sources */,
+                               726EE9E41E946B320017A5B9 /* systemx.c in Sources */,
+                               726EE9E01E9425160017A5B9 /* sparse_test.c in Sources */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                FCCE17B7135A6444002CEE6D /* Sources */ = {
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
 /* End PBXSourcesBuildPhase section */
 
 /* Begin XCBuildConfiguration section */
+               726EE9DC1E9423E50017A5B9 /* Release */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               ALWAYS_SEARCH_USER_PATHS = NO;
+                               CLANG_ANALYZER_NONNULL = YES;
+                               CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+                               CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+                               CLANG_CXX_LIBRARY = "libc++";
+                               CLANG_ENABLE_MODULES = YES;
+                               CLANG_ENABLE_OBJC_ARC = YES;
+                               CLANG_WARN_BOOL_CONVERSION = YES;
+                               CLANG_WARN_CONSTANT_CONVERSION = YES;
+                               CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+                               CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+                               CLANG_WARN_EMPTY_BODY = YES;
+                               CLANG_WARN_ENUM_CONVERSION = YES;
+                               CLANG_WARN_INFINITE_RECURSION = YES;
+                               CLANG_WARN_INT_CONVERSION = YES;
+                               CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+                               CLANG_WARN_SUSPICIOUS_MOVE = YES;
+                               CLANG_WARN_UNREACHABLE_CODE = YES;
+                               CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+                               CODE_SIGN_IDENTITY = "-";
+                               COPY_PHASE_STRIP = NO;
+                               DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+                               ENABLE_NS_ASSERTIONS = NO;
+                               ENABLE_STRICT_OBJC_MSGSEND = YES;
+                               GCC_C_LANGUAGE_STANDARD = gnu99;
+                               GCC_NO_COMMON_BLOCKS = YES;
+                               GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+                               GCC_WARN_UNDECLARED_SELECTOR = YES;
+                               GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+                               LIBRARY_SEARCH_PATHS = (
+                                       "$(inherited)",
+                                       "$(SDKROOT)/usr/lib/system",
+                               );
+                               MACOSX_DEPLOYMENT_TARGET = 10.13;
+                               MTL_ENABLE_DEBUG_INFO = NO;
+                               PRODUCT_NAME = "$(TARGET_NAME)";
+                               SDKROOT = macosx;
+                       };
+                       name = Release;
+               };
                FCCE17B3135A5FFB002CEE6D /* Release */ = {
                        isa = XCBuildConfiguration;
                        baseConfigurationReference = 3F1EFD4C185C4EB400D1C970 /* copyfile.xcconfig */;
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
+               726EE9DD1E9423E50017A5B9 /* Build configuration list for PBXNativeTarget "copyfile_test" */ = {
+                       isa = XCConfigurationList;
+                       buildConfigurations = (
+                               726EE9DC1E9423E50017A5B9 /* Release */,
+                       );
+                       defaultConfigurationIsVisible = 0;
+                       defaultConfigurationName = Release;
+               };
                FCCE17B0135A5FFB002CEE6D /* Build configuration list for PBXProject "copyfile" */ = {
                        isa = XCConfigurationList;
                        buildConfigurations = (
diff --git a/copyfile_test/main.c b/copyfile_test/main.c
new file mode 100644 (file)
index 0000000..616dbe3
--- /dev/null
@@ -0,0 +1,65 @@
+//
+//  main.c
+//  copyfile_test
+//
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <removefile.h>
+
+#include "sparse_test.h"
+#include "test_utils.h"
+
+#define DISK_IMAGE_SIZE_MB     512
+
+#if TARGET_OS_OSX
+#define TEST_DIR                       MOUNT_PATH
+#define USING_DISK_IMAGE       1
+#else
+#define TEST_DIR                       "/tmp/copyfile_test"
+#define USING_DISK_IMAGE       0
+#endif // TARGET_OS_OSX
+
+#define MIN_BLOCKSIZE_B                512
+#define DEFAULT_BLOCKSIZE_B    4096
+#define MAX_BLOCKSIZE_B                16384
+
+int main(__unused int argc, __unused const char * argv[]) {
+       bool failed = false;
+       struct statfs stb;
+
+       // Create a disk image to run our tests in.
+       if (USING_DISK_IMAGE) {
+               disk_image_create(APFS_FSTYPE, DISK_IMAGE_SIZE_MB);
+       } else {
+               (void)removefile(TEST_DIR, NULL, REMOVEFILE_RECURSIVE);
+               assert_no_err(mkdir(TEST_DIR, 0777));
+       }
+
+       // Make sure the test directory exists, is apfs formatted,
+       // and that we have a sane block size.
+       assert_no_err(statfs(TEST_DIR, &stb));
+       assert_no_err(memcmp(stb.f_fstypename, APFS_FSTYPE, sizeof(APFS_FSTYPE)));
+       if (stb.f_bsize < MIN_BLOCKSIZE_B || stb.f_bsize > MAX_BLOCKSIZE_B) {
+               stb.f_bsize = DEFAULT_BLOCKSIZE_B;
+       }
+
+       // Run our tests.
+       sranddev();
+       failed |= do_sparse_test(TEST_DIR, stb.f_bsize);
+       failed |= do_sparse_recursive_test(TEST_DIR, stb.f_bsize);
+       failed |= do_fcopyfile_offset_test(TEST_DIR, stb.f_bsize);
+
+       // Cleanup the disk image we ran our tests on.
+       if (USING_DISK_IMAGE) {
+               disk_image_destroy(false);
+       } else {
+               (void)removefile(TEST_DIR, NULL, REMOVEFILE_RECURSIVE);
+       }
+
+       return failed ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/copyfile_test/sparse_test.c b/copyfile_test/sparse_test.c
new file mode 100644 (file)
index 0000000..ccced99
--- /dev/null
@@ -0,0 +1,402 @@
+//
+//  sparse_test.c
+//  copyfile_test
+//
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <removefile.h>
+#include <sys/fcntl.h>
+#include <sys/stat.h>
+
+#include "../copyfile.h"
+#include "sparse_test.h"
+#include "test_utils.h"
+#include "systemx.h"
+
+#define OPEN_FLAGS     O_CREAT|O_TRUNC|O_RDWR
+#define OPEN_PERM      0666
+#define MKDIR_PERM     0777
+#define NAME_MOD       999
+
+/*
+ * Copy the file pointed to by src_fd (and orig_name) to copy_name,
+ * using copyfile()/fcopyfile() and COPYFILE_DATA. If do_sparse, also pass COPYFILE_DATA_SPARSE.
+ * Before copying, rewind src_fd to start_off.
+ */
+static bool test_copy(int src_fd, char* orig_name, char* copy_name, bool do_sparse, off_t start_off) {
+       struct stat orig_sb, copy_sb;
+       int copy_fd;
+       bool result = true;
+       copyfile_state_t cpf_state;
+
+       // Get ready for the test.
+       memset(&orig_sb, 0, sizeof(orig_sb));
+       memset(&copy_sb, 0, sizeof(copy_sb));
+       assert_with_errno((cpf_state = copyfile_state_alloc()) != NULL);
+       assert_with_errno(lseek(src_fd, start_off, SEEK_SET) == start_off);
+
+       // First, verify copyfile().
+       copyfile_flags_t flags = COPYFILE_ALL;
+       if (do_sparse) {
+               flags |= COPYFILE_DATA_SPARSE;
+       }
+       assert_no_err(copyfile(orig_name, copy_name, cpf_state, flags));
+
+       // The file was (hopefully) copied. Now, we must verify three things:
+       // 1. If (do_sparse), verify that the copy is a sparse file.
+       // For now, let's approximate this by testing that the sizes of the two files are equal.
+       // 2. The copyfile_state_t for the copy returns that all bytes were copied.
+       // 3. The copy and the source have identical contents.
+
+       // 1. The copy is a sparse file.
+       // 2. The copyfile_state_t for the copy returns that all bytes were copied.
+       assert_no_err(stat(orig_name, &orig_sb));
+       assert_no_err(stat(copy_name, &copy_sb));
+       result &= verify_copy_sizes(&orig_sb, &copy_sb, cpf_state, do_sparse, 0);
+
+       // 3. The copy and the source have identical contents.
+       result &= verify_copy_contents(orig_name, copy_name);
+
+       // Post-test cleanup.
+       assert_no_err(copyfile_state_free(cpf_state));
+       assert_no_err(removefile(copy_name, NULL, REMOVEFILE_RECURSIVE));
+       memset(&orig_sb, 0, sizeof(struct stat));
+       memset(&copy_sb, 0, sizeof(struct stat));
+
+       // Next, verify fcopyfile().
+       // Make an fd for the destination.
+       assert_with_errno((copy_fd = open(copy_name, OPEN_FLAGS, OPEN_PERM)) > 0);
+
+       // Call fcopyfile().
+       assert_with_errno((cpf_state = copyfile_state_alloc()) != NULL);
+       assert_no_err(fcopyfile(src_fd, copy_fd, cpf_state, flags));
+
+       // 1. The copy is a sparse file (if start_off is a multiple of the block size).
+       // 2. The copyfile_state_t for the copy returns that all bytes were copied.
+       assert_no_err(fstat(src_fd, &orig_sb));
+       assert_no_err(fstat(copy_fd, &copy_sb));
+       result &= verify_copy_sizes(&orig_sb, &copy_sb, cpf_state,
+                                                               start_off % copy_sb.st_blksize ? false : do_sparse, start_off);
+
+       // 3. The copy and the source have identical contents.
+       if (start_off == 0) {
+               result &= verify_copy_contents(orig_name, copy_name);
+       }
+
+       // Post-test cleanup.
+       assert_no_err(copyfile_state_free(cpf_state));
+       assert_no_err(removefile(copy_name, NULL, REMOVEFILE_RECURSIVE));
+       assert_no_err(close(copy_fd));
+
+       return result;
+}
+
+
+// Sparse file creation functions.
+// Each take the source file descriptor pointing at the beginning of the file and the block size.
+// Each return the offset we should return the fd to before any copying should be performed.
+typedef off_t (*creator_func)(int, off_t);
+
+static off_t write_start_and_end_holes(int fd, off_t block_size) {
+       assert_with_errno(pwrite(fd, "j", 1, block_size) == 1);
+       assert_no_err(ftruncate(fd, 3 * block_size));
+
+       assert_no_err(create_hole_in_fd(fd, 0, block_size));
+       assert_no_err(create_hole_in_fd(fd, 2 * block_size, block_size));
+       return 0;
+}
+
+static off_t write_end_hole(int fd, off_t block_size) {
+       assert_with_errno(pwrite(fd, "n", 1, 0) == 1);
+       assert_no_err(ftruncate(fd, 16 * block_size));
+
+       assert_no_err(create_hole_in_fd(fd, block_size, 15 * block_size));
+       return 0;
+}
+
+static off_t write_start_hole(int fd, off_t block_size) {
+       assert_with_errno(pwrite(fd, "p", 1, 16 * block_size) == 1);
+
+       assert_no_err(create_hole_in_fd(fd, 0, 16 * block_size));
+       return 0;
+}
+
+static off_t write_middle_hole(int fd, off_t block_size) {
+       assert_with_errno(pwrite(fd, "k", 1, 0) == 1);
+       assert_with_errno(pwrite(fd, "k", 1, 4 * block_size) == 1);
+       assert_no_err(ftruncate(fd, 5 * block_size));
+
+       assert_no_err(create_hole_in_fd(fd, block_size, 3 * block_size));
+       return 0;
+}
+
+static off_t write_start_and_middle_holes(int fd, off_t block_size) {
+       assert_with_errno(pwrite(fd, "l", 1, 16 * block_size) == 1);
+       assert_with_errno(pwrite(fd, "l", 1, 32 * block_size) == 1);
+
+       assert_no_err(create_hole_in_fd(fd, 0, 16 * block_size));
+       assert_no_err(create_hole_in_fd(fd, 17 * block_size, 15 * block_size));
+       return 0;
+}
+
+static off_t write_middle_and_end_holes(int fd, off_t block_size) {
+       assert_with_errno(pwrite(fd, "m", 1, 0) == 1);
+       assert_with_errno(pwrite(fd, "m", 1, 16 * block_size) == 1);
+       assert_no_err(ftruncate(fd, 32 * block_size));
+
+       assert_no_err(create_hole_in_fd(fd, block_size, 15 * block_size));
+       assert_no_err(create_hole_in_fd(fd, 17 * block_size, 15 * block_size));
+       return 0;
+}
+
+static off_t write_no_sparse(int fd, __unused off_t block_size) {
+       assert_with_errno(pwrite(fd, "z", 1, 0) == 1);
+       return 0;
+}
+
+static off_t write_sparse_odd_offset(int fd, off_t block_size) {
+       assert_with_errno(pwrite(fd, "q", 1, block_size) == 1);
+
+       assert_no_err(create_hole_in_fd(fd, 0, block_size));
+       // Return with the fd pointing at offset 1.
+       assert_with_errno(lseek(fd, 1, SEEK_SET) == 1);
+       return 1;
+}
+
+static off_t write_sparse_bs_offset(int fd, off_t block_size) {
+       assert_with_errno(pwrite(fd, "a", 1, block_size) == 1);
+       assert_with_errno(pwrite(fd, "b", 1, 2 * block_size) == 1);
+
+       assert_no_err(create_hole_in_fd(fd, 0, block_size));
+       // Return with the fd pointing at block_size.
+       assert_with_errno(lseek(fd, block_size, SEEK_SET) == block_size);
+       return block_size;
+}
+
+static off_t write_diff_adj_holes(int fd, off_t block_size) {
+       assert_with_errno(pwrite(fd, "w", 1, 0));
+       assert_with_errno(pwrite(fd, "w", 1, 3 * block_size));
+       assert_no_err(ftruncate(fd, 4 * block_size));
+
+       assert_no_err(create_hole_in_fd(fd, block_size, block_size));
+       assert_no_err(create_hole_in_fd(fd, 2 * block_size, block_size));
+       return 0;
+}
+
+typedef struct {
+       creator_func func; // pointer to function to create a sparse file
+       const char * name; // null terminated string
+} sparse_test_func;
+
+#define NUM_TEST_FUNCTIONS 10
+sparse_test_func test_functions[NUM_TEST_FUNCTIONS] = {
+       {write_start_and_end_holes,             "start_and_end_holes"},
+       {write_middle_hole,                             "middle_hole"},
+       {write_start_and_middle_holes,  "start_and_middle_holes"},
+       {write_middle_and_end_holes,    "middle_and_end_holes"},
+       {write_end_hole,                                "end_hole"},
+       {write_start_hole,                              "start_hole"},
+       {write_no_sparse,                               "no_sparse"},
+       {write_sparse_odd_offset,               "write_sparse_odd_offset"},
+       {write_sparse_bs_offset,                "write_sparse_bs_offset"},
+       {write_diff_adj_holes,                  "write_diff_adj_holes"}
+};
+
+bool do_sparse_test(const char* apfs_test_directory, size_t block_size) {
+       int fd, test_file_id;
+       char out_name[BSIZE_B], sparse_copy_name[BSIZE_B], full_copy_name[BSIZE_B];
+       bool success = true, sub_test_success;
+       off_t start_off;
+
+       for (size_t sub_test = 0; sub_test < NUM_TEST_FUNCTIONS; sub_test++) {
+               printf("START [%s]\n", test_functions[sub_test].name);
+               sub_test_success = false;
+
+               // Make new names for this file and its copies.
+               test_file_id = rand() % NAME_MOD;
+               create_test_file_name(apfs_test_directory, "sparse", test_file_id, out_name);
+               create_test_file_name(apfs_test_directory, "copy_sparse", test_file_id, sparse_copy_name);
+               create_test_file_name(apfs_test_directory, "copy_full", test_file_id, full_copy_name);
+
+               // Create the test file.
+               fd = open(out_name, OPEN_FLAGS, OPEN_PERM);
+               assert_with_errno(fd >= 0);
+
+               // Write to the test file, making it sparse.
+               start_off = test_functions[sub_test].func(fd, (off_t) block_size);
+               assert_no_err(fsync(fd));
+
+               // Make sure that a sparse copy is successful.
+               sub_test_success = test_copy(fd, out_name, sparse_copy_name, true, start_off);
+               if (sub_test_success) {
+                       // Make sure that a full copy is successful.
+                       sub_test_success = test_copy(fd, out_name, full_copy_name, false, start_off);
+               }
+
+               // Report the result on stdout.
+               if (!sub_test_success) {
+                       printf("FAIL  [%s]\n", test_functions[sub_test].name);
+                       success = false;
+               } else {
+                       printf("PASS  [%s]\n", test_functions[sub_test].name);
+               }
+
+               // Cleanup for the next test.
+               assert_no_err(close(fd));
+               (void)removefile(out_name, NULL, 0);
+               (void)removefile(sparse_copy_name, NULL, 0);
+               (void)removefile(full_copy_name, NULL, 0);
+               memset(out_name, 0, BSIZE_B);
+               memset(sparse_copy_name, 0, BSIZE_B);
+               memset(full_copy_name, 0, BSIZE_B);
+       }
+
+       return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+bool do_sparse_recursive_test(const char *apfs_test_directory, size_t block_size) {
+       int exterior_file_src_fd, interior_file_src_fd, test_file_id;
+       char exterior_dir_src[BSIZE_B] = {0}, interior_dir_src[BSIZE_B] = {0}, exterior_dir_dst[BSIZE_B] = {0}, interior_dir_dst[BSIZE_B] = {0};
+       char exterior_file_src[BSIZE_B] = {0}, interior_file_src[BSIZE_B] = {0}, exterior_file_dst[BSIZE_B] = {0}, interior_file_dst[BSIZE_B] = {0};
+       struct stat exterior_file_src_sb, interior_file_src_sb, exterior_file_dst_sb, interior_file_dst_sb;
+       bool success = true;
+
+       printf("START [sparse_recursive]\n");
+
+       // Get ready for the test.
+       memset(&exterior_file_src_sb, 0, sizeof(exterior_file_src_sb));
+       memset(&interior_file_src_sb, 0, sizeof(interior_file_src_sb));
+       memset(&exterior_file_dst_sb, 0, sizeof(exterior_file_dst_sb));
+       memset(&interior_file_dst_sb, 0, sizeof(interior_file_dst_sb));
+
+       // Construct our source layout.
+       assert_with_errno(snprintf(exterior_dir_src, BSIZE_B, "%s/recursive_src", apfs_test_directory) > 0);
+       assert_with_errno(snprintf(interior_dir_src, BSIZE_B, "%s/interior", exterior_dir_src) > 0);
+
+       assert_no_err(mkdir(exterior_dir_src, MKDIR_PERM));
+       assert_no_err(mkdir(interior_dir_src, MKDIR_PERM));
+
+       test_file_id = rand() % NAME_MOD;
+       create_test_file_name(exterior_dir_src, "exterior_sparse_file", test_file_id, exterior_file_src);
+       create_test_file_name(interior_dir_src, "interior_sparse_file", test_file_id, interior_file_src);
+
+       // Create the actual test files.
+       exterior_file_src_fd = open(exterior_file_src, OPEN_FLAGS, OPEN_PERM);
+       assert_with_errno(exterior_file_src_fd >= 0);
+       write_start_and_end_holes(exterior_file_src_fd, block_size);
+
+       interior_file_src_fd = open(interior_file_src, OPEN_FLAGS, OPEN_PERM);
+       assert_with_errno(interior_file_src_fd >= 0);
+       write_middle_hole(interior_file_src_fd, block_size);
+
+       // Now, recursively copy our folder using sparse data copying.
+       assert_with_errno(snprintf(exterior_dir_dst, BSIZE_B, "%s/recursive_dst", apfs_test_directory) > 0);
+       assert_no_err(copyfile(exterior_dir_src, exterior_dir_dst, NULL, COPYFILE_ALL|COPYFILE_RECURSIVE|COPYFILE_DATA_SPARSE));
+
+       // The files were (hopefully) copied. Now, we must verify three things:
+       // 1. Verify that the copy is a sparse file.
+       // For now, let's approximate this by testing that the sizes of the two files are equal.
+       // 2. The copy and the source have identical contents.
+
+       // First, construct our destination layout.
+       assert_with_errno(snprintf(exterior_dir_dst, BSIZE_B, "%s/recursive_dst", apfs_test_directory) > 0);
+       create_test_file_name(exterior_dir_dst, "exterior_sparse_file", test_file_id, exterior_file_dst);
+       assert_with_errno(snprintf(interior_dir_dst, BSIZE_B, "%s/interior", exterior_dir_dst) > 0);
+       create_test_file_name(interior_dir_dst, "interior_sparse_file", test_file_id, interior_file_dst);
+
+       // 1. The copy is a sparse file.
+       assert_no_err(fstat(exterior_file_src_fd, &exterior_file_src_sb));
+       assert_no_err(stat(exterior_file_dst, &exterior_file_dst_sb));
+
+       assert_no_err(fstat(interior_file_src_fd, &interior_file_src_sb));
+       assert_no_err(stat(interior_file_dst, &interior_file_dst_sb));
+
+       success &= verify_copy_sizes(&exterior_file_src_sb, &exterior_file_dst_sb, NULL, true, 0);
+       success &= verify_copy_sizes(&interior_file_src_sb, &interior_file_dst_sb, NULL, true, 0);
+
+       // 2. The copy and the source have identical contents.
+       success &= verify_copy_contents(exterior_file_src, exterior_file_dst);
+       success &= verify_copy_contents(interior_file_src, interior_file_dst);
+
+       if (success) {
+               printf("PASS  [sparse_recursive]\n");
+       } else {
+               printf("FAIL  [sparse_recursive]\n");
+       }
+
+       assert_no_err(close(interior_file_src_fd));
+       assert_no_err(close(exterior_file_src_fd));
+       (void)removefile(exterior_dir_src, NULL, REMOVEFILE_RECURSIVE);
+       (void)removefile(exterior_dir_dst, NULL, REMOVEFILE_RECURSIVE);
+
+       return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+bool do_fcopyfile_offset_test(const char *apfs_test_directory, size_t block_size) {
+       int src_fd, sparse_copy_fd, full_copy_fd, test_file_id;
+       char out_name[BSIZE_B], sparse_copy_name[BSIZE_B], full_copy_name[BSIZE_B];
+       bool success = true;
+
+       printf("START [fcopyfile_offset]\n");
+
+       // Make new names for this file and its copies.
+       test_file_id = rand() % NAME_MOD;
+
+       create_test_file_name(apfs_test_directory, "foff_sparse", test_file_id, out_name);
+       create_test_file_name(apfs_test_directory, "foff_copy_sparse", test_file_id, sparse_copy_name);
+       create_test_file_name(apfs_test_directory, "foff_copy_full", test_file_id, full_copy_name);
+
+       // Create the test file.
+       src_fd = open(out_name, OPEN_FLAGS, OPEN_PERM);
+       assert_with_errno(src_fd >= 0);
+       // This writes 5 * block_size bytes.
+       assert_with_errno(lseek(src_fd, write_middle_hole(src_fd, block_size), SEEK_SET) == 0);
+
+       // Create a sparse copy using fcopyfile().
+       sparse_copy_fd = open(sparse_copy_name, OPEN_FLAGS, OPEN_PERM);
+       assert_with_errno(sparse_copy_fd >= 0);
+
+       // Seek the sparse copy to a non-zero offset.
+       assert_with_errno(lseek(sparse_copy_fd, block_size, SEEK_SET) == (off_t) block_size);
+       // Write into the sparse copy a different byte.
+       assert_with_errno(pwrite(sparse_copy_fd, "z", 1, block_size) == 1);
+
+       // Run fcopyfile().
+       assert_no_err(fcopyfile(src_fd, sparse_copy_fd, NULL, COPYFILE_ALL|COPYFILE_DATA_SPARSE));
+
+       // Check that the source matches the copy at the appropriate region.
+       success &= verify_fd_contents(src_fd, 0, sparse_copy_fd, block_size, 4 * block_size);
+
+       // Now, repeat the same procedure with a full copy.
+       assert_with_errno(lseek(src_fd, 0, SEEK_SET) == 0);
+       full_copy_fd = open(full_copy_name, OPEN_FLAGS, OPEN_PERM);
+       assert_with_errno(full_copy_name >= 0);
+
+       assert_with_errno(lseek(full_copy_fd, block_size, SEEK_SET) == (off_t) block_size);
+       assert_with_errno(pwrite(full_copy_fd, "r", 1, block_size) == 1);
+
+       // Run fcopyfile().
+       assert_no_err(fcopyfile(src_fd, full_copy_fd, NULL, COPYFILE_ALL));
+
+       // Check that the source matches the copy at the appropriate region.
+       success &= verify_fd_contents(src_fd, 0, full_copy_fd, block_size, 4 * block_size);
+
+       if (success) {
+               printf("PASS  [fcopyfile_offset]\n");
+       } else {
+               printf("FAIL  [fcopyfile_offset]\n");
+       }
+
+       assert_no_err(close(full_copy_fd));
+       assert_no_err(close(sparse_copy_fd));
+       assert_no_err(close(src_fd));
+       (void)removefile(full_copy_name, NULL, 0);
+       (void)removefile(sparse_copy_name, NULL, 0);
+       (void)removefile(out_name, NULL, 0);
+
+       return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/copyfile_test/sparse_test.h b/copyfile_test/sparse_test.h
new file mode 100644 (file)
index 0000000..414007e
--- /dev/null
@@ -0,0 +1,13 @@
+//
+//  sparse_test.h
+//  copyfile_test
+//
+
+#ifndef sparse_test_h
+#define sparse_test_h
+
+bool do_sparse_test(const char *apfs_test_directory, size_t block_size);
+bool do_sparse_recursive_test(const char *apfs_test_directory, size_t block_size);
+bool do_fcopyfile_offset_test(const char *apfs_test_directory, size_t block_size);
+
+#endif /* sparse_test_h */
diff --git a/copyfile_test/systemx.c b/copyfile_test/systemx.c
new file mode 100644 (file)
index 0000000..2b08bb1
--- /dev/null
@@ -0,0 +1,68 @@
+//
+//     systemx.c
+//     copyfile_test
+//     Stolen from the test routines from the apfs project.
+//
+#include <fcntl.h>
+#include <libgen.h>
+#include <spawn.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+#include "systemx.h"
+#include "test_utils.h"
+
+
+int __attribute__((sentinel)) systemx(const char *prog, ...)
+{
+       const char *args[64];
+
+       va_list ap;
+
+       const char **parg = args;
+
+       *parg++ = basename((char *)prog);
+
+       va_start(ap, prog);
+
+       bool quiet = false, quiet_stderr = false;
+
+       while ((*parg = va_arg(ap, char *))) {
+               if (*parg == SYSTEMX_QUIET) {
+                       quiet = true;
+               } else if (*parg == SYSTEMX_QUIET_STDERR)
+                       quiet_stderr = true;
+               else
+                       ++parg;
+       }
+
+       va_end(ap);
+
+       posix_spawn_file_actions_t facts, *pfacts = NULL;
+
+       if (quiet || quiet_stderr) {
+               posix_spawn_file_actions_init(&facts);
+               if (quiet)
+                       posix_spawn_file_actions_addopen(&facts, STDOUT_FILENO, "/dev/null", O_APPEND, 0);
+               if (quiet_stderr)
+                       posix_spawn_file_actions_addopen(&facts, STDERR_FILENO, "/dev/null", O_APPEND, 0);
+               pfacts = &facts;
+       }
+
+       pid_t pid;
+       assert_no_err(errno = posix_spawn(&pid, prog, pfacts, NULL,
+                                                                         (char * const *)args, NULL));
+
+       if (pfacts)
+               posix_spawn_file_actions_destroy(pfacts);
+
+       int status;
+       assert(ignore_eintr(waitpid(pid, &status, 0), -1) == pid);
+
+       if (WIFEXITED(status))
+               return WEXITSTATUS(status);
+       else
+               return -1;
+}
diff --git a/copyfile_test/systemx.h b/copyfile_test/systemx.h
new file mode 100644 (file)
index 0000000..3ef7388
--- /dev/null
@@ -0,0 +1,15 @@
+//
+//     systemx.h
+//     copyfile_test
+//     Stolen from the test routines from the apfs project.
+//
+
+#ifndef systemx_h_
+#define systemx_h_
+
+#define SYSTEMX_QUIET                          ((void *)1)
+#define SYSTEMX_QUIET_STDERR           ((void *)2)
+
+int __attribute__((sentinel)) systemx(const char *prog, ...);
+
+#endif /* systemx_h_ */
diff --git a/copyfile_test/test_utils.c b/copyfile_test/test_utils.c
new file mode 100644 (file)
index 0000000..97c9efe
--- /dev/null
@@ -0,0 +1,148 @@
+//
+//  test_utils.c
+//  copyfile_test
+//
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <removefile.h>
+#include <unistd.h>
+#include <sys/fcntl.h>
+#include <sys/stat.h>
+
+#include "test_utils.h"
+#include "systemx.h"
+
+bool verify_fd_contents(int orig_fd, off_t orig_pos, int copy_fd, off_t copy_pos, size_t length) {
+       // Read *length* contents of the two fds and make sure they compare as equal.
+       // Don't alter the position of either fd.
+       char orig_contents[length], copy_contents[length];
+       bool equal;
+
+       assert(orig_fd > 0 && copy_fd > 0);
+       memset(orig_contents, 0, length);
+       memset(copy_contents, 0, length);
+
+       // Read the contents into our temporary buffers, and call memcmp().
+       errno = 0;
+       ssize_t pread_res = pread(orig_fd, orig_contents, length, 0);
+       assert_with_errno(pread_res == (off_t) length);
+       assert_with_errno(pread(copy_fd, copy_contents, length, copy_pos) >= 0);
+       equal = (memcmp(orig_contents, copy_contents, length) == 0);
+       if (!equal) {
+               printf("original fd (%lld - %lld) did not match copy (%lld - %lld)\n",
+                          orig_pos, orig_pos + length, copy_pos, copy_pos + length);
+
+               // Find the first non-matching byte and print it out.
+               for (size_t bad_off = 0; bad_off < length; bad_off++) {
+                       if (orig_contents[bad_off] != copy_contents[bad_off]) {
+                               printf("first mismatch is at offset %zu, original 0x%llx COPY 0x%llx\n",
+                                          bad_off, orig_contents[bad_off], copy_contents[bad_off]);
+                               break;
+                       }
+               }
+       }
+
+       return equal;
+}
+
+bool verify_copy_contents(const char *orig_name, const char *copy_name) {
+       // Verify that the copy and the source have identical contents.
+       // Here, we just call out to 'diff' to do the work for us.
+       int rc = systemx(DIFF_PATH,     orig_name, copy_name, SYSTEMX_QUIET, SYSTEMX_QUIET_STDERR, NULL);
+       if (rc != 0) {
+               printf("%s and %s are not identical: diff returned %d\n", orig_name, copy_name, rc);
+               exit(1);
+       }
+
+       return !rc;
+}
+
+bool verify_copy_sizes(struct stat *orig_sb, struct stat *copy_sb, copyfile_state_t cpf_state,
+                                          bool do_sparse, off_t src_start) {
+       off_t cpf_bytes_copied, blocks_offset;
+       bool result = true;
+
+       // If requested, verify that the copy is a sparse file.
+       if (do_sparse) {
+               if (orig_sb->st_size - src_start != copy_sb->st_size) {
+                       printf("original size - offset (%zd) != copy size (%zd)\n",
+                                  orig_sb->st_size - src_start, copy_sb->st_size);
+                       result = false;
+               }
+
+               blocks_offset = src_start / orig_sb->st_blksize;
+               if (orig_sb->st_blocks - blocks_offset < copy_sb->st_blocks) {
+                       printf("original blocks - offset (%zd) < copy blocks (%zd)\n",
+                                  orig_sb->st_blocks - blocks_offset, copy_sb->st_blocks);
+                       result = false;
+               }
+       }
+
+       // Verify that the copyfile_state_t for the copy returns that all bytes were copied.
+       if (cpf_state) {
+               assert_no_err(copyfile_state_get(cpf_state, COPYFILE_STATE_COPIED, &cpf_bytes_copied));
+               if (orig_sb->st_size - src_start != cpf_bytes_copied) {
+                       printf("original size - start (%zd) != copied bytes (%zd)\n",
+                                  orig_sb->st_size - src_start, cpf_bytes_copied);
+                       result = false;
+               }
+       }
+
+       return result;
+}
+
+int create_hole_in_fd(int fd, off_t offset, off_t length) {
+       struct fpunchhole punchhole_args = {
+               .fp_flags = 0,
+               .reserved = 0,
+               .fp_offset = offset,
+               .fp_length = length
+       };
+
+       return fcntl(fd, F_PUNCHHOLE, &punchhole_args);
+}
+
+
+void create_test_file_name(const char *dir, const char *postfix, int id, char *string_out) {
+       // Make a name for this new file and put it in out_name, which should be BSIZE_B bytes.
+       assert_with_errno(snprintf(string_out, BSIZE_B, "%s/testfile-%d.%s", dir, id, postfix) > 0);
+}
+
+void disk_image_create(const char *fstype, size_t size_in_mb) {
+       char size[BSIZE_B];
+
+       // Set up good default values.
+       if (!fstype) {
+               fstype = DEFAULT_FSTYPE;
+       }
+       if (size_in_mb > MAX_DISK_IMAGE_SIZE_MB) {
+               size_in_mb = MAX_DISK_IMAGE_SIZE_MB;
+       }
+       assert_with_errno(snprintf(size, BSIZE_B, "%zum", size_in_mb) >= 3);
+
+       // Unmount and remove the sparseimage if it already exists.
+       disk_image_destroy(true);
+       if (removefile(DISK_IMAGE_PATH, NULL, REMOVEFILE_RECURSIVE) < 0) {
+               assert_with_errno(errno == ENOENT);
+       }
+
+       // Make the disk image.
+       assert_no_err(systemx(HDIUTIL_PATH, SYSTEMX_QUIET, "create", "-fs", fstype,
+                                                 "-size", size, "-type", "SPARSE", "-volname", "apfs_sparse",
+                                                 DISK_IMAGE_PATH, NULL));
+
+       // Attach the disk image.
+       assert_no_err(systemx(HDIUTIL_PATH, SYSTEMX_QUIET, "attach", DISK_IMAGE_PATH, NULL));
+}
+
+void disk_image_destroy(bool allow_failure) {
+       // If the caller allows, ignore any failures (also silence stderr).
+       if (allow_failure) {
+               systemx(HDIUTIL_PATH, "eject", MOUNT_PATH, SYSTEMX_QUIET, SYSTEMX_QUIET_STDERR, NULL);
+       } else {
+               assert_no_err(systemx(HDIUTIL_PATH, "eject", MOUNT_PATH, SYSTEMX_QUIET, NULL));
+       }
+}
diff --git a/copyfile_test/test_utils.h b/copyfile_test/test_utils.h
new file mode 100644 (file)
index 0000000..a8fabf8
--- /dev/null
@@ -0,0 +1,119 @@
+//
+//  test_utils.h
+//  copyfile_test
+//     Based on the test routines from the apfs project.
+//
+
+#ifndef test_utils_h
+#define test_utils_h
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/errno.h>
+
+#include "../copyfile.h"
+
+#define BSIZE_B                                        128
+#define MAX_DISK_IMAGE_SIZE_MB 1024
+
+#define DISK_IMAGE_PATH                        "/tmp/copyfile_sparse.sparseimage"
+#define VOLUME_NAME                            "apfs_sparse"
+#define DEFAULT_FSTYPE                 "JHFS+"
+#define APFS_FSTYPE                            "apfs"
+
+// We assume that we're mounted on /Volumes.
+#define MOUNT_PATH                             "/Volumes/" VOLUME_NAME
+
+#define HDIUTIL_PATH                   "/usr/bin/hdiutil"
+#define DIFF_PATH                              "/usr/bin/diff"
+
+// Test routine helpers.
+bool verify_fd_contents(int orig_fd, off_t orig_pos, int copy_fd, off_t copy_pos, size_t length);
+bool verify_copy_contents(const char *orig_name, const char *copy_name);
+bool verify_copy_sizes(struct stat *orig_sb, struct stat *copy_sb, copyfile_state_t cpf_state,
+                                          bool do_sparse, off_t src_start);
+int create_hole_in_fd(int fd, off_t offset, off_t length);
+void create_test_file_name(const char *dir, const char *postfix, int id, char *string_out);
+
+// Our disk image test functions.
+void disk_image_create(const char *fstype, size_t size_in_mb);
+void disk_image_destroy(bool allow_failure);
+
+// Assertion functions/macros for tests.
+static inline void
+__attribute__((format (printf, 3, 4)))
+__attribute__((noreturn))
+assert_fail_(const char *file, int line, const char *assertion, ...)
+{
+       va_list args;
+       va_start(args, assertion);
+       char *msg;
+       vasprintf(&msg, assertion, args);
+       va_end(args);
+       printf("\n%s:%u: error: %s\n", file, line, msg);
+       exit(1);
+}
+
+#define assert_fail(str, ...)                                          \
+       assert_fail_(__FILE__, __LINE__, str, ## __VA_ARGS__)
+
+#undef assert
+#define assert(condition)                                                                                      \
+       do {                                                                                                                    \
+               if (!(condition))                                                                                       \
+                       assert_fail_(__FILE__, __LINE__,                                                \
+                                               "assertion failed: %s", #condition);            \
+       } while (0)
+
+#define assert_with_errno_(condition, condition_str)                                   \
+       do {                                                                                                                            \
+               if (!(condition))                                                                                               \
+                       assert_fail_(__FILE__, __LINE__, "%s failed: %s",                       \
+                                               condition_str, strerror(errno));                                \
+       } while (0)
+
+#define assert_with_errno(condition)                   \
+       assert_with_errno_((condition), #condition)
+
+#define assert_no_err(condition) \
+       assert_with_errno_(!(condition), #condition)
+
+#define assert_equal(lhs, rhs, fmt)                                                                            \
+       do {                                                                                                                            \
+               typeof (lhs) lhs_ = (lhs);                                                                              \
+               typeof (lhs) rhs_ = (rhs);                                                                              \
+               if (lhs_ != rhs_)                                                                                               \
+                       assert_fail(#lhs " (" fmt ") != " #rhs " (" fmt ")",            \
+                                               lhs_, rhs_);                                                                    \
+       } while (0)
+
+#define assert_equal_(lhs, rhs, lhs_str, rhs_str, fmt)                                 \
+       do {                                                                                                                            \
+               typeof (lhs) lhs_ = (lhs);                                                                              \
+               typeof (lhs) rhs_ = (rhs);                                                                              \
+               if (lhs_ != rhs_)                                                                                               \
+                       assert_fail(lhs_str " (" fmt ") != " rhs_str " (" fmt ")",      \
+                                               lhs_, rhs_);                                                                    \
+       } while (0)
+
+#define assert_equal_int(lhs, rhs)     assert_equal_(lhs, rhs, #lhs, #rhs, "%d")
+#define assert_equal_ll(lhs, rhs)      assert_equal_(lhs, rhs, #lhs, #rhs, "%lld")
+#define assert_equal_str(lhs, rhs)                                                                             \
+       do {                                                                                                                            \
+               const char *lhs_ = (lhs), *rhs_ = (rhs);                                                \
+               if (strcmp(lhs_, rhs_))                                                                                 \
+                       assert_fail("\"%s\" != \"%s\"", lhs_, rhs_);                            \
+       } while (0)
+
+#define ignore_eintr(x, error_val)                                                             \
+       ({                                                                                                                      \
+               typeof(x) eintr_ret_;                                                                   \
+               do {                                                                                                    \
+                       eintr_ret_ = (x);                                                                       \
+               } while (eintr_ret_ == (error_val) && errno == EINTR);  \
+               eintr_ret_;                                                                                             \
+       })
+
+#endif /* test_utils_h */