diff --git a/usr.bin/truncate/tests/Makefile b/usr.bin/truncate/tests/Makefile --- a/usr.bin/truncate/tests/Makefile +++ b/usr.bin/truncate/tests/Makefile @@ -1,3 +1,7 @@ + +BINDIR?= ${TESTSDIR} +PROGS+= lssparse + ATF_TESTS_SH= truncate_test .include diff --git a/usr.bin/truncate/tests/lssparse.c b/usr.bin/truncate/tests/lssparse.c new file mode 100644 --- /dev/null +++ b/usr.bin/truncate/tests/lssparse.c @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2025 Klara, Inc. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +static void +usage(const char *prog) +{ + + fprintf(stderr, "usage: %s -v \n", prog); + exit(1); +} + +static void +report_hole(off_t start, off_t end) +{ + + printf("[%zu,%zu)\n", start, end); +} + +static int +lssparse(int fd, off_t filesz) +{ + off_t doff = 0, hoff; + + while ((hoff = lseek(fd, doff, SEEK_HOLE)) != filesz) { + doff = lseek(fd, hoff, SEEK_DATA); + if (doff == -1) { + /* Hole to EOF */ + report_hole(hoff, filesz); + break; + } + + report_hole(hoff, doff); + } + + return (0); +} + +int +main(int argc, char *argv[]) +{ + struct stat sb; + const char *prog = argv[0]; + const char *file; + int ch, fd; + bool sizeonly = false; + + while ((ch = getopt(argc, argv, "s")) != -1) { + switch (ch) { + case 's': + sizeonly = true; + break; + default: + usage(prog); + break; + } + } + + argc -= optind; + argv += optind; + + if (argc != 1) + usage(prog); + + file = argv[0]; + fd = open(file, O_RDONLY, 0644); + if (fd == -1) + err(1, "open"); + + if (fstat(fd, &sb) == -1) + err(1, "fstat"); + + /* + * Not all systems have _PC_MIN_HOLE_SIZE, so we'll start there. If + * that fails, we'll try to guess based on the reported block size. + * + * Some filesystems may also report a smaller minimum hole size because + * it's complicated. Files on ZFS, for instance, can have holes in the + * neighborhood of the _PC_MIN_HOLE_SIZE that will later disappear if + * non-zero data shows up within the same blksize segment of the file. + * Additionally, it won't *complain* if you try to punch a hole that's + * smaller than the block size, but it may not actually be able to punch + * that hole if the block contains other data. We'll use the larger of + * the minimum hole size and the block size to be safe. + */ + if (sizeonly) { + size_t holesz = 0; + + holesz = fpathconf(fd, _PC_MIN_HOLE_SIZE); + if (holesz == (size_t)-1) + err(1, "fpathconf"); + else if (holesz == 0) + errx(1, "filesystem does not report hole support"); + if ((size_t)sb.st_blksize > holesz) + holesz = sb.st_blksize; + + printf("%zu\n", holesz); + return (0); + } + + return (lssparse(fd, sb.st_size)); +} diff --git a/usr.bin/truncate/tests/truncate_test.sh b/usr.bin/truncate/tests/truncate_test.sh --- a/usr.bin/truncate/tests/truncate_test.sh +++ b/usr.bin/truncate/tests/truncate_test.sh @@ -438,6 +438,85 @@ [ ${st_size} -eq 0 ] || atf_fail "new file should now be 0 bytes" } +atf_test_case deallocate cleanup +deallocate_head() +{ + atf_set "descr" "Verifies that -d punches a hole in the file" +} +deallocate_body() +{ + lssparse="$(atf_get_srcdir)/lssparse" + + # We mount our own tmpfs here to avoid false positives due to the + # native filesystem. ZFS with compression enabled, in particular, + # will helpfully compress zero blocks down transparently and fail our + # first (empty) lssparse check below. + atf_check mkdir fs + atf_check mount -t tmpfs -o size=8m tmp fs + + :> fs/sparse + blocksz=$("$lssparse" -s fs/sparse) + atf_check test -n ${blocksz} + + # Data block, hole (but real zeroes), data block + atf_check -x "jot -nb 'A' -s '' ${blocksz} > fs/sparse" + atf_check -e not-empty dd if=/dev/zero of=fs/sparse \ + bs=${blocksz} oseek=1 count=1 conv=notrunc + atf_check -x "jot -nb 'A' -s '' ${blocksz} >> fs/sparse" + + atf_check cp fs/sparse fs/sparse.orig + atf_check cp fs/sparse fs/sparse.orig.orig + + # Punch a hole in the middle, ensure the file hasn't changed. + hstart=${blocksz} + hend=$((blocksz + blocksz)) + atf_check -o empty "${lssparse}" fs/sparse + atf_check truncate -d -o ${blocksz} -l ${blocksz} fs/sparse + atf_check -o inline:"[${hstart},${hend})\n" "${lssparse}" fs/sparse + atf_check cmp -s fs/sparse fs/sparse.orig + + # cmp(1) above may have caused us to page-in the holes in fs/sparse and + # thus, it becomes non-sparse. Just cheat and redo the previously + # sparsified segment to setup the next test (which confirms that we can + # create holes without disturbing the rest). + atf_check truncate -d -o ${blocksz} -l ${blocksz} fs/sparse + + # Clobber the end part of the original file and punch a hole in + # the same spot on the new file, ensure that it has zeroed out that + # portion. + hend=$((blocksz * 3)) + atf_check -e not-empty dd if=/dev/zero of=fs/sparse.orig \ + bs=${blocksz} oseek=2 count=1 conv=notrunc + # Still no real holes... + atf_check -o empty "${lssparse}" fs/sparse.orig + atf_check truncate -d -o $((blocksz * 2)) -l ${blocksz} fs/sparse + atf_check cmp -s fs/sparse fs/sparse.orig + + # Cheat one last time to try it at the beginning. + atf_check truncate -d -o ${blocksz} -l $((blocksz * 2)) fs/sparse + + hstart="0" + atf_check -e not-empty dd if=/dev/zero of=fs/sparse.orig \ + bs=${blocksz} oseek=0 count=1 conv=notrunc + atf_check -o empty "${lssparse}" fs/sparse.orig + atf_check truncate -d -l ${blocksz} fs/sparse + atf_check cmp -s fs/sparse fs/sparse.orig + + # Now bring the original file back and make sure that punching a hole + # in data at the beginning doesn't disturb the data at the end. + atf_check mv fs/sparse.orig.orig fs/sparse.orig + atf_check -o empty "${lssparse}" fs/sparse.orig + atf_check cp fs/sparse.orig fs/sparse + atf_check -e not-empty dd if=/dev/zero of=fs/sparse.orig \ + bs=${blocksz} oseek=0 count=1 conv=notrunc + atf_check truncate -d -l ${blocksz} fs/sparse + atf_check cmp -s fs/sparse fs/sparse.orig +} +deallocate_cleanup() +{ + umount fs || true +} + atf_init_test_cases() { atf_add_test_case illegal_option @@ -459,4 +538,5 @@ atf_add_test_case roundup atf_add_test_case rounddown atf_add_test_case rounddown_zero + atf_add_test_case deallocate } diff --git a/usr.bin/truncate/truncate.c b/usr.bin/truncate/truncate.c --- a/usr.bin/truncate/truncate.c +++ b/usr.bin/truncate/truncate.c @@ -62,7 +62,6 @@ int do_refer; int got_size; char *fname, *rname; - struct spacectl_range sr; fd = -1; rsize = tsize = sz = off = 0; @@ -198,6 +197,8 @@ tsize = 0; if (do_dealloc == 1) { + struct spacectl_range sr; + sr.r_offset = off; sr.r_len = len; r = fspacectl(fd, SPACECTL_DEALLOC, &sr, 0, &sr);