diff --git a/src/basic/compress.c b/src/basic/compress.c index b741f3db2f..eeecd59b41 100644 --- a/src/basic/compress.c +++ b/src/basic/compress.c @@ -1632,21 +1632,34 @@ int compress_stream( return 0; } -/* Determine whether sparse writes should be used for this fd. Sparse writes are only safe on - * regular files without O_APPEND (O_APPEND ignores lseek position, which would collapse holes). */ static int should_sparse(int fd) { struct stat st; assert(fd >= 0); + /* Determine whether sparse writes should be used for this fd. Sparse writes are only safe on regular + * files, without O_APPEND (which ignores the lseek position, collapsing any holes we try to create), + * and only when starting from a position at or beyond the current end of file (otherwise the skipped + * ranges would overlap pre-existing data instead of being genuinely unwritten). */ + if (fstat(fd, &st) < 0) return -errno; + if (!S_ISREG(st.st_mode)) + return false; + int flags = fcntl(fd, F_GETFL); if (flags < 0) return -errno; - return S_ISREG(st.st_mode) && !FLAGS_SET(flags, O_APPEND); + if (FLAGS_SET(flags, O_APPEND)) + return false; + + off_t pos = lseek(fd, 0, SEEK_CUR); + if (pos < 0) + return -errno; + + return pos >= st.st_size; } /* After sparse decompression, set the file size to the current position to account for diff --git a/src/test/test-compress.c b/src/test/test-compress.c index ec28d6f294..6ab482f718 100644 --- a/src/test/test-compress.c +++ b/src/test/test-compress.c @@ -433,8 +433,8 @@ static void compare_fd(int fda, int fdb) { uint8_t bufa[4096], bufb[4096]; size_t to_read = MIN((size_t) (sta.st_size - offset), sizeof(bufa)); - ASSERT_OK_EQ(loop_read(fda, bufa, to_read, true), (ssize_t) to_read); - ASSERT_OK_EQ(loop_read(fdb, bufb, to_read, true), (ssize_t) to_read); + ASSERT_OK(loop_read_exact(fda, bufa, to_read, /* do_poll= */ false)); + ASSERT_OK(loop_read_exact(fdb, bufb, to_read, /* do_poll= */ false)); ASSERT_EQ(memcmp(bufa, bufb, to_read), 0); offset += to_read; } @@ -499,6 +499,27 @@ TEST(decompress_stream_sparse) { else log_debug("Filesystem does not support holes, skipping sparsity check"); + /* Check if the decompressor clears previous data. + * The original data is 4K data, 64K zeros, 4K data, 64K zeros. + * Now, fill the file with 4K zeros, 64K data, 4K zeros, 64K data. */ + log_debug("/* testing %s sparse decompression to an existing file */", compression_to_string(c)); + + ASSERT_OK_ERRNO(ftruncate(decompressed, 0)); + ASSERT_OK_ERRNO(ftruncate(decompressed, 4096)); + ASSERT_OK_ERRNO(lseek(decompressed, 4096, SEEK_SET)); + for (unsigned i = 0; i < 16; i++) + ASSERT_OK(loop_write(decompressed, data_block, sizeof(data_block))); + ASSERT_OK_ERRNO(ftruncate(decompressed, 4096 + 16 * sizeof(data_block) + 4096)); + ASSERT_OK_ERRNO(lseek(decompressed, 4096 + 16 * sizeof(data_block) + 4096, SEEK_SET)); + for (unsigned i = 0; i < 16; i++) + ASSERT_OK(loop_write(decompressed, data_block, sizeof(data_block))); + + ASSERT_OK_ERRNO(lseek(compressed, 0, SEEK_SET)); + ASSERT_OK_ERRNO(lseek(decompressed, 0, SEEK_SET)); + ASSERT_OK_ZERO(decompress_stream(c, compressed, decompressed, st_src.st_size)); + + compare_fd(src, decompressed); + /* Test all-zeros input: entire output should be a hole */ log_debug("/* testing %s sparse decompression of all-zeros */", compression_to_string(c)); {