Index: tests/Makefile =================================================================== --- tests/Makefile (revision 267309) +++ tests/Makefile (working copy) @@ -1,6 +1,14 @@ # $FreeBSD$ TESTSDIR= ${TESTSBASE}/usr.bin/truncate -ATF_TESTS_SH= truncate_test +ATF_TESTS_SH= allocate_test truncate_test +ATF_TESTS_C= sparse_test +CLEANFILES+= allocate_test.sh truncate_test.sh +allocate_test.sh: base_test.func allocate_test.func + cat ${.ALLSRC} > ${.TARGET} + +truncate_test.sh: base_test.func truncate_test.func + cat ${.ALLSRC} > ${.TARGET} + .include Index: tests/allocate_test.func =================================================================== --- tests/allocate_test.func (revision 0) +++ tests/allocate_test.func (working copy) @@ -0,0 +1,21 @@ +atf_init_test_cases() +{ + TRUNC="truncate -a" + + atf_add_test_case illegal_option + atf_add_test_case illegal_size + atf_add_test_case too_large_size + atf_add_test_case opt_c + atf_add_test_case opt_rs + atf_add_test_case no_files + atf_add_test_case bad_refer + atf_add_test_case bad_truncate + atf_add_test_case cannot_open + atf_add_test_case new_absolute_grow + atf_add_test_case new_absolute_shrink + atf_add_test_case new_relative_grow + atf_add_test_case new_relative_shrink + atf_add_test_case reference + atf_add_test_case new_zero + atf_add_test_case negative +} Index: tests/base_test.func =================================================================== --- tests/base_test.func (revision 267309) +++ tests/base_test.func (working copy) @@ -57,8 +57,8 @@ _custom_create_file creat _custom_create_file print "${1}" _custom_create_file print \ - "usage: truncate [-c] -s [+|-]size[K|k|M|m|G|g|T|t] file ..." - _custom_create_file print " truncate [-c] -r rfile file ..." + "usage: truncate [-ac] -s [+|-]size[K|k|M|m|G|g|T|t|P|p|E|e] file ..." + _custom_create_file print " truncate [-ac] -r rfile file ..." } atf_test_case illegal_option @@ -72,7 +72,7 @@ create_stderr_usage_file 'truncate: illegal option -- 7' # We expect the error message, with no new files. - atf_check -s not-exit:0 -e file:stderr.txt truncate -7 -s0 output.txt + atf_check -s not-exit:0 -e file:stderr.txt ${TRUNC} -7 -s0 output.txt [ ! -e output.txt ] || atf_fail "output.txt should not exist" } @@ -87,7 +87,7 @@ create_stderr_file "truncate: invalid size argument \`+1L'" # We expect the error message, with no new files. - atf_check -s not-exit:0 -e file:stderr.txt truncate -s+1L output.txt + atf_check -s not-exit:0 -e file:stderr.txt ${TRUNC} -s+1L output.txt [ ! -e output.txt ] || atf_fail "output.txt should not exist" } @@ -103,7 +103,7 @@ # We expect the error message, with no new files. atf_check -s not-exit:0 -e file:stderr.txt \ - truncate -s8388608t output.txt + ${TRUNC} -s8388608t output.txt [ ! -e output.txt ] || atf_fail "output.txt should not exist" } @@ -115,10 +115,10 @@ opt_c_body() { # No new files and truncate returns 0 as if this is a success. - atf_check truncate -c -s 0 doesnotexist.txt + atf_check ${TRUNC} -c -s 0 doesnotexist.txt [ ! -e output.txt ] || atf_fail "doesnotexist.txt should not exist" > reference - atf_check truncate -c -r reference doesnotexist.txt + atf_check ${TRUNC} -c -r reference doesnotexist.txt [ ! -e output.txt ] || atf_fail "doesnotexist.txt should not exist" create_stderr_file @@ -125,7 +125,7 @@ # The existing file will be altered by truncate. > exists.txt - atf_check -e file:stderr.txt truncate -c -s1 exists.txt + atf_check -e file:stderr.txt ${TRUNC} -c -s1 exists.txt [ -s exists.txt ] || atf_fail "exists.txt be larger than zero bytes" } @@ -141,7 +141,7 @@ # Force an error due to the use of both -s and -r. > afile - atf_check -s not-exit:0 -e file:stderr.txt truncate -s0 -r afile afile + atf_check -s not-exit:0 -e file:stderr.txt ${TRUNC} -s0 -r afile afile } atf_test_case no_files @@ -155,7 +155,7 @@ create_stderr_usage_file # A list of files must be present on the command line. - atf_check -s not-exit:0 -e file:stderr.txt truncate -s1 + atf_check -s not-exit:0 -e file:stderr.txt ${TRUNC} -s1 } atf_test_case bad_refer @@ -169,7 +169,7 @@ create_stderr_file "truncate: afile: No such file or directory" # The reference file must exist before you try to use it. - atf_check -s not-exit:0 -e file:stderr.txt truncate -r afile afile + atf_check -s not-exit:0 -e file:stderr.txt ${TRUNC} -r afile afile [ ! -e afile ] || atf_fail "afile should not exist" } @@ -185,9 +185,9 @@ # Trying to get the ftruncate() call to return -1. > exists.txt - atf_check chflags uimmutable exists.txt + chflags schg exists.txt || atf_skip "expected immutable flag support" - atf_check -s not-exit:0 -e file:stderr.txt truncate -s1 exists.txt + atf_check -s not-exit:0 -e file:stderr.txt ${TRUNC} -s1 exists.txt } bad_truncate_cleanup() { @@ -204,7 +204,7 @@ create_stderr_file # Create a new file and grow it to 1024 bytes. - atf_check -s exit:0 -e file:stderr.txt truncate -s1k output.txt + atf_check -s exit:0 -e file:stderr.txt ${TRUNC} -s1k output.txt atf_check -s exit:1 cmp -s output.txt /dev/zero eval $(stat -s output.txt) [ ${st_size} -eq 1024 ] || atf_fail "expected file size of 1k" @@ -212,7 +212,7 @@ create_stderr_file # Grow the existing file to 1M. We are using absolute sizes. - atf_check -s exit:0 -e file:stderr.txt truncate -c -s1M output.txt + atf_check -s exit:0 -e file:stderr.txt ${TRUNC} -c -s1M output.txt atf_check -s exit:1 cmp -s output.txt /dev/zero eval $(stat -s output.txt) [ ${st_size} -eq 1048576 ] || atf_fail "expected file size of 1m" @@ -229,7 +229,7 @@ create_stderr_file # Create a new file and grow it to 1048576 bytes. - atf_check -s exit:0 -e file:stderr.txt truncate -s1M output.txt + atf_check -s exit:0 -e file:stderr.txt ${TRUNC} -s1M output.txt atf_check -s exit:1 cmp -s output.txt /dev/zero eval $(stat -s output.txt) [ ${st_size} -eq 1048576 ] || atf_fail "expected file size of 1m" @@ -237,7 +237,7 @@ create_stderr_file # Shrink the existing file to 1k. We are using absolute sizes. - atf_check -s exit:0 -e file:stderr.txt truncate -s1k output.txt + atf_check -s exit:0 -e file:stderr.txt ${TRUNC} -s1k output.txt atf_check -s exit:1 cmp -s output.txt /dev/zero eval $(stat -s output.txt) [ ${st_size} -eq 1024 ] || atf_fail "expected file size of 1k" @@ -254,7 +254,7 @@ create_stderr_file # Create a new file and grow it to 1024 bytes. - atf_check -s exit:0 -e file:stderr.txt truncate -s+1k output.txt + atf_check -s exit:0 -e file:stderr.txt ${TRUNC} -s+1k output.txt atf_check -s exit:1 cmp -s output.txt /dev/zero eval $(stat -s output.txt) [ ${st_size} -eq 1024 ] || atf_fail "expected file size of 1k" @@ -262,7 +262,7 @@ create_stderr_file # Grow the existing file to 1M. We are using relative sizes. - atf_check -s exit:0 -e file:stderr.txt truncate -s+1047552 output.txt + atf_check -s exit:0 -e file:stderr.txt ${TRUNC} -s+1047552 output.txt atf_check -s exit:1 cmp -s output.txt /dev/zero eval $(stat -s output.txt) [ ${st_size} -eq 1048576 ] || atf_fail "expected file size of 1m" @@ -279,7 +279,7 @@ create_stderr_file # Create a new file and grow it to 1049600 bytes. - atf_check -s exit:0 -e file:stderr.txt truncate -s+1049600 output.txt + atf_check -s exit:0 -e file:stderr.txt ${TRUNC} -s+1049600 output.txt atf_check -s exit:1 cmp -s output.txt /dev/zero eval $(stat -s output.txt) [ ${st_size} -eq 1049600 ] || atf_fail "expected file size of 1m" @@ -287,7 +287,7 @@ create_stderr_file # Shrink the existing file to 1k. We are using relative sizes. - atf_check -s exit:0 -e file:stderr.txt truncate -s-1M output.txt + atf_check -s exit:0 -e file:stderr.txt ${TRUNC} -s-1M output.txt atf_check -s exit:1 cmp -s output.txt /dev/zero eval $(stat -s output.txt) [ ${st_size} -eq 1024 ] || atf_fail "expected file size of 1k" @@ -312,7 +312,7 @@ # Create a new file and grow it to 1024 bytes. atf_check -s not-exit:0 -e file:stderr.txt \ - truncate -c -s1k before 0000 after + ${TRUNC} -c -s1k before 0000 after eval $(stat -s before) [ ${st_size} -eq 1024 ] || atf_fail "expected file size of 1k" eval $(stat -s after) @@ -336,7 +336,7 @@ create_stderr_file # Create a new file and grow it to 4 bytes. - atf_check -e file:stderr.txt truncate -r reference afile + atf_check -e file:stderr.txt ${TRUNC} -r reference afile eval $(stat -s afile) [ ${st_size} -eq 4 ] || atf_fail "new file should also be 4 bytes" } @@ -351,12 +351,12 @@ create_stderr_file # Create a new file and grow it to zero bytes. - atf_check -s exit:0 -e file:stderr.txt truncate -s0 output.txt + atf_check -s exit:0 -e file:stderr.txt ${TRUNC} -s0 output.txt eval $(stat -s output.txt) [ ${st_size} -eq 0 ] || atf_fail "expected file size of zero" # Pretend to grow the file. - atf_check -s exit:0 -e file:stderr.txt truncate -s+0 output.txt + atf_check -s exit:0 -e file:stderr.txt ${TRUNC} -s+0 output.txt eval $(stat -s output.txt) [ ${st_size} -eq 0 ] || atf_fail "expected file size of zero" } @@ -376,27 +376,7 @@ create_stderr_file # Create a new file and do a 100 byte negative relative shrink. - atf_check -e file:stderr.txt truncate -s-100 afile + atf_check -e file:stderr.txt ${TRUNC} -s-100 afile eval $(stat -s afile) [ ${st_size} -eq 0 ] || atf_fail "new file should now be zero bytes" } - -atf_init_test_cases() -{ - atf_add_test_case illegal_option - atf_add_test_case illegal_size - atf_add_test_case too_large_size - atf_add_test_case opt_c - atf_add_test_case opt_rs - atf_add_test_case no_files - atf_add_test_case bad_refer - atf_add_test_case bad_truncate - atf_add_test_case cannot_open - atf_add_test_case new_absolute_grow - atf_add_test_case new_absolute_shrink - atf_add_test_case new_relative_grow - atf_add_test_case new_relative_shrink - atf_add_test_case reference - atf_add_test_case new_zero - atf_add_test_case negative -} Index: tests/sparse_test.c =================================================================== --- tests/sparse_test.c (revision 0) +++ tests/sparse_test.c (working copy) @@ -0,0 +1,198 @@ +/*- + * Copyright 2014, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +__FBSDID("$FreeBSD$"); + +#include +#include + +#include +#include +#include +#include +#include + +#include + +/* Create child process and block for successful termination. */ +void +run_truncate(const char *const *argv) +{ + int status; + pid_t child; + extern char **environ; + + ATF_REQUIRE(posix_spawnp(&child, argv[0], NULL, NULL, + (char *const *)argv, environ) == 0); + ATF_REQUIRE(waitpid(child, &status, 0) == child); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE(WEXITSTATUS(status) == 0); +} + +/* Return the number of bytes used for holes or data in this file. */ +off_t +get_size(const char *path, int whence) +{ + int fd, opposite; + off_t hstart, hend, total; + + assert(whence == SEEK_HOLE || whence == SEEK_DATA); + + /* We expect the file to exist and to have SEEK_HOLE support. */ + ATF_REQUIRE((fd = open(path, O_RDONLY)) != -1); + if (fpathconf(fd, _PC_MIN_HOLE_SIZE) <= 0) + atf_tc_skip("Require _PC_MIN_HOLE_SIZE support."); + + hstart = hend = total = 0; + opposite = (whence == SEEK_DATA) ? SEEK_HOLE : SEEK_DATA; + for (;;) { + if ((hstart = lseek(fd, hend, whence)) == -1) { + ATF_REQUIRE(errno == ENXIO); + break; + } + if ((hend = lseek(fd, hstart, opposite)) == -1) { + ATF_REQUIRE(errno == ENXIO); + ATF_REQUIRE((hend = lseek(fd, 0, SEEK_END)) != -1); + } + total += hend - hstart; + } + + ATF_REQUIRE(close(fd) == 0); + return (total); +} + +/* Return the number of bytes used for holes in this sparse file. */ +off_t +get_hole_size(const char *path) +{ + + return (get_size(path, SEEK_HOLE)); +} + +/* Return the number of bytes actually allocated in this file. */ +off_t +get_allocated_size(const char *path) +{ + + return (get_size(path, SEEK_DATA)); +} + +ATF_TC_WITHOUT_HEAD(default_absolute_file_is_sparse); +ATF_TC_BODY(default_absolute_file_is_sparse, tc) +{ + off_t hole, data, expected; + const char *filename = "afile"; + const char *const cmd[] = { "truncate", "-s", "5m", filename, NULL }; + + run_truncate(cmd); + + expected = 5242880; + hole = get_hole_size(filename); + data = get_allocated_size(filename); + + if (hole + data != expected) + atf_tc_fail("Expected size of %jd, but got %jd + %jd.", + expected, hole, data); + else if (hole <= 0) + atf_tc_fail("Expected a sparse file, but got %jd of data.", + data); +} + +ATF_TC_WITHOUT_HEAD(default_relative_file_is_sparse); +ATF_TC_BODY(default_relative_file_is_sparse, tc) +{ + off_t hole, data, expected; + const char *const before[] = { "truncate", "-s", "1", "afile", NULL }; + const char *const after[] = { "truncate", "-cs", "+5242879", "afile", + NULL }; + + run_truncate(before); + run_truncate(after); + + expected = 5242880; + hole = get_hole_size(before[3]); + data = get_allocated_size(before[3]); + + if (hole + data != expected) + atf_tc_fail("Expected size of %jd, but got %jd + %jd.", + expected, hole, data); + else if (hole <= 0) + atf_tc_fail("Expected a sparse file, but got %jd of data.", + data); +} + +ATF_TC_WITHOUT_HEAD(allocate_absolute_file); +ATF_TC_BODY(allocate_absolute_file, tc) +{ + off_t hole, data, expected; + const char *filename = "afile"; + const char *const cmd[] = { "truncate", "-as", "5m", filename, NULL }; + + run_truncate(cmd); + + expected = 5242880; + hole = get_hole_size(filename); + data = get_allocated_size(filename); + + if (hole != 0 || data != expected) + atf_tc_fail("Expected size of %jd, but got %jd + %jd.", + expected, hole, data); +} + +ATF_TC_WITHOUT_HEAD(allocate_relative_file); +ATF_TC_BODY(allocate_relative_file, tc) +{ + off_t hole, data, expected; + const char *filename = "afile"; + const char *const before[] = { "truncate", "-s1m", filename, NULL }; + const char *const after[] = { "truncate", "-acs+4m", filename, NULL }; + + run_truncate(before); + run_truncate(after); + + expected = 5242880; + hole = get_hole_size(filename); + data = get_allocated_size(filename); + + if (hole + data != expected) + atf_tc_fail("Expected size of %jd, but got %jd + %jd.", + expected, hole, data); + else if (hole <= 0 || data < 4194304) + atf_tc_fail("got hole=%jd and data=%jd.", hole, data); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, default_absolute_file_is_sparse); + ATF_TP_ADD_TC(tp, default_relative_file_is_sparse); + ATF_TP_ADD_TC(tp, allocate_absolute_file); + ATF_TP_ADD_TC(tp, allocate_relative_file); + return atf_no_error(); +} Index: tests/truncate_test.func =================================================================== --- tests/truncate_test.func (revision 0) +++ tests/truncate_test.func (working copy) @@ -0,0 +1,21 @@ +atf_init_test_cases() +{ + TRUNC="truncate" + + atf_add_test_case illegal_option + atf_add_test_case illegal_size + atf_add_test_case too_large_size + atf_add_test_case opt_c + atf_add_test_case opt_rs + atf_add_test_case no_files + atf_add_test_case bad_refer + atf_add_test_case bad_truncate + atf_add_test_case cannot_open + atf_add_test_case new_absolute_grow + atf_add_test_case new_absolute_shrink + atf_add_test_case new_relative_grow + atf_add_test_case new_relative_shrink + atf_add_test_case reference + atf_add_test_case new_zero + atf_add_test_case negative +} Index: truncate.1 =================================================================== --- truncate.1 (revision 268016) +++ truncate.1 (working copy) @@ -33,6 +33,7 @@ .Nd truncate or extend the length of files .Sh SYNOPSIS .Nm +.Op Fl a .Op Fl c .Bk -words .Fl s Xo @@ -39,12 +40,13 @@ .Sm off .Op Cm + | - .Ar size -.Op Cm K | k | M | m | G | g | T | t +.Op Cm K | k | M | m | G | g | T | t | P | p | E | e .Sm on .Xc .Ek .Ar .Nm +.Op Fl a .Op Fl c .Bk -words .Fl r Ar rfile @@ -57,6 +59,10 @@ .Pp The following options are available: .Bl -tag -width indent +.It Fl a +When increasing the size of a file, +allocate the required disk storage instead of using a +.Qq hole. .It Fl c Do not create files if they do not exist. The @@ -71,7 +77,7 @@ .Sm off .Op Cm + | - .Ar size -.Op Cm K | k | M | m | G | g | T | t +.Op Cm K | k | M | m | G | g | T | t | P | p | E | e .Sm on .Xc If the @@ -119,13 +125,11 @@ .Pp Note that, while truncating a file causes space on disk to be freed, -extending a file does not cause space to be allocated. -To extend a file and actually allocate the space, -it is necessary to explicitly write data to it, -using (for example) the shell's -.Ql >> -redirection syntax, or -.Xr dd 1 . +extending a file does not cause space to be allocated on disk. +To extend a file and actually allocate the disk space, +it is necessary to to specify the +.Fl a +option. .Sh EXIT STATUS .Ex -std If the operation fails for an argument, @@ -132,9 +136,24 @@ .Nm will issue a diagnostic and continue processing the remaining arguments. +.Sh EXAMPLES +The following commands: +.Dl rm -f rz35.dsk +.Dl truncate -s 852m rz35.dsk +creates a sparse file that can be used as an emulated disk image. +Future writes to the sparse file could fail to the a lack of free space +on the base file system storage. +.Pp +The following commands: +.Dl rm -f rz28.dsk +.Dl truncate -a -s 2100m rz28.dsk +creates a file that can be used as an emulated disk image. +The required disk space should have been reserved on the storage media. +We do not expect future writes to fail due to lack of free space on the +base file system storage. .Sh SEE ALSO -.Xr dd 1 , -.Xr touch 1 , +.Xr lseek 2 , +.Xr posix_fallocate 2 , .Xr truncate 2 .Sh STANDARDS The Index: truncate.c =================================================================== --- truncate.c (revision 267309) +++ truncate.c (working copy) @@ -45,6 +45,7 @@ static void usage(void); static int no_create; +static int do_allocate; static int do_relative; static int do_refer; static int got_size; @@ -63,8 +64,11 @@ rsize = tsize = sz = 0; error = 0; rname = NULL; - while ((ch = getopt(argc, argv, "cr:s:")) != -1) + while ((ch = getopt(argc, argv, "acr:s:")) != -1) switch (ch) { + case 'a': + do_allocate = 1; + break; case 'c': no_create = 1; break; @@ -122,12 +126,12 @@ } continue; } + if (fstat(fd, &sb) == -1) { + warn("%s", fname); + error++; + continue; + } if (do_relative) { - if (fstat(fd, &sb) == -1) { - warn("%s", fname); - error++; - continue; - } oflow = sb.st_size + rsize; if (oflow < (sb.st_size + rsize)) { errno = EFBIG; @@ -135,12 +139,19 @@ error++; continue; } - tsize = oflow; + tsize = oflow < 0 ? 0 : oflow; + } else { + rsize = tsize - sb.st_size; } - if (tsize < 0) - tsize = 0; - if (ftruncate(fd, tsize) == -1) { + /* + * Use posix_fallocate() when we need to grow the file with + * the required on-disk storage. In all other cases, use + * ftruncate(). + */ + if (((do_allocate && rsize > 0) ? + posix_fallocate(fd, sb.st_size, rsize) : + ftruncate(fd, tsize)) == -1) { warn("%s", fname); error++; continue; @@ -155,8 +166,8 @@ static void usage(void) { - fprintf(stderr, "%s\n%s\n", - "usage: truncate [-c] -s [+|-]size[K|k|M|m|G|g|T|t] file ...", - " truncate [-c] -r rfile file ..."); + fprintf(stderr, "usage: %s\n %s\n", + "truncate [-ac] -s [+|-]size[K|k|M|m|G|g|T|t|P|p|E|e] file ...", + "truncate [-ac] -r rfile file ..."); exit(EXIT_FAILURE); }