xref: /freebsd/bin/cp/tests/cp_test.sh (revision c27f7d6b9cf6d4ab01cb3d0972726c14e0aca146)
1#
2# SPDX-License-Identifier: BSD-2-Clause
3#
4# Copyright (c) 2020 Kyle Evans <kevans@FreeBSD.org>
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26#
27
28check_size()
29{
30	file=$1
31	sz=$2
32
33	atf_check -o inline:"$sz\n" stat -f '%z' $file
34}
35
36atf_test_case basic
37basic_body()
38{
39	echo "foo" > bar
40
41	atf_check cp bar baz
42	check_size baz 4
43}
44
45atf_test_case basic_symlink
46basic_symlink_body()
47{
48	echo "foo" > bar
49	ln -s bar baz
50
51	atf_check cp baz foo
52	atf_check test '!' -L foo
53
54	atf_check cmp foo bar
55}
56
57atf_test_case chrdev
58chrdev_body()
59{
60	echo "foo" > bar
61
62	check_size bar 4
63	atf_check cp /dev/null trunc
64	check_size trunc 0
65	atf_check cp bar trunc
66	check_size trunc 4
67	atf_check cp /dev/null trunc
68	check_size trunc 0
69}
70
71atf_test_case hardlink
72hardlink_body()
73{
74	echo "foo" >foo
75	atf_check cp -l foo bar
76	atf_check -o inline:"foo\n" cat bar
77	atf_check_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)"
78}
79
80atf_test_case hardlink_exists
81hardlink_exists_body()
82{
83	echo "foo" >foo
84	echo "bar" >bar
85	atf_check -s not-exit:0 -e match:exists cp -l foo bar
86	atf_check -o inline:"bar\n" cat bar
87	atf_check_not_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)"
88}
89
90atf_test_case hardlink_exists_force
91hardlink_exists_force_body()
92{
93	echo "foo" >foo
94	echo "bar" >bar
95	atf_check cp -fl foo bar
96	atf_check -o inline:"foo\n" cat bar
97	atf_check_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)"
98}
99
100atf_test_case matching_srctgt
101matching_srctgt_body()
102{
103
104	# PR235438: `cp -R foo foo` would previously infinitely recurse and
105	# eventually error out.
106	mkdir foo
107	echo "qux" > foo/bar
108	cp foo/bar foo/zoo
109
110	atf_check cp -R foo foo
111	atf_check -o inline:"qux\n" cat foo/foo/bar
112	atf_check -o inline:"qux\n" cat foo/foo/zoo
113	atf_check -e not-empty -s not-exit:0 stat foo/foo/foo
114}
115
116atf_test_case matching_srctgt_contained
117matching_srctgt_contained_body()
118{
119
120	# Let's do the same thing, except we'll try to recursively copy foo into
121	# one of its subdirectories.
122	mkdir foo
123	ln -s foo coo
124	echo "qux" > foo/bar
125	mkdir foo/moo
126	touch foo/moo/roo
127	cp foo/bar foo/zoo
128
129	atf_check cp -R foo foo/moo
130	atf_check cp -RH coo foo/moo
131	atf_check -o inline:"qux\n" cat foo/moo/foo/bar
132	atf_check -o inline:"qux\n" cat foo/moo/coo/bar
133	atf_check -o inline:"qux\n" cat foo/moo/foo/zoo
134	atf_check -o inline:"qux\n" cat foo/moo/coo/zoo
135
136	# We should have copied the contents of foo/moo before foo, coo started
137	# getting copied in.
138	atf_check -o not-empty stat foo/moo/foo/moo/roo
139	atf_check -o not-empty stat foo/moo/coo/moo/roo
140	atf_check -e not-empty -s not-exit:0 stat foo/moo/foo/moo/foo
141	atf_check -e not-empty -s not-exit:0 stat foo/moo/coo/moo/coo
142}
143
144atf_test_case matching_srctgt_link
145matching_srctgt_link_body()
146{
147
148	mkdir foo
149	echo "qux" > foo/bar
150	cp foo/bar foo/zoo
151
152	atf_check ln -s foo roo
153	atf_check cp -RH roo foo
154	atf_check -o inline:"qux\n" cat foo/roo/bar
155	atf_check -o inline:"qux\n" cat foo/roo/zoo
156}
157
158atf_test_case matching_srctgt_nonexistent
159matching_srctgt_nonexistent_body()
160{
161
162	# We'll copy foo to a nonexistent subdirectory; ideally, we would
163	# skip just the directory and end up with a layout like;
164	#
165	# foo/
166	#     bar
167	#     dne/
168	#         bar
169	#         zoo
170	#     zoo
171	#
172	mkdir foo
173	echo "qux" > foo/bar
174	cp foo/bar foo/zoo
175
176	atf_check cp -R foo foo/dne
177	atf_check -o inline:"qux\n" cat foo/dne/bar
178	atf_check -o inline:"qux\n" cat foo/dne/zoo
179	atf_check -e not-empty -s not-exit:0 stat foo/dne/foo
180}
181
182atf_test_case pflag_acls
183pflag_acls_body()
184{
185	mkdir dir
186	echo "hello" >dir/file
187	if ! setfacl -m g:staff:D::allow dir ||
188	   ! setfacl -m g:staff:d::allow dir/file ; then
189		atf_skip "file system does not support ACLs"
190	fi
191	atf_check cp -p dir/file dst
192	atf_check -o match:"group:staff:-+d-+" getfacl dst
193	rm dst
194	mkdir dst
195	atf_check cp -rp dir dst
196	atf_check -o not-match:"group:staff:-+D-+" getfacl dst
197	atf_check -o match:"group:staff:-+D-+" getfacl dst/dir
198	atf_check -o match:"group:staff:-+d-+" getfacl dst/dir/file
199	rm -rf dst
200	atf_check cp -rp dir dst
201	atf_check -o match:"group:staff:-+D-+" getfacl dst/
202	atf_check -o match:"group:staff:-+d-+" getfacl dst/file
203}
204
205atf_test_case pflag_flags
206pflag_flags_body()
207{
208	mkdir dir
209	echo "hello" >dir/file
210	if ! chflags nodump dir ||
211	   ! chflags nodump dir/file ; then
212		atf_skip "file system does not support flags"
213	fi
214	atf_check cp -p dir/file dst
215	atf_check -o match:"nodump" stat -f%Sf dst
216	rm dst
217	mkdir dst
218	atf_check cp -rp dir dst
219	atf_check -o not-match:"nodump" stat -f%Sf dst
220	atf_check -o match:"nodump" stat -f%Sf dst/dir
221	atf_check -o match:"nodump" stat -f%Sf dst/dir/file
222	rm -rf dst
223	atf_check cp -rp dir dst
224	atf_check -o match:"nodump" stat -f%Sf dst
225	atf_check -o match:"nodump" stat -f%Sf dst/file
226}
227
228recursive_link_setup()
229{
230	extra_cpflag=$1
231
232	mkdir -p foo/bar
233	ln -s bar foo/baz
234
235	mkdir foo-mirror
236	eval "cp -R $extra_cpflag foo foo-mirror"
237}
238
239atf_test_case recursive_link_dflt
240recursive_link_dflt_body()
241{
242	recursive_link_setup
243
244	# -P is the default, so this should work and preserve the link.
245	atf_check cp -R foo foo-mirror
246	atf_check test -L foo-mirror/foo/baz
247}
248
249atf_test_case recursive_link_Hflag
250recursive_link_Hflag_body()
251{
252	recursive_link_setup
253
254	# -H will not follow either, so this should also work and preserve the
255	# link.
256	atf_check cp -RH foo foo-mirror
257	atf_check test -L foo-mirror/foo/baz
258}
259
260atf_test_case recursive_link_Lflag
261recursive_link_Lflag_body()
262{
263	recursive_link_setup -L
264
265	# -L will work, but foo/baz ends up expanded to a directory.
266	atf_check test -d foo-mirror/foo/baz -a \
267	    '(' ! -L foo-mirror/foo/baz ')'
268	atf_check cp -RL foo foo-mirror
269	atf_check test -d foo-mirror/foo/baz -a \
270	    '(' ! -L foo-mirror/foo/baz ')'
271}
272
273atf_test_case samefile
274samefile_body()
275{
276	echo "foo" >foo
277	ln foo bar
278	ln -s bar baz
279	atf_check -e match:"baz and baz are identical" \
280	    -s exit:1 cp baz baz
281	atf_check -e match:"bar and baz are identical" \
282	    -s exit:1 cp baz bar
283	atf_check -e match:"foo and baz are identical" \
284	    -s exit:1 cp baz foo
285	atf_check -e match:"bar and foo are identical" \
286	    -s exit:1 cp foo bar
287}
288
289file_is_sparse()
290{
291	atf_check ${0%/*}/sparse "$1"
292}
293
294files_are_equal()
295{
296	atf_check_not_equal "$(stat -f%d,%i "$1")" "$(stat -f%d,%i "$2")"
297	atf_check cmp "$1" "$2"
298}
299
300atf_test_case sparse_leading_hole
301sparse_leading_hole_body()
302{
303	# A 16-megabyte hole followed by one megabyte of data
304	truncate -s 16M foo
305	seq -f%015g 65536 >>foo
306	file_is_sparse foo
307
308	atf_check cp foo bar
309	files_are_equal foo bar
310	file_is_sparse bar
311}
312
313atf_test_case sparse_multiple_holes
314sparse_multiple_holes_body()
315{
316	# Three one-megabyte blocks of data preceded, separated, and
317	# followed by 16-megabyte holes
318	truncate -s 16M foo
319	seq -f%015g 65536 >>foo
320	truncate -s 33M foo
321	seq -f%015g 65536 >>foo
322	truncate -s 50M foo
323	seq -f%015g 65536 >>foo
324	truncate -s 67M foo
325	file_is_sparse foo
326
327	atf_check cp foo bar
328	files_are_equal foo bar
329	file_is_sparse bar
330}
331
332atf_test_case sparse_only_hole
333sparse_only_hole_body()
334{
335	# A 16-megabyte hole
336	truncate -s 16M foo
337	file_is_sparse foo
338
339	atf_check cp foo bar
340	files_are_equal foo bar
341	file_is_sparse bar
342}
343
344atf_test_case sparse_to_dev
345sparse_to_dev_body()
346{
347	# Three one-megabyte blocks of data preceded, separated, and
348	# followed by 16-megabyte holes
349	truncate -s 16M foo
350	seq -f%015g 65536 >>foo
351	truncate -s 33M foo
352	seq -f%015g 65536 >>foo
353	truncate -s 50M foo
354	seq -f%015g 65536 >>foo
355	truncate -s 67M foo
356	file_is_sparse foo
357
358	atf_check -o file:foo cp foo /dev/stdout
359}
360
361atf_test_case sparse_trailing_hole
362sparse_trailing_hole_body()
363{
364	# One megabyte of data followed by a 16-megabyte hole
365	seq -f%015g 65536 >foo
366	truncate -s 17M foo
367	file_is_sparse foo
368
369	atf_check cp foo bar
370	files_are_equal foo bar
371	file_is_sparse bar
372}
373
374atf_test_case standalone_Pflag
375standalone_Pflag_body()
376{
377	echo "foo" > bar
378	ln -s bar foo
379
380	atf_check cp -P foo baz
381	atf_check -o inline:'Symbolic Link\n' stat -f %SHT baz
382}
383
384atf_test_case symlink
385symlink_body()
386{
387	echo "foo" >foo
388	atf_check cp -s foo bar
389	atf_check -o inline:"foo\n" cat bar
390	atf_check -o inline:"foo\n" readlink bar
391}
392
393atf_test_case symlink_exists
394symlink_exists_body()
395{
396	echo "foo" >foo
397	echo "bar" >bar
398	atf_check -s not-exit:0 -e match:exists cp -s foo bar
399	atf_check -o inline:"bar\n" cat bar
400}
401
402atf_test_case symlink_exists_force
403symlink_exists_force_body()
404{
405	echo "foo" >foo
406	echo "bar" >bar
407	atf_check cp -fs foo bar
408	atf_check -o inline:"foo\n" cat bar
409	atf_check -o inline:"foo\n" readlink bar
410}
411
412atf_test_case directory_to_symlink
413directory_to_symlink_body()
414{
415	mkdir -p foo
416	ln -s .. foo/bar
417	mkdir bar
418	touch bar/baz
419	atf_check -s not-exit:0 -e match:"Not a directory" \
420	    cp -R bar foo
421	atf_check -s not-exit:0 -e match:"Not a directory" \
422	    cp -r bar foo
423}
424
425atf_test_case overwrite_directory
426overwrite_directory_body()
427{
428	mkdir -p foo/bar/baz
429	touch bar
430	atf_check -s not-exit:0 -e match:"Is a directory" \
431	    cp bar foo
432	rm bar
433	mkdir bar
434	touch bar/baz
435	atf_check -s not-exit:0 -e match:"Is a directory" \
436	    cp -R bar foo
437	atf_check -s not-exit:0 -e match:"Is a directory" \
438	    cp -r bar foo
439}
440
441atf_test_case to_dir_dne
442to_dir_dne_body()
443{
444	mkdir dir
445	echo "foo" >dir/foo
446	atf_check cp -r dir dne
447	atf_check test -d dne
448	atf_check test -f dne/foo
449	atf_check cmp dir/foo dne/foo
450}
451
452atf_test_case to_nondir
453to_nondir_body()
454{
455	echo "foo" >foo
456	echo "bar" >bar
457	echo "baz" >baz
458	# This is described as “case 1” in source code comments
459	atf_check cp foo bar
460	atf_check cmp -s foo bar
461	# This is “case 2”, the target must be a directory
462	atf_check -s not-exit:0 -e match:"Not a directory" \
463	    cp foo bar baz
464}
465
466atf_test_case to_deadlink
467to_deadlink_body()
468{
469	echo "foo" >foo
470	ln -s bar baz
471	atf_check cp foo baz
472	atf_check cmp -s foo bar
473}
474
475atf_test_case to_deadlink_append
476to_deadlink_append_body()
477{
478	echo "foo" >foo
479	mkdir bar
480	ln -s baz bar/foo
481	atf_check cp foo bar
482	atf_check cmp -s foo bar/baz
483	rm -f bar/foo bar/baz
484	ln -s baz bar/foo
485	atf_check cp foo bar/
486	atf_check cmp -s foo bar/baz
487	rm -f bar/foo bar/baz
488	ln -s $PWD/baz bar/foo
489	atf_check cp foo bar/
490	atf_check cmp -s foo baz
491}
492
493atf_test_case to_dirlink
494to_dirlink_body()
495{
496	mkdir src dir
497	echo "foo" >src/file
498	ln -s dir dst
499	atf_check cp -r src dst
500	atf_check cmp -s src/file dir/src/file
501	rm -r dir/*
502	atf_check cp -r src dst/
503	atf_check cmp -s src/file dir/src/file
504	rm -r dir/*
505	# If the source is a directory and ends in a slash, our cp has
506	# traditionally copied the contents of the source rather than
507	# the source itself.  It is unclear whether this is intended
508	# or simply a consequence of how FTS handles the situation.
509	# Notably, GNU cp does not behave in this manner.
510	atf_check cp -r src/ dst
511	atf_check cmp -s src/file dir/file
512	rm -r dir/*
513	atf_check cp -r src/ dst/
514	atf_check cmp -s src/file dir/file
515	rm -r dir/*
516}
517
518atf_test_case to_deaddirlink
519to_deaddirlink_body()
520{
521	mkdir src
522	echo "foo" >src/file
523	ln -s dir dst
524	# It is unclear which error we should expect in these cases.
525	# Our current implementation always reports ENOTDIR, but one
526	# might be equally justified in expecting EEXIST or ENOENT.
527	# GNU cp reports EEXIST when the destination is given with a
528	# trailing slash and “cannot overwrite non-directory with
529	# directory” otherwise.
530	atf_check -s not-exit:0 -e ignore \
531	    cp -r src dst
532	atf_check -s not-exit:0 -e ignore \
533	    cp -r src dst/
534	atf_check -s not-exit:0 -e ignore \
535	    cp -r src/ dst
536	atf_check -s not-exit:0 -e ignore \
537	    cp -r src/ dst/
538	atf_check -s not-exit:0 -e ignore \
539	    cp -R src dst
540	atf_check -s not-exit:0 -e ignore \
541	    cp -R src dst/
542	atf_check -s not-exit:0 -e ignore \
543	    cp -R src/ dst
544	atf_check -s not-exit:0 -e ignore \
545	    cp -R src/ dst/
546}
547
548atf_test_case to_link_outside
549to_link_outside_body()
550{
551	mkdir dir dst dst/dir
552	echo "foo" >dir/file
553	ln -s ../../file dst/dir/file
554	atf_check \
555	    -s exit:1 \
556	    -e match:"dst/dir/file: Permission denied" \
557	    cp -r dir dst
558}
559
560atf_test_case dstmode
561dstmode_body()
562{
563	mkdir -m 0755 dir
564	echo "foo" >dir/file
565	umask 0177
566	atf_check cp -R dir dst
567	umask 022
568	atf_check -o inline:"40600\n" stat -f%p dst
569	atf_check chmod 0750 dst
570	atf_check cmp dir/file dst/file
571}
572
573atf_test_case to_root cleanup
574to_root_head()
575{
576	atf_set "require.user" "unprivileged"
577}
578to_root_body()
579{
580	dst="test.$(atf_get ident).$$"
581	echo "$dst" >dst
582	echo "foo" >"$dst"
583	atf_check -s not-exit:0 \
584	    -e match:"^cp: /$dst: (Permission|Read-only)" \
585	    cp "$dst" /
586	atf_check -s not-exit:0 \
587	    -e match:"^cp: /$dst: (Permission|Read-only)" \
588	    cp "$dst" //
589}
590to_root_cleanup()
591{
592	(dst=$(cat dst) && rm "/$dst") 2>/dev/null || true
593}
594
595atf_init_test_cases()
596{
597	atf_add_test_case basic
598	atf_add_test_case basic_symlink
599	atf_add_test_case chrdev
600	atf_add_test_case hardlink
601	atf_add_test_case hardlink_exists
602	atf_add_test_case hardlink_exists_force
603	atf_add_test_case matching_srctgt
604	atf_add_test_case matching_srctgt_contained
605	atf_add_test_case matching_srctgt_link
606	atf_add_test_case matching_srctgt_nonexistent
607	atf_add_test_case pflag_acls
608	atf_add_test_case pflag_flags
609	atf_add_test_case recursive_link_dflt
610	atf_add_test_case recursive_link_Hflag
611	atf_add_test_case recursive_link_Lflag
612	atf_add_test_case samefile
613	atf_add_test_case sparse_leading_hole
614	atf_add_test_case sparse_multiple_holes
615	atf_add_test_case sparse_only_hole
616	atf_add_test_case sparse_to_dev
617	atf_add_test_case sparse_trailing_hole
618	atf_add_test_case standalone_Pflag
619	atf_add_test_case symlink
620	atf_add_test_case symlink_exists
621	atf_add_test_case symlink_exists_force
622	atf_add_test_case directory_to_symlink
623	atf_add_test_case overwrite_directory
624	atf_add_test_case to_dir_dne
625	atf_add_test_case to_nondir
626	atf_add_test_case to_deadlink
627	atf_add_test_case to_deadlink_append
628	atf_add_test_case to_dirlink
629	atf_add_test_case to_deaddirlink
630	atf_add_test_case to_link_outside
631	atf_add_test_case dstmode
632	atf_add_test_case to_root
633}
634