diff --git a/src/shared/copy.c b/src/shared/copy.c index 457488efa2..a92cfc0f61 100644 --- a/src/shared/copy.c +++ b/src/shared/copy.c @@ -107,6 +107,45 @@ static int look_for_signals(CopyFlags copy_flags) { return 0; } +static int create_hole(int fd, off_t size) { + off_t offset; + off_t end; + + offset = lseek(fd, 0, SEEK_CUR); + if (offset < 0) + return -errno; + + end = lseek(fd, 0, SEEK_END); + if (end < 0) + return -errno; + + /* If we're not at the end of the target file, punch a hole in the existing space using fallocate(). */ + + if (offset < end && fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, offset, MIN(size, end - offset)) < 0) + return -errno; + + if (end - offset >= size) { + /* If we've created the full hole, set the file pointer to the end of the hole we created and exit. */ + if (lseek(fd, offset + size, SEEK_SET) < 0) + return -errno; + + return 0; + } + + /* If we haven't created the full hole, use ftruncate() to grow the file (and the hole) to the + * required size and move the file pointer to the end of the file. */ + + size -= end - offset; + + if (ftruncate(fd, end + size) < 0) + return -errno; + + if (lseek(fd, 0, SEEK_END) < 0) + return -errno; + + return 0; +} + int copy_bytes_full( int fdf, int fdt, uint64_t max_bytes, @@ -220,8 +259,11 @@ int copy_bytes_full( /* If we're in a hole (current offset is not a data offset), create a hole of the * same size in the target file. */ - if (e > c && lseek(fdt, e - c, SEEK_CUR) < 0) - return -errno; + if (e > c) { + r = create_hole(fdt, e - c); + if (r < 0) + return r; + } c = e; /* Set c to the start of the data segment. */ diff --git a/src/test/test-copy.c b/src/test/test-copy.c index 5a1cf605c1..6c7b8d220b 100644 --- a/src/test/test-copy.c +++ b/src/test/test-copy.c @@ -327,6 +327,7 @@ TEST_RET(copy_holes) { char fn[] = "/var/tmp/test-copy-hole-fd-XXXXXX"; char fn_copy[] = "/var/tmp/test-copy-hole-fd-XXXXXX"; struct stat stat; + off_t blksz; int r, fd, fd_copy; fd = mkostemp_safe(fn); @@ -340,11 +341,18 @@ TEST_RET(copy_holes) { return log_tests_skipped("Filesystem doesn't support hole punching"); assert_se(r >= 0); - /* We need to make sure to create a large enough hole and to write some data after it, otherwise - * filesystems (btrfs) might silently discard it. */ + assert_se(fstat(fd, &stat) >= 0); + blksz = stat.st_blksize; + char buf[blksz]; - assert_se(lseek(fd, 1024 * 1024, SEEK_CUR) >= 0); - assert_se(write(fd, "abc", strlen("abc")) >= 0); + /* We need to make sure to create hole in multiples of the block size, otherwise filesystems (btrfs) + * might silently truncate/extend the holes. */ + + assert_se(lseek(fd, blksz, SEEK_CUR) >= 0); + assert_se(write(fd, buf, blksz) >= 0); + assert_se(lseek(fd, 0, SEEK_END) == 2 * blksz); + /* Only ftruncate() can create holes at the end of a file. */ + assert_se(ftruncate(fd, 3 * blksz) >= 0); assert_se(lseek(fd, 0, SEEK_SET) >= 0); assert_se(copy_bytes(fd, fd_copy, UINT64_MAX, COPY_HOLES) >= 0); @@ -352,11 +360,13 @@ TEST_RET(copy_holes) { /* Test that the hole starts at the beginning of the file. */ assert_se(lseek(fd_copy, 0, SEEK_HOLE) == 0); /* Test that the hole has the expected size. */ - assert_se(lseek(fd_copy, 0, SEEK_DATA) == 1024 * 1024); + assert_se(lseek(fd_copy, 0, SEEK_DATA) == blksz); + assert_se(lseek(fd_copy, blksz, SEEK_HOLE) == 2 * blksz); + assert_se(lseek(fd_copy, 2 * blksz, SEEK_DATA) < 0 && errno == ENXIO); /* Test that the copied file has the correct size. */ assert_se(fstat(fd_copy, &stat) >= 0); - assert_se(stat.st_size == 1024 * 1024 + strlen("abc")); + assert_se(stat.st_size == 3 * blksz); close(fd); close(fd_copy);