diff options
Diffstat (limited to 'bin/cp/tests')
-rw-r--r-- | bin/cp/tests/Makefile | 4 | ||||
-rwxr-xr-x | bin/cp/tests/cp_test.sh | 710 | ||||
-rw-r--r-- | bin/cp/tests/sparse.c | 73 |
3 files changed, 768 insertions, 19 deletions
diff --git a/bin/cp/tests/Makefile b/bin/cp/tests/Makefile index faad22df713a..3fa9ae8f0685 100644 --- a/bin/cp/tests/Makefile +++ b/bin/cp/tests/Makefile @@ -1,7 +1,7 @@ -# $FreeBSD$ - PACKAGE= tests ATF_TESTS_SH= cp_test +PROGS+= sparse +BINDIR= ${TESTSDIR} .include <bsd.test.mk> diff --git a/bin/cp/tests/cp_test.sh b/bin/cp/tests/cp_test.sh index 7362168d7303..999993bfad67 100755 --- a/bin/cp/tests/cp_test.sh +++ b/bin/cp/tests/cp_test.sh @@ -1,5 +1,5 @@ # -# SPDX-License-Identifier: BSD-2-Clause-FreeBSD +# SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2020 Kyle Evans <kevans@FreeBSD.org> # @@ -24,7 +24,6 @@ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # -# $FreeBSD$ check_size() { @@ -35,6 +34,10 @@ check_size() } atf_test_case basic +basic_head() +{ + atf_set "descr" "Copy a file" +} basic_body() { echo "foo" > bar @@ -44,21 +47,26 @@ basic_body() } atf_test_case basic_symlink +basic_symlink_head() +{ + atf_set "descr" "Copy a symlink to a file" +} basic_symlink_body() { echo "foo" > bar ln -s bar baz atf_check cp baz foo - atf_check test '!' -L foo + atf_check test ! -L foo - atf_check -e inline:"cp: baz and baz are identical (not copied).\n" \ - -s exit:1 cp baz baz - atf_check -e inline:"cp: bar and baz are identical (not copied).\n" \ - -s exit:1 cp baz bar + atf_check cmp foo bar } atf_test_case chrdev +chrdev_head() +{ + atf_set "descr" "Copy a character device" +} chrdev_body() { echo "foo" > bar @@ -72,10 +80,56 @@ chrdev_body() check_size trunc 0 } +atf_test_case hardlink +hardlink_head() +{ + atf_set "descr" "Create a hard link to a file" +} +hardlink_body() +{ + echo "foo" >foo + atf_check cp -l foo bar + atf_check -o inline:"foo\n" cat bar + atf_check_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)" +} + +atf_test_case hardlink_exists +hardlink_exists_head() +{ + atf_set "descr" "Attempt to create a hard link to a file, " \ + "but the destination already exists" +} +hardlink_exists_body() +{ + echo "foo" >foo + echo "bar" >bar + atf_check -s not-exit:0 -e match:exists cp -l foo bar + atf_check -o inline:"bar\n" cat bar + atf_check_not_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)" +} + +atf_test_case hardlink_exists_force +hardlink_exists_force_head() +{ + atf_set "descr" "Force creation of a hard link to a file " \ + "when the destination already exists" +} +hardlink_exists_force_body() +{ + echo "foo" >foo + echo "bar" >bar + atf_check cp -fl foo bar + atf_check -o inline:"foo\n" cat bar + atf_check_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)" +} + atf_test_case matching_srctgt +matching_srctgt_head() +{ + atf_set "descr" "Avoid infinite loop when copying a directory to itself" +} matching_srctgt_body() { - # PR235438: `cp -R foo foo` would previously infinitely recurse and # eventually error out. mkdir foo @@ -85,13 +139,17 @@ matching_srctgt_body() atf_check cp -R foo foo atf_check -o inline:"qux\n" cat foo/foo/bar atf_check -o inline:"qux\n" cat foo/foo/zoo - atf_check -e not-empty -s not-exit:0 stat foo/foo/foo + atf_check test ! -e foo/foo/foo } atf_test_case matching_srctgt_contained +matching_srctgt_contained_head() +{ + atf_set "descr" "Avoid infinite loop when copying a directory " \ + "into an existing subdirectory of itself" +} matching_srctgt_contained_body() { - # Let's do the same thing, except we'll try to recursively copy foo into # one of its subdirectories. mkdir foo @@ -117,9 +175,13 @@ matching_srctgt_contained_body() } atf_test_case matching_srctgt_link +matching_srctgt_link_head() +{ + atf_set "descr" "Avoid infinite loop when recursively copying a " \ + "symlink to a directory into the directory it links to" +} matching_srctgt_link_body() { - mkdir foo echo "qux" > foo/bar cp foo/bar foo/zoo @@ -131,9 +193,13 @@ matching_srctgt_link_body() } atf_test_case matching_srctgt_nonexistent +matching_srctgt_nonexistent_head() +{ + atf_set "descr" "Avoid infinite loop when recursively copying a " \ + "directory into a new subdirectory of itself" +} matching_srctgt_nonexistent_body() { - # We'll copy foo to a nonexistent subdirectory; ideally, we would # skip just the directory and end up with a layout like; # @@ -154,6 +220,86 @@ matching_srctgt_nonexistent_body() atf_check -e not-empty -s not-exit:0 stat foo/dne/foo } +atf_test_case pflag_acls +pflag_acls_head() +{ + atf_set "descr" "Verify that -p preserves access control lists" +} +pflag_acls_body() +{ + mkdir dir + ln -s dir lnk + echo "hello" >dir/file + if ! setfacl -m g:staff:D::allow dir || + ! setfacl -m g:staff:d::allow dir/file ; then + atf_skip "file system does not support ACLs" + fi + atf_check -o match:"group:staff:-+D-+" getfacl dir + atf_check -o match:"group:staff:-+d-+" getfacl dir/file + # file-to-file copy without -p + atf_check cp dir/file dst1 + atf_check -o not-match:"group:staff:-+d-+" getfacl dst1 + # file-to-file copy with -p + atf_check cp -p dir/file dst2 + atf_check -o match:"group:staff:-+d-+" getfacl dst2 + # recursive copy without -p + atf_check cp -r dir dst3 + atf_check -o not-match:"group:staff:-+D-+" getfacl dst3 + atf_check -o not-match:"group:staff:-+d-+" getfacl dst3/file + # recursive copy with -p + atf_check cp -rp dir dst4 + atf_check -o match:"group:staff:-+D-+" getfacl dst4 + atf_check -o match:"group:staff:-+d-+" getfacl dst4/file + # source is a link without -p + atf_check cp -r lnk dst5 + atf_check -o not-match:"group:staff:-+D-+" getfacl dst5 + atf_check -o not-match:"group:staff:-+d-+" getfacl dst5/file + # source is a link with -p + atf_check cp -rp lnk dst6 + atf_check -o match:"group:staff:-+D-+" getfacl dst6 + atf_check -o match:"group:staff:-+d-+" getfacl dst6/file +} + +atf_test_case pflag_flags +pflag_flags_head() +{ + atf_set "descr" "Verify that -p preserves file flags" +} +pflag_flags_body() +{ + mkdir dir + ln -s dir lnk + echo "hello" >dir/file + if ! chflags nodump dir || + ! chflags nodump dir/file ; then + atf_skip "file system does not support flags" + fi + atf_check -o match:"nodump" stat -f%Sf dir + atf_check -o match:"nodump" stat -f%Sf dir/file + # file-to-file copy without -p + atf_check cp dir/file dst1 + atf_check -o not-match:"nodump" stat -f%Sf dst1 + # file-to-file copy with -p + atf_check cp -p dir/file dst2 + atf_check -o match:"nodump" stat -f%Sf dst2 + # recursive copy without -p + atf_check cp -r dir dst3 + atf_check -o not-match:"nodump" stat -f%Sf dst3 + atf_check -o not-match:"nodump" stat -f%Sf dst3/file + # recursive copy with -p + atf_check cp -rp dir dst4 + atf_check -o match:"nodump" stat -f%Sf dst4 + atf_check -o match:"nodump" stat -f%Sf dst4/file + # source is a link without -p + atf_check cp -r lnk dst5 + atf_check -o not-match:"nodump" stat -f%Sf dst5 + atf_check -o not-match:"nodump" stat -f%Sf dst5/file + # source is a link with -p + atf_check cp -rp lnk dst6 + atf_check -o match:"nodump" stat -f%Sf dst6 + atf_check -o match:"nodump" stat -f%Sf dst6/file +} + recursive_link_setup() { extra_cpflag=$1 @@ -166,6 +312,11 @@ recursive_link_setup() } atf_test_case recursive_link_dflt +recursive_link_dflt_head() +{ + atf_set "descr" "Copy a directory containing a subdirectory and a " \ + "symlink to that subdirectory" +} recursive_link_dflt_body() { recursive_link_setup @@ -173,9 +324,15 @@ recursive_link_dflt_body() # -P is the default, so this should work and preserve the link. atf_check cp -R foo foo-mirror atf_check test -L foo-mirror/foo/baz + atf_check test -d foo-mirror/foo/baz } atf_test_case recursive_link_Hflag +recursive_link_Hflag_head() +{ + atf_set "descr" "Copy a directory containing a subdirectory and a " \ + "symlink to that subdirectory" +} recursive_link_Hflag_body() { recursive_link_setup @@ -184,29 +341,519 @@ recursive_link_Hflag_body() # link. atf_check cp -RH foo foo-mirror atf_check test -L foo-mirror/foo/baz + atf_check test -d foo-mirror/foo/baz } atf_test_case recursive_link_Lflag +recursive_link_Lflag_head() +{ + atf_set "descr" "Copy a directory containing a subdirectory and a " \ + "symlink to that subdirectory" +} recursive_link_Lflag_body() { recursive_link_setup -L # -L will work, but foo/baz ends up expanded to a directory. - atf_check test -d foo-mirror/foo/baz -a \ - '(' ! -L foo-mirror/foo/baz ')' + atf_check test ! -L foo-mirror/foo/baz + atf_check test -d foo-mirror/foo/baz atf_check cp -RL foo foo-mirror - atf_check test -d foo-mirror/foo/baz -a \ - '(' ! -L foo-mirror/foo/baz ')' + atf_check test ! -L foo-mirror/foo/baz + atf_check test -d foo-mirror/foo/baz +} + +atf_test_case samefile +samefile_head() +{ + atf_set "descr" "Copy a file to itself" +} +samefile_body() +{ + echo "foo" >foo + ln foo bar + ln -s bar baz + atf_check -e match:"baz and baz are identical" \ + -s exit:1 cp baz baz + atf_check -e match:"bar and baz are identical" \ + -s exit:1 cp baz bar + atf_check -e match:"foo and baz are identical" \ + -s exit:1 cp baz foo + atf_check -e match:"bar and foo are identical" \ + -s exit:1 cp foo bar +} + +file_is_sparse() +{ + atf_check ${0%/*}/sparse "$1" +} + +files_are_equal() +{ + atf_check_not_equal "$(stat -f%d,%i "$1")" "$(stat -f%d,%i "$2")" + atf_check cmp "$1" "$2" +} + +atf_test_case sparse_leading_hole +sparse_leading_hole_head() +{ + atf_set "descr" "Copy a sparse file stat starts with a hole" +} +sparse_leading_hole_body() +{ + # A 16-megabyte hole followed by one megabyte of data + truncate -s 16M foo + seq -f%015g 65536 >>foo + file_is_sparse foo + + atf_check cp foo bar + files_are_equal foo bar + file_is_sparse bar +} + +atf_test_case sparse_multiple_holes +sparse_multiple_hole_head() +{ + atf_set "descr" "Copy a sparse file with multiple holes" +} +sparse_multiple_holes_body() +{ + # Three one-megabyte blocks of data preceded, separated, and + # followed by 16-megabyte holes + truncate -s 16M foo + seq -f%015g 65536 >>foo + truncate -s 33M foo + seq -f%015g 65536 >>foo + truncate -s 50M foo + seq -f%015g 65536 >>foo + truncate -s 67M foo + file_is_sparse foo + + atf_check cp foo bar + files_are_equal foo bar + file_is_sparse bar +} + +atf_test_case sparse_only_hole +sparse_only_hole_head() +{ + atf_set "descr" "Copy a sparse file consisting entirely of a hole" +} +sparse_only_hole_body() +{ + # A 16-megabyte hole + truncate -s 16M foo + file_is_sparse foo + + atf_check cp foo bar + files_are_equal foo bar + file_is_sparse bar +} + +atf_test_case sparse_to_dev +sparse_to_dev_head() +{ + atf_set "descr" "Copy a sparse file to a device" +} +sparse_to_dev_body() +{ + # Three one-megabyte blocks of data preceded, separated, and + # followed by 16-megabyte holes + truncate -s 16M foo + seq -f%015g 65536 >>foo + truncate -s 33M foo + seq -f%015g 65536 >>foo + truncate -s 50M foo + seq -f%015g 65536 >>foo + truncate -s 67M foo + file_is_sparse foo + + atf_check -o file:foo cp foo /dev/stdout +} + +atf_test_case sparse_trailing_hole +sparse_trailing_hole_head() +{ + atf_set "descr" "Copy a sparse file that ends with a hole" +} +sparse_trailing_hole_body() +{ + # One megabyte of data followed by a 16-megabyte hole + seq -f%015g 65536 >foo + truncate -s 17M foo + file_is_sparse foo + + atf_check cp foo bar + files_are_equal foo bar + file_is_sparse bar } atf_test_case standalone_Pflag +standalone_Pflag_head() +{ + atf_set "descr" "Test -P without -R" +} standalone_Pflag_body() { echo "foo" > bar ln -s bar foo atf_check cp -P foo baz - atf_check -o inline:'Symbolic Link\n' stat -f %SHT baz + atf_check test -L baz +} + +atf_test_case symlink +symlink_head() +{ + atf_set "descr" "Create a symbolic link to a file" +} +symlink_body() +{ + echo "foo" >foo + atf_check cp -s foo bar + atf_check -o inline:"foo\n" cat bar + atf_check -o inline:"foo\n" readlink bar +} + +atf_test_case symlink_exists +symlink_exists_head() +{ + atf_set "descr" "Attempt to create a symbolic link to a file, " \ + "but the destination already exists" +} +symlink_exists_body() +{ + echo "foo" >foo + echo "bar" >bar + atf_check -s not-exit:0 -e match:exists cp -s foo bar + atf_check -o inline:"bar\n" cat bar +} + +atf_test_case symlink_exists_force +symlink_exists_force_head() +{ + atf_set "descr" "Force creation of a symbolic link to a file " \ + "when the destination already exists" +} +symlink_exists_force_body() +{ + echo "foo" >foo + echo "bar" >bar + atf_check cp -fs foo bar + atf_check -o inline:"foo\n" cat bar + atf_check -o inline:"foo\n" readlink bar +} + +atf_test_case directory_to_symlink +directory_to_symlink_head() +{ + atf_set "descr" "Attempt to copy a directory to a symlink" +} +directory_to_symlink_body() +{ + mkdir -p foo + ln -s .. foo/bar + mkdir bar + touch bar/baz + atf_check -s not-exit:0 -e match:"Not a directory" \ + cp -R bar foo + atf_check -s not-exit:0 -e match:"Not a directory" \ + cp -r bar foo +} + +atf_test_case overwrite_directory +overwrite_directory_head() +{ + atf_set "descr" "Attempt to overwrite a directory with a file" +} +overwrite_directory_body() +{ + mkdir -p foo/bar/baz + touch bar + atf_check -s not-exit:0 -e match:"Is a directory" \ + cp bar foo + rm bar + mkdir bar + touch bar/baz + atf_check -s not-exit:0 -e match:"Is a directory" \ + cp -R bar foo + atf_check -s not-exit:0 -e match:"Is a directory" \ + cp -r bar foo +} + +atf_test_case to_dir_dne +to_dir_dne_head() +{ + atf_set "descr" "Copy a directory to a nonexistent directory" +} +to_dir_dne_body() +{ + mkdir dir + echo "foo" >dir/foo + atf_check cp -r dir dne + atf_check test -d dne + atf_check test -f dne/foo + atf_check cmp dir/foo dne/foo +} + +atf_test_case to_nondir +to_dir_dne_head() +{ + atf_set "descr" "Copy one or more files to a non-directory" +} +to_nondir_body() +{ + echo "foo" >foo + echo "bar" >bar + echo "baz" >baz + # This is described as “case 1” in source code comments + atf_check cp foo bar + atf_check cmp -s foo bar + # This is “case 2”, the target must be a directory + atf_check -s not-exit:0 -e match:"Not a directory" \ + cp foo bar baz +} + +atf_test_case to_deadlink +to_deadlink_head() +{ + atf_set "descr" "Copy a file to a dead symbolic link" +} +to_deadlink_body() +{ + echo "foo" >foo + ln -s bar baz + atf_check cp foo baz + atf_check cmp -s foo bar +} + +atf_test_case to_deadlink_append +to_deadlink_append_head() +{ + atf_set "descr" "Copy multiple files to a dead symbolic link" +} +to_deadlink_append_body() +{ + echo "foo" >foo + mkdir bar + ln -s baz bar/foo + atf_check cp foo bar + atf_check cmp -s foo bar/baz + rm -f bar/foo bar/baz + ln -s baz bar/foo + atf_check cp foo bar/ + atf_check cmp -s foo bar/baz + rm -f bar/foo bar/baz + ln -s $PWD/baz bar/foo + atf_check cp foo bar/ + atf_check cmp -s foo baz +} + +atf_test_case to_dirlink +to_dirlink_head() +{ + atf_set "descr" "Copy things to a symbolic link to a directory" +} +to_dirlink_body() +{ + mkdir src dir + echo "foo" >src/file + ln -s dir dst + atf_check cp -r src dst + atf_check cmp -s src/file dir/src/file + rm -r dir/* + atf_check cp -r src dst/ + atf_check cmp -s src/file dir/src/file + rm -r dir/* + # If the source is a directory and ends in a slash, our cp has + # traditionally copied the contents of the source rather than + # the source itself. It is unclear whether this is intended + # or simply a consequence of how FTS handles the situation. + # Notably, GNU cp does not behave in this manner. + atf_check cp -r src/ dst + atf_check cmp -s src/file dir/file + rm -r dir/* + atf_check cp -r src/ dst/ + atf_check cmp -s src/file dir/file + rm -r dir/* +} + +atf_test_case to_deaddirlink +to_deaddirlink_head() +{ + atf_set "descr" "Copy things to a symbolic link to a nonexistent " \ + "directory" +} +to_deaddirlink_body() +{ + mkdir src + echo "foo" >src/file + ln -s dir dst + # It is unclear which error we should expect in these cases. + # Our current implementation always reports ENOTDIR, but one + # might be equally justified in expecting EEXIST or ENOENT. + # GNU cp reports EEXIST when the destination is given with a + # trailing slash and “cannot overwrite non-directory with + # directory” otherwise. + atf_check -s not-exit:0 -e ignore \ + cp -r src dst + atf_check -s not-exit:0 -e ignore \ + cp -r src dst/ + atf_check -s not-exit:0 -e ignore \ + cp -r src/ dst + atf_check -s not-exit:0 -e ignore \ + cp -r src/ dst/ + atf_check -s not-exit:0 -e ignore \ + cp -R src dst + atf_check -s not-exit:0 -e ignore \ + cp -R src dst/ + atf_check -s not-exit:0 -e ignore \ + cp -R src/ dst + atf_check -s not-exit:0 -e ignore \ + cp -R src/ dst/ +} + +atf_test_case to_link_outside +to_link_outside_head() +{ + atf_set "descr" "Recursively copy a directory containing a symbolic " \ + "link that points to somewhere outside the source directory" +} +to_link_outside_body() +{ + mkdir dir dst dst/dir + echo "foo" >dir/file + ln -s ../../file dst/dir/file + atf_check \ + -s exit:1 \ + -e match:"dst/dir/file: Permission denied" \ + cp -r dir dst +} + +atf_test_case dstmode +dstmode_head() +{ + atf_set "descr" "Verify that directories are created with the " \ + "correct permissions" +} +dstmode_body() +{ + mkdir -m 0755 dir + echo "foo" >dir/file + umask 0177 + atf_check cp -R dir dst + umask 022 + atf_check -o inline:"40600\n" stat -f%p dst + atf_check chmod 0750 dst + atf_check cmp dir/file dst/file +} + +atf_test_case to_root cleanup +to_root_head() +{ + atf_set "require.user" "unprivileged" +} +to_root_body() +{ + dst="test.$(atf_get ident).$$" + echo "$dst" >dst + echo "foo" >"$dst" + atf_check -s not-exit:0 \ + -e match:"^cp: /$dst: (Permission|Read-only)" \ + cp "$dst" / + atf_check -s not-exit:0 \ + -e match:"^cp: /$dst: (Permission|Read-only)" \ + cp "$dst" // +} +to_root_cleanup() +{ + (dst=$(cat dst) && rm "/$dst") 2>/dev/null || true +} + +atf_test_case dirloop +dirloop_head() +{ + atf_set "descr" "Test cycle detection when recursing" +} +dirloop_body() +{ + mkdir -p src/a src/b + ln -s ../b src/a + ln -s ../a src/b + atf_check \ + -s exit:1 \ + -e match:"src/a/b/a: directory causes a cycle" \ + -e match:"src/b/a/b: directory causes a cycle" \ + cp -r src dst + atf_check test -d dst + atf_check test -d dst/a + atf_check test -d dst/b + atf_check test -d dst/a/b + atf_check test ! -e dst/a/b/a + atf_check test -d dst/b/a + atf_check test ! -e dst/b/a/b +} + +atf_test_case unrdir +unrdir_head() +{ + atf_set "descr" "Test handling of unreadable directories" + atf_set "require.user" "unprivileged" +} +unrdir_body() +{ + for d in a b c ; do + mkdir -p src/$d + echo "$d" >src/$d/f + done + chmod 0 src/b + atf_check \ + -s exit:1 \ + -e match:"^cp: src/b: Permission denied" \ + cp -R --sort src dst + atf_check test -d dst/a + atf_check cmp src/a/f dst/a/f + atf_check test -d dst/b + atf_check test ! -e dst/b/f + atf_check test -d dst/c + atf_check cmp src/c/f dst/c/f +} + +atf_test_case unrfile +unrfile_head() +{ + atf_set "descr" "Test handling of unreadable files" + atf_set "require.user" "unprivileged" +} +unrfile_body() +{ + mkdir src + for d in a b c ; do + echo "$d" >src/$d + done + chmod 0 src/b + atf_check \ + -s exit:1 \ + -e match:"^cp: src/b: Permission denied" \ + cp -R --sort src dst + atf_check test -d dst + atf_check cmp src/a dst/a + atf_check test ! -e dst/b + atf_check cmp src/c dst/c +} + +atf_test_case nopermute +nopermute_head() +{ + atf_set descr "Check that getopt_long does not permute options" +} +nopermute_body() +{ + mkdir src dst + atf_check \ + -s exit:1 \ + -e match:'cp: -p: No such file' \ + cp -R src -p dst + atf_check test -d dst/src } atf_init_test_cases() @@ -214,12 +861,41 @@ atf_init_test_cases() atf_add_test_case basic atf_add_test_case basic_symlink atf_add_test_case chrdev + atf_add_test_case hardlink + atf_add_test_case hardlink_exists + atf_add_test_case hardlink_exists_force atf_add_test_case matching_srctgt atf_add_test_case matching_srctgt_contained atf_add_test_case matching_srctgt_link atf_add_test_case matching_srctgt_nonexistent + atf_add_test_case pflag_acls + atf_add_test_case pflag_flags atf_add_test_case recursive_link_dflt atf_add_test_case recursive_link_Hflag atf_add_test_case recursive_link_Lflag + atf_add_test_case samefile + atf_add_test_case sparse_leading_hole + atf_add_test_case sparse_multiple_holes + atf_add_test_case sparse_only_hole + atf_add_test_case sparse_to_dev + atf_add_test_case sparse_trailing_hole atf_add_test_case standalone_Pflag + atf_add_test_case symlink + atf_add_test_case symlink_exists + atf_add_test_case symlink_exists_force + atf_add_test_case directory_to_symlink + atf_add_test_case overwrite_directory + atf_add_test_case to_dir_dne + atf_add_test_case to_nondir + atf_add_test_case to_deadlink + atf_add_test_case to_deadlink_append + atf_add_test_case to_dirlink + atf_add_test_case to_deaddirlink + atf_add_test_case to_link_outside + atf_add_test_case dstmode + atf_add_test_case to_root + atf_add_test_case dirloop + atf_add_test_case unrdir + atf_add_test_case unrfile + atf_add_test_case nopermute } diff --git a/bin/cp/tests/sparse.c b/bin/cp/tests/sparse.c new file mode 100644 index 000000000000..78957581a56c --- /dev/null +++ b/bin/cp/tests/sparse.c @@ -0,0 +1,73 @@ +/*- + * Copyright (c) 2023 Klara, Inc. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <err.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <sysexits.h> +#include <unistd.h> + +static bool verbose; + +/* + * Returns true if the file named by its argument is sparse, i.e. if + * seeking to SEEK_HOLE returns a different value than seeking to + * SEEK_END. + */ +static bool +sparse(const char *filename) +{ + off_t hole, end; + int fd; + + if ((fd = open(filename, O_RDONLY)) < 0 || + (hole = lseek(fd, 0, SEEK_HOLE)) < 0 || + (end = lseek(fd, 0, SEEK_END)) < 0) + err(1, "%s", filename); + close(fd); + if (end > hole) { + if (verbose) + printf("%s: hole at %zu\n", filename, (size_t)hole); + return (true); + } + return (false); +} + +static void +usage(void) +{ + + fprintf(stderr, "usage: sparse [-v] file [...]\n"); + exit(EX_USAGE); +} + +int +main(int argc, char *argv[]) +{ + int opt, rv; + + while ((opt = getopt(argc, argv, "v")) != -1) { + switch (opt) { + case 'v': + verbose = true; + break; + default: + usage(); + break; + } + } + argc -= optind; + argv += optind; + if (argc == 0) + usage(); + rv = EXIT_SUCCESS; + while (argc-- > 0) + if (!sparse(*argv++)) + rv = EXIT_FAILURE; + exit(rv); +} |