From 696cb27313f7b792cb4621775703717af189a5c2 Mon Sep 17 00:00:00 2001 From: SFG545 Date: Mon, 16 Mar 2026 22:06:27 -0500 Subject: [PATCH] Import VX multicall utility and applets --- .gitignore | 3 + AGENTS.md | 75 + LICENSE | 21 + LICENSE-FREEBSD | 125 ++ README.md | 41 + meson.build | 192 ++ src/cmp/cmp.1 | 212 ++ src/cmp/cmp.c | 266 +++ src/cmp/extern.h | 57 + src/cmp/link.c | 103 + src/cmp/misc.c | 68 + src/cmp/regular.c | 195 ++ src/cmp/special.c | 131 ++ src/cmp/tests/Makefile | 14 + src/cmp/tests/Makefile.depend | 10 + src/cmp/tests/b_flag.out | 1 + src/cmp/tests/bl_flag.out | 1 + src/cmp/tests/cmp_test2.sh | 175 ++ src/compat-headers/compat.h | 273 +++ src/compat-headers/expand_number.c | 83 + src/compat-headers/fgetln.c | 46 + src/compat-headers/reallocf.c | 28 + src/compat-headers/setmode.c | 213 ++ src/compat-headers/strlcpy.c | 81 + src/compat-headers/strmode.c | 53 + src/compat-headers/strtonum.c | 67 + src/compat-headers/sys/tree.h | 1069 +++++++++ src/compat-headers/ugid.c | 41 + src/diff/diff.1 | 760 +++++++ src/diff/diff.c | 651 ++++++ src/diff/diff.h | 123 ++ src/diff/diffdir.c | 384 ++++ src/diff/diffreg.c | 1722 +++++++++++++++ src/diff/pr.c | 110 + src/diff/pr.h | 35 + src/diff/tests/Bflag_C.out | 2 + src/diff/tests/Bflag_D.out | 2 + src/diff/tests/Bflag_F.out | 4 + src/diff/tests/Makefile.depend | 10 + src/diff/tests/diff_test.sh | 478 ++++ src/diff/tests/functionname.in | 29 + src/diff/tests/functionname_c.in | 29 + src/diff/tests/functionname_c.out | 11 + src/diff/tests/functionname_objcclassm.in | 31 + src/diff/tests/functionname_objcclassm.out | 11 + src/diff/tests/functionname_objcm.in | 29 + src/diff/tests/functionname_objcm.out | 11 + src/diff/tests/group-format.out | 27 + src/diff/tests/header.out | 4 + src/diff/tests/header_ns.out | 4 + src/diff/tests/ifdef.out | 26 + src/diff/tests/input1.in | 2 + src/diff/tests/input2.in | 3 + src/diff/tests/input_c1.in | 15 + src/diff/tests/input_c2.in | 16 + src/diff/tests/side_by_side_tabbed_a.in | 4 + src/diff/tests/side_by_side_tabbed_b.in | 4 + src/diff/tests/simple.out | 5 + src/diff/tests/simple_b.out | 6 + src/diff/tests/simple_e.out | 4 + src/diff/tests/simple_i.out | 6 + src/diff/tests/simple_n.out | 4 + src/diff/tests/simple_p.out | 34 + src/diff/tests/simple_u.out | 7 + src/diff/tests/simple_w.out | 6 + src/diff/tests/strip_o.out | 7 + src/diff/tests/unified_9999.out | 21 + src/diff/tests/unified_c9999.out | 36 + src/diff/tests/unified_p.out | 20 + src/diff/xmalloc.c | 85 + src/diff/xmalloc.h | 30 + src/diff3/diff3.1 | 223 ++ src/diff3/diff3.c | 977 +++++++++ src/diff3/tests/1.out | 14 + src/diff3/tests/1.txt | 6 + src/diff3/tests/1cr.txt | 6 + src/diff3/tests/1t.out | 14 + src/diff3/tests/2.out | 3 + src/diff3/tests/2.txt | 7 + src/diff3/tests/3.out | 8 + src/diff3/tests/3.txt | 5 + src/diff3/tests/4.out | 8 + src/diff3/tests/5.out | 3 + src/diff3/tests/6.out | 0 src/diff3/tests/7.out | 14 + src/diff3/tests/8.out | 19 + src/diff3/tests/9.out | 17 + src/diff3/tests/Makefile | 27 + src/diff3/tests/Makefile.depend | 10 + src/diff3/tests/diff3_test.sh | 76 + src/diff3/tests/long-A.out | 22 + src/diff3/tests/long-ed.out | 7 + src/diff3/tests/long-m.txt | 26 + src/diff3/tests/long-merge.out | 35 + src/diff3/tests/long-o.txt | 27 + src/diff3/tests/long-y.txt | 26 + src/find/extern.h | 133 ++ src/find/find.1 | 1206 ++++++++++ src/find/find.c | 243 +++ src/find/find.h | 179 ++ src/find/function.c | 2304 +++++++++++++++++++ src/find/getdate.y | 945 ++++++++ src/find/ls.c | 118 + src/find/main.c | 186 ++ src/find/misc.c | 107 + src/find/operator.c | 267 +++ src/find/option.c | 203 ++ src/grep/file.c | 253 +++ src/grep/grep.1 | 594 +++++ src/grep/grep.c | 756 +++++++ src/grep/grep.h | 157 ++ src/grep/queue.c | 139 ++ src/grep/util.c | 850 ++++++++ src/gzip/gzexe | 178 ++ src/gzip/gzexe.1 | 71 + src/gzip/gzip.1 | 246 +++ src/gzip/gzip.c | 2305 ++++++++++++++++++++ src/gzip/unlz.c | 642 ++++++ src/gzip/unpack.c | 334 +++ src/gzip/unxz.c | 473 ++++ src/gzip/zdiff | 141 ++ src/gzip/zdiff.1 | 140 ++ src/gzip/zforce | 54 + src/gzip/zforce.1 | 51 + src/gzip/zgrep | 230 ++ src/gzip/zgrep.1 | 94 + src/gzip/zless | 6 + src/gzip/zmore | 81 + src/gzip/zmore.1 | 108 + src/gzip/znew | 136 ++ src/gzip/znew.1 | 69 + src/gzip/zuncompress.c | 412 ++++ src/main.c | 91 + src/patch/backupfile.c | 244 +++ src/patch/backupfile.h | 38 + src/patch/common.h | 111 + src/patch/inp.c | 437 ++++ src/patch/inp.h | 31 + src/patch/mkpath.c | 79 + src/patch/patch.1 | 691 ++++++ src/patch/patch.c | 1230 +++++++++++ src/patch/pathnames.h | 11 + src/patch/pch.c | 1659 ++++++++++++++ src/patch/pch.h | 57 + src/patch/tests/PR74127-cline.diff | 4 + src/patch/tests/PR74127-good.diff | 4 + src/patch/tests/PR74127-repro.diff | 4 + src/patch/tests/PR74127.in | 14 + src/patch/tests/unified_patch_test.sh | 170 ++ src/patch/util.c | 407 ++++ src/patch/util.h | 50 + src/sdiff/edit.c | 208 ++ src/sdiff/extern.h | 11 + src/sdiff/sdiff.1 | 169 ++ src/sdiff/sdiff.c | 1178 ++++++++++ src/sdiff/tests/Makefile | 34 + src/sdiff/tests/Makefile.depend | 10 + src/sdiff/tests/d_dot.in | 1 + src/sdiff/tests/d_flags_l.out | 102 + src/sdiff/tests/d_flags_s.out | 79 + src/sdiff/tests/d_flags_w.out | 102 + src/sdiff/tests/d_iflags_a1.out | 100 + src/sdiff/tests/d_iflags_a2.out | 96 + src/sdiff/tests/d_iflags_b1.out | 69 + src/sdiff/tests/d_iflags_b2.out | 65 + src/sdiff/tests/d_iflags_c1.out | 99 + src/sdiff/tests/d_iflags_c2.out | 94 + src/sdiff/tests/d_iflags_d1.out | 69 + src/sdiff/tests/d_iflags_d2.out | 65 + src/sdiff/tests/d_input1 | 72 + src/sdiff/tests/d_input2 | 69 + src/sdiff/tests/d_oneline.in | 1 + src/sdiff/tests/d_oneline_a.out | 1 + src/sdiff/tests/d_oneline_b.out | 1 + src/sdiff/tests/d_same.out | 72 + src/sdiff/tests/d_short.out | 15 + src/sdiff/tests/d_tabends.in | 17 + src/sdiff/tests/d_tabends_a.out | 17 + src/sdiff/tests/d_tabends_b.out | 17 + src/sdiff/tests/d_tabends_c.out | 17 + src/sdiff/tests/d_tabs.out | 102 + src/sdiff/tests/d_tabs1.in | 72 + src/sdiff/tests/d_tabs2.in | 69 + src/sdiff/tests/sdiff_test.sh | 242 ++ src/which/which.1 | 125 ++ src/which/which.c | 144 ++ src/xargs/pathnames.h | 34 + src/xargs/strnsubst.c | 111 + src/xargs/xargs.1 | 390 ++++ src/xargs/xargs.c | 814 +++++++ tests/harness.sh | 109 + tests/test_cmp.sh | 60 + tests/test_diff.sh | 101 + tests/test_diff3.sh | 81 + tests/test_find.sh | 60 + tests/test_grep.sh | 171 ++ tests/test_gzip.sh | 105 + tests/test_patch.sh | 62 + tests/test_sdiff.sh | 71 + tests/test_which.sh | 77 + tests/test_xargs.sh | 35 + 201 files changed, 35660 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 LICENSE create mode 100644 LICENSE-FREEBSD create mode 100644 README.md create mode 100644 meson.build create mode 100644 src/cmp/cmp.1 create mode 100644 src/cmp/cmp.c create mode 100644 src/cmp/extern.h create mode 100644 src/cmp/link.c create mode 100644 src/cmp/misc.c create mode 100644 src/cmp/regular.c create mode 100644 src/cmp/special.c create mode 100644 src/cmp/tests/Makefile create mode 100644 src/cmp/tests/Makefile.depend create mode 100644 src/cmp/tests/b_flag.out create mode 100644 src/cmp/tests/bl_flag.out create mode 100755 src/cmp/tests/cmp_test2.sh create mode 100644 src/compat-headers/compat.h create mode 100644 src/compat-headers/expand_number.c create mode 100644 src/compat-headers/fgetln.c create mode 100644 src/compat-headers/reallocf.c create mode 100644 src/compat-headers/setmode.c create mode 100644 src/compat-headers/strlcpy.c create mode 100644 src/compat-headers/strmode.c create mode 100644 src/compat-headers/strtonum.c create mode 100644 src/compat-headers/sys/tree.h create mode 100644 src/compat-headers/ugid.c create mode 100644 src/diff/diff.1 create mode 100644 src/diff/diff.c create mode 100644 src/diff/diff.h create mode 100644 src/diff/diffdir.c create mode 100644 src/diff/diffreg.c create mode 100644 src/diff/pr.c create mode 100644 src/diff/pr.h create mode 100644 src/diff/tests/Bflag_C.out create mode 100644 src/diff/tests/Bflag_D.out create mode 100644 src/diff/tests/Bflag_F.out create mode 100644 src/diff/tests/Makefile.depend create mode 100755 src/diff/tests/diff_test.sh create mode 100644 src/diff/tests/functionname.in create mode 100644 src/diff/tests/functionname_c.in create mode 100644 src/diff/tests/functionname_c.out create mode 100644 src/diff/tests/functionname_objcclassm.in create mode 100644 src/diff/tests/functionname_objcclassm.out create mode 100644 src/diff/tests/functionname_objcm.in create mode 100644 src/diff/tests/functionname_objcm.out create mode 100644 src/diff/tests/group-format.out create mode 100644 src/diff/tests/header.out create mode 100644 src/diff/tests/header_ns.out create mode 100644 src/diff/tests/ifdef.out create mode 100644 src/diff/tests/input1.in create mode 100644 src/diff/tests/input2.in create mode 100644 src/diff/tests/input_c1.in create mode 100644 src/diff/tests/input_c2.in create mode 100644 src/diff/tests/side_by_side_tabbed_a.in create mode 100644 src/diff/tests/side_by_side_tabbed_b.in create mode 100644 src/diff/tests/simple.out create mode 100644 src/diff/tests/simple_b.out create mode 100644 src/diff/tests/simple_e.out create mode 100644 src/diff/tests/simple_i.out create mode 100644 src/diff/tests/simple_n.out create mode 100644 src/diff/tests/simple_p.out create mode 100644 src/diff/tests/simple_u.out create mode 100644 src/diff/tests/simple_w.out create mode 100644 src/diff/tests/strip_o.out create mode 100644 src/diff/tests/unified_9999.out create mode 100644 src/diff/tests/unified_c9999.out create mode 100644 src/diff/tests/unified_p.out create mode 100644 src/diff/xmalloc.c create mode 100644 src/diff/xmalloc.h create mode 100644 src/diff3/diff3.1 create mode 100644 src/diff3/diff3.c create mode 100644 src/diff3/tests/1.out create mode 100644 src/diff3/tests/1.txt create mode 100644 src/diff3/tests/1cr.txt create mode 100644 src/diff3/tests/1t.out create mode 100644 src/diff3/tests/2.out create mode 100644 src/diff3/tests/2.txt create mode 100644 src/diff3/tests/3.out create mode 100644 src/diff3/tests/3.txt create mode 100644 src/diff3/tests/4.out create mode 100644 src/diff3/tests/5.out create mode 100644 src/diff3/tests/6.out create mode 100644 src/diff3/tests/7.out create mode 100644 src/diff3/tests/8.out create mode 100644 src/diff3/tests/9.out create mode 100644 src/diff3/tests/Makefile create mode 100644 src/diff3/tests/Makefile.depend create mode 100755 src/diff3/tests/diff3_test.sh create mode 100644 src/diff3/tests/long-A.out create mode 100644 src/diff3/tests/long-ed.out create mode 100644 src/diff3/tests/long-m.txt create mode 100644 src/diff3/tests/long-merge.out create mode 100644 src/diff3/tests/long-o.txt create mode 100644 src/diff3/tests/long-y.txt create mode 100644 src/find/extern.h create mode 100644 src/find/find.1 create mode 100644 src/find/find.c create mode 100644 src/find/find.h create mode 100644 src/find/function.c create mode 100644 src/find/getdate.y create mode 100644 src/find/ls.c create mode 100644 src/find/main.c create mode 100644 src/find/misc.c create mode 100644 src/find/operator.c create mode 100644 src/find/option.c create mode 100644 src/grep/file.c create mode 100644 src/grep/grep.1 create mode 100644 src/grep/grep.c create mode 100644 src/grep/grep.h create mode 100644 src/grep/queue.c create mode 100644 src/grep/util.c create mode 100644 src/gzip/gzexe create mode 100644 src/gzip/gzexe.1 create mode 100644 src/gzip/gzip.1 create mode 100644 src/gzip/gzip.c create mode 100644 src/gzip/unlz.c create mode 100644 src/gzip/unpack.c create mode 100644 src/gzip/unxz.c create mode 100644 src/gzip/zdiff create mode 100644 src/gzip/zdiff.1 create mode 100644 src/gzip/zforce create mode 100644 src/gzip/zforce.1 create mode 100755 src/gzip/zgrep create mode 100644 src/gzip/zgrep.1 create mode 100644 src/gzip/zless create mode 100644 src/gzip/zmore create mode 100644 src/gzip/zmore.1 create mode 100644 src/gzip/znew create mode 100644 src/gzip/znew.1 create mode 100644 src/gzip/zuncompress.c create mode 100644 src/main.c create mode 100644 src/patch/backupfile.c create mode 100644 src/patch/backupfile.h create mode 100644 src/patch/common.h create mode 100644 src/patch/inp.c create mode 100644 src/patch/inp.h create mode 100644 src/patch/mkpath.c create mode 100644 src/patch/patch.1 create mode 100644 src/patch/patch.c create mode 100644 src/patch/pathnames.h create mode 100644 src/patch/pch.c create mode 100644 src/patch/pch.h create mode 100644 src/patch/tests/PR74127-cline.diff create mode 100644 src/patch/tests/PR74127-good.diff create mode 100644 src/patch/tests/PR74127-repro.diff create mode 100644 src/patch/tests/PR74127.in create mode 100755 src/patch/tests/unified_patch_test.sh create mode 100644 src/patch/util.c create mode 100644 src/patch/util.h create mode 100644 src/sdiff/edit.c create mode 100644 src/sdiff/extern.h create mode 100644 src/sdiff/sdiff.1 create mode 100644 src/sdiff/sdiff.c create mode 100644 src/sdiff/tests/Makefile create mode 100644 src/sdiff/tests/Makefile.depend create mode 100644 src/sdiff/tests/d_dot.in create mode 100644 src/sdiff/tests/d_flags_l.out create mode 100644 src/sdiff/tests/d_flags_s.out create mode 100644 src/sdiff/tests/d_flags_w.out create mode 100644 src/sdiff/tests/d_iflags_a1.out create mode 100644 src/sdiff/tests/d_iflags_a2.out create mode 100644 src/sdiff/tests/d_iflags_b1.out create mode 100644 src/sdiff/tests/d_iflags_b2.out create mode 100644 src/sdiff/tests/d_iflags_c1.out create mode 100644 src/sdiff/tests/d_iflags_c2.out create mode 100644 src/sdiff/tests/d_iflags_d1.out create mode 100644 src/sdiff/tests/d_iflags_d2.out create mode 100644 src/sdiff/tests/d_input1 create mode 100644 src/sdiff/tests/d_input2 create mode 100644 src/sdiff/tests/d_oneline.in create mode 100644 src/sdiff/tests/d_oneline_a.out create mode 100644 src/sdiff/tests/d_oneline_b.out create mode 100644 src/sdiff/tests/d_same.out create mode 100644 src/sdiff/tests/d_short.out create mode 100644 src/sdiff/tests/d_tabends.in create mode 100644 src/sdiff/tests/d_tabends_a.out create mode 100644 src/sdiff/tests/d_tabends_b.out create mode 100644 src/sdiff/tests/d_tabends_c.out create mode 100644 src/sdiff/tests/d_tabs.out create mode 100644 src/sdiff/tests/d_tabs1.in create mode 100644 src/sdiff/tests/d_tabs2.in create mode 100755 src/sdiff/tests/sdiff_test.sh create mode 100644 src/which/which.1 create mode 100644 src/which/which.c create mode 100644 src/xargs/pathnames.h create mode 100644 src/xargs/strnsubst.c create mode 100644 src/xargs/xargs.1 create mode 100644 src/xargs/xargs.c create mode 100755 tests/harness.sh create mode 100755 tests/test_cmp.sh create mode 100755 tests/test_diff.sh create mode 100755 tests/test_diff3.sh create mode 100755 tests/test_find.sh create mode 100755 tests/test_grep.sh create mode 100755 tests/test_gzip.sh create mode 100755 tests/test_patch.sh create mode 100755 tests/test_sdiff.sh create mode 100755 tests/test_which.sh create mode 100755 tests/test_xargs.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a36ad8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build*/ +references +.vscode \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..25e4e0e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,75 @@ +# AGENTS.md + +## Purpose + +This document defines mandatory development rules for contributors and automated agents working on the VX Multicall Utility. + +All code changes must comply with the policies below. + +--- + +## 1. No Stubs + +Stubs are not allowed. + +Do **not**: + +- Add placeholder functions +- Leave partially implemented logic +- Add empty modules “for later” +- Return dummy values to satisfy the compiler + +All submitted code must: + +- Be fully implemented +- Compile cleanly +- Function as described +- Pass relevant tests + +If a feature is not ready, do not merge it. + +## 2. No Dead Code + +Do not introduce: + +- Unused structs +- Unused enums +- Unused functions +- Unused modules +- Commented-out code +- Remove makefiles etc in freshly copied BSD sources + +Remove obsolete code instead of commenting it out. + +## 3. Deterministic Behavior + +VX Multicall Utility requires reproducibility. + +All logic must: + +- Avoid non-deterministic behavior +- Avoid reliance on environment state unless explicitly defined +- Avoid hidden global state + +## 4. Testing + +New features must include: + +- add tests for new functionality +- ensure existing tests pass + +--- + +## Summary + +VX Multicall Utility is infrastructure. + +Code must be: + +- Complete +- Deterministic +- Secure +- Minimal +- Explicit + +No shortcuts. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..231004c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 SFG545 and ReallyUnusual + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE-FREEBSD b/LICENSE-FREEBSD new file mode 100644 index 0000000..9ed4f98 --- /dev/null +++ b/LICENSE-FREEBSD @@ -0,0 +1,125 @@ +# @(#)COPYRIGHT 8.2 (Berkeley) 3/21/94 + +The compilation of software known as FreeBSD is distributed under the +following terms: + +Copyright (c) 1992-2023 The FreeBSD Project. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. 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. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +The 4.4BSD and 4.4BSD-Lite software is distributed under the following +terms: + +All of the documentation and software included in the 4.4BSD and 4.4BSD-Lite +Releases is copyrighted by The Regents of the University of California. + +Copyright 1979, 1980, 1983, 1986, 1988, 1989, 1991, 1992, 1993, 1994 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. 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. +3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: +This product includes software developed by the University of +California, Berkeley and its contributors. +4. Neither the name of the University 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 REGENTS 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 REGENTS 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. + +The Institute of Electrical and Electronics Engineers and the American +National Standards Committee X3, on Information Processing Systems have +given us permission to reprint portions of their documentation. + +In the following statement, the phrase ``this text'' refers to portions +of the system documentation. + +Portions of this text are reprinted and reproduced in electronic form in +the second BSD Networking Software Release, from IEEE Std 1003.1-1988, IEEE +Standard Portable Operating System Interface for Computer Environments +(POSIX), copyright C 1988 by the Institute of Electrical and Electronics +Engineers, Inc. In the event of any discrepancy between these versions +and the original IEEE Standard, the original IEEE Standard is the referee +document. + +In the following statement, the phrase ``This material'' refers to portions +of the system documentation. + +This material is reproduced with permission from American National +Standards Committee X3, on Information Processing Systems. Computer and +Business Equipment Manufacturers Association (CBEMA), 311 First St., NW, +Suite 500, Washington, DC 20001-2178. The developmental work of +Programming Language C was completed by the X3J11 Technical Committee. + +The views and conclusions contained in the software and documentation are +those of the authors and should not be interpreted as representing official +policies, either expressed or implied, of the Regents of the University +of California. + + +NOTE: The copyright of UC Berkeley's Berkeley Software Distribution ("BSD") +source has been updated. The copyright addendum may be found at +ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change and is +included below. + +July 22, 1999 + +To All Licensees, Distributors of Any Version of BSD: + +As you know, certain of the Berkeley Software Distribution ("BSD") source +code files require that further distributions of products containing all or +portions of the software, acknowledge within their advertising materials +that such products contain software developed by UC Berkeley and its +contributors. + +Specifically, the provision reads: + +" * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by the University of + * California, Berkeley and its contributors." + +Effective immediately, licensees and distributors are no longer required to +include the acknowledgement within advertising materials. Accordingly, the +foregoing paragraph of those BSD Unix files containing it is hereby deleted +in its entirety. + +William Hoskins +Director, Office of Technology Licensing +University of California, Berkeley diff --git a/README.md b/README.md new file mode 100644 index 0000000..13c853c --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# VX + +VX is a BSD-oriented multicall utility for Linux. + +It bundles a growing set of FreeBSD-derived userland tools into a single `vx` binary, with applets selected by `argv[0]` or by subcommand. + +## Included applets + +- `patch` +- `diff`, `cmp`, `diff3`, `sdiff` +- `which` +- `gzip`, `gunzip`, `zcat`, `gzcat`, `uncompress` +- `grep`, `egrep`, `fgrep`, `rgrep` +- `find` +- `xargs` + +## Build + +VX uses Meson. + +```text +meson setup builddir +meson compile -C builddir +meson test -C builddir +``` + +## Run + +You can invoke applets either through the multicall binary: + +```text +./builddir/vx grep pattern file +./builddir/vx find . -name '*.c' +``` + +or via installed symlinks such as `grep`, `find`, or `patch`. + +## Notes + +- The project force-includes `src/compat-headers/compat.h` to provide Linux compatibility shims for BSD userland code. +- Some behavior is intentionally BSD-flavored, even where GNU tools differ. diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..945d3cf --- /dev/null +++ b/meson.build @@ -0,0 +1,192 @@ +project('vx', 'c', + version : '0.1.0', + default_options : [ + 'c_std=c11', + 'warning_level=2', + ], +) + +# --- Portability shim for building BSD userland on Linux --- +compat_inc = include_directories('src/compat-headers') + +compat_src = files( + 'src/compat-headers/fgetln.c', + 'src/compat-headers/strlcpy.c', + 'src/compat-headers/reallocf.c', + 'src/compat-headers/strtonum.c', + 'src/compat-headers/expand_number.c', + 'src/compat-headers/strmode.c', + 'src/compat-headers/ugid.c', + 'src/compat-headers/setmode.c', +) + +# Force-include compat.h before every translation unit so that +# _GNU_SOURCE and type shims are always available. +compat_args = ['-include', 'compat.h'] + +patch_src = files( + 'src/patch/patch.c', + 'src/patch/pch.c', + 'src/patch/inp.c', + 'src/patch/util.c', + 'src/patch/backupfile.c', + 'src/patch/mkpath.c', +) + +diff_src = files( + 'src/diff/diff.c', + 'src/diff/diffdir.c', + 'src/diff/diffreg.c', + 'src/diff/pr.c', + 'src/diff/xmalloc.c', +) + +cmp_src = files( + 'src/cmp/cmp.c', + 'src/cmp/link.c', + 'src/cmp/misc.c', + 'src/cmp/regular.c', + 'src/cmp/special.c', +) + +diff3_src = files( + 'src/diff3/diff3.c', +) + +sdiff_src = files( + 'src/sdiff/sdiff.c', + 'src/sdiff/edit.c', +) + +which_src = files( + 'src/which/which.c', +) + +gzip_src = files( + 'src/gzip/gzip.c', +) + +gzip_inc = include_directories('src/gzip') + +grep_src = files( + 'src/grep/grep.c', + 'src/grep/file.c', + 'src/grep/queue.c', + 'src/grep/util.c', +) + +grep_inc = include_directories('src/grep') + +# --- yacc generator for find's getdate.y --- +yacc = find_program('yacc', 'bison') +getdate_gen = custom_target('getdate', + input : 'src/find/getdate.y', + output : 'getdate.c', + command : [yacc, '-o', '@OUTPUT@', '@INPUT@'], +) + +find_src = files( + 'src/find/find.c', + 'src/find/function.c', + 'src/find/ls.c', + 'src/find/main.c', + 'src/find/misc.c', + 'src/find/operator.c', + 'src/find/option.c', +) + +find_inc = include_directories('src/find') + +xargs_src = files( + 'src/xargs/xargs.c', + 'src/xargs/strnsubst.c', +) + +xargs_inc = include_directories('src/xargs') + +gzip_defines = [ + '-DNO_BZIP2_SUPPORT', + '-DNO_ZSTD_SUPPORT', +] + +cc = meson.get_compiler('c') +libm = cc.find_library('m', required : false) +libz = dependency('zlib') +liblzma = dependency('liblzma') +libacl = cc.find_library('acl', required : true) + +vx = executable('vx', + 'src/main.c', + patch_src, + diff_src, + cmp_src, + diff3_src, + sdiff_src, + which_src, + gzip_src, + grep_src, + find_src, + getdate_gen, + xargs_src, + compat_src, + include_directories : [compat_inc, gzip_inc, grep_inc, find_inc, xargs_inc], + c_args : compat_args + gzip_defines, + dependencies : [libm, libz, liblzma, libacl], + install : true, +) + +# Create symlinks for all applets pointing to the vx binary +foreach name : ['patch', 'diff', 'cmp', 'diff3', 'sdiff', 'which', 'gzip', 'gunzip', 'zcat', 'uncompress', 'grep', 'egrep', 'fgrep', 'rgrep', 'find', 'xargs'] + meson.add_install_script('sh', '-c', + 'ln -sf vx "$DESTDIR$1/' + name + '"', + '_', get_option('bindir'), + ) +endforeach + +# --- Manpages --- +install_man( + 'src/patch/patch.1', + 'src/diff/diff.1', + 'src/cmp/cmp.1', + 'src/diff3/diff3.1', + 'src/sdiff/sdiff.1', + 'src/which/which.1', + 'src/gzip/gzip.1', + 'src/gzip/gzexe.1', + 'src/gzip/zdiff.1', + 'src/gzip/zforce.1', + 'src/gzip/zmore.1', + 'src/gzip/znew.1', + 'src/gzip/zgrep.1', + 'src/grep/grep.1', + 'src/find/find.1', + 'src/xargs/xargs.1', +) + +# --- Gzip shell script utilities --- +foreach s : ['zdiff', 'zforce', 'zmore', 'znew', 'gzexe', 'zgrep', 'zless'] + install_data('src/gzip/' + s, + install_dir : get_option('bindir'), + install_mode : 'rwxr-xr-x', + ) +endforeach + +# Install symlinks for script aliases +foreach pair : [['zcmp', 'zdiff'], ['zegrep', 'zgrep'], ['zfgrep', 'zgrep']] + meson.add_install_script('sh', '-c', + 'ln -sf ' + pair[1] + ' "$DESTDIR$1/' + pair[0] + '"', + '_', get_option('bindir'), + ) +endforeach + +# --- Tests --- +test_env = environment() +test_env.set('VX', vx.full_path()) + +foreach t : ['test_patch', 'test_diff', 'test_cmp', 'test_diff3', 'test_sdiff', 'test_which', 'test_gzip', 'test_grep', 'test_find', 'test_xargs'] + test(t, find_program('tests/' + t + '.sh'), + env : test_env, + depends : vx, + workdir : meson.project_source_root(), + ) +endforeach diff --git a/src/cmp/cmp.1 b/src/cmp/cmp.1 new file mode 100644 index 0000000..d7442c6 --- /dev/null +++ b/src/cmp/cmp.1 @@ -0,0 +1,212 @@ +.\" Copyright (c) 1987, 1990, 1993 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" This code is derived from software contributed to Berkeley by +.\" the Institute of Electrical and Electronics Engineers, Inc. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. +.\" +.\" @(#)cmp.1 8.1 (Berkeley) 6/6/93 +.\" +.Dd September 23, 2021 +.Dt CMP 1 +.Os +.Sh NAME +.Nm cmp +.Nd compare two files +.Sh SYNOPSIS +.Nm +.Op Fl l | s | x +.Op Fl bhz +.Op Fl -ignore-initial Ns Cm = Ns Ar num1 Ns Op :num2 +.Op Fl -bytes Ns Cm = Ns Ar num +.Ar file1 file2 +.Op Ar skip1 Op Ar skip2 +.Sh DESCRIPTION +The +.Nm +utility compares two files of any type and writes the results +to the standard output. +By default, +.Nm +is silent if the files are the same; if they differ, the byte +and line number at which the first difference occurred is reported. +.Pp +Bytes and lines are numbered beginning with one. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl b , Fl -print-bytes +Print each byte when a difference is found. +.It Fl h +Do not follow symbolic links. +.It Fl i Ar num1 Ns Oo :num2 Oc , Fl -ignore-initial= Ns Ar num1 Ns Op :num2 +Skip +.Ar num1 +bytes from +.Ar file1 , +and optionally skip +.Ar num2 +bytes from +.Ar file2 . +If +.Ar num2 +is not specified, then +.Ar num1 +is applied for both +.Ar file1 +and +.Ar file2 . +.It Fl l , Fl -verbose +Print the byte number (decimal) and the differing +byte values (octal) for each difference. +.It Fl n Ar num , Fl -bytes= Ns num +Only compare up to +.Ar num +bytes. +.It Fl s , Fl -silent , Fl -quiet +Print nothing for differing files; return exit +status only. +.It Fl x +Like +.Fl l +but prints in hexadecimal and using zero as index +for the first byte in the files. +.It Fl z +For regular files compare file sizes first, and fail the comparison if they +are not equal. +.El +.Pp +The optional arguments +.Ar skip1 +and +.Ar skip2 +are the byte offsets from the beginning of +.Ar file1 +and +.Ar file2 , +respectively, where the comparison will begin. +The offset is decimal by default, but may be expressed as a hexadecimal +or octal value by preceding it with a leading ``0x'' or ``0''. +.Pp +.Ar skip1 +and +.Ar skip2 +may also be specified with SI size suffixes. +.Sh EXIT STATUS +The +.Nm +utility exits with one of the following values: +.Bl -tag -width 4n +.It 0 +The files are identical. +.It 1 +The files are different; this includes the case +where one file is identical to the first part of +the other. +In the latter case, if the +.Fl s +option has not been specified, +.Nm +writes to standard error that EOF was reached in the shorter +file (before any differences were found). +.It >1 +An error occurred. +.El +.Sh EXAMPLES +Assuming a file named +.Pa example.txt +with the following contents: +.Bd -literal -offset indent +a +b +c +.Ed +.Pp +Compare stdin with +.Pa example.txt : +.Bd -literal -offset indent +$ echo -e "a\\nb\\nc" | cmp - example.txt +.Ed +.Pp +Same as above but introducing a change in the third byte of stdin. +Show the byte number (decimal) and differing byte (octal): +.Bd -literal -offset indent +$ echo -e "a\\nR\\nc" | cmp -l - example.txt + 3 122 142 +.Ed +.Pp +Compare file sizes of +.Pa example.txt +and +.Pa /boot/loader.conf +and return 1 if they are not equal. +Note that +.Fl z +can only be used with regular files: +.Bd -literal -offset indent +$ cmp -z example.txt /boot/loader.conf +example.txt /boot/loader.conf differ: size +.Ed +.Pp +Compare stdin with +.Pa example.txt +omitting the first 4 bytes from stdin and the first 2 bytes from +.Pa example.txt : +.Bd -literal -offset indent +$ echo -e "a\\nR\\nb\\nc" | cmp - example.txt 4 2 +.Ed +.Sh SEE ALSO +.Xr diff 1 , +.Xr diff3 1 +.Sh STANDARDS +The +.Nm +utility is expected to be +.St -p1003.2 +compatible. +The +.Fl b , +.Fl h , +.Fl i , +.Fl n , +.Fl x , +and +.Fl z +options are extensions to the standard. +.Ar skip1 +and +.Ar skip2 +arguments are extensions to the standard. +.Sh HISTORY +A +.Nm +command appeared in +.At v1 . +.Sh BUGS +The phrase +.Dq SI size suffixes +above refers to the traditional power of two convention, as described in +.Xr expand_number 3 . diff --git a/src/cmp/cmp.c b/src/cmp/cmp.c new file mode 100644 index 0000000..e6c7271 --- /dev/null +++ b/src/cmp/cmp.c @@ -0,0 +1,266 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1987, 1990, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "extern.h" + +bool bflag, lflag, sflag, xflag, zflag; + +static const struct option long_opts[] = +{ + {"print-bytes", no_argument, NULL, 'b'}, + {"ignore-initial", required_argument, NULL, 'i'}, + {"verbose", no_argument, NULL, 'l'}, + {"bytes", required_argument, NULL, 'n'}, + {"silent", no_argument, NULL, 's'}, + {"quiet", no_argument, NULL, 's'}, + {NULL, no_argument, NULL, 0} +}; + +#ifdef SIGINFO +volatile sig_atomic_t info; + +static void +siginfo(int signo) +{ + info = signo; +} +#endif + +static void usage(void) __dead2; + +static bool +parse_iskipspec(char *spec, off_t *skip1, off_t *skip2) +{ + char *colon; + + colon = strchr(spec, ':'); + if (colon != NULL) + *colon++ = '\0'; + + if (expand_number(spec, skip1) < 0) + return (false); + + if (colon != NULL) + return (expand_number(colon, skip2) == 0); + + *skip2 = *skip1; + return (true); +} + +int +cmp_main(int argc, char *argv[]) +{ + struct stat sb1, sb2; + off_t skip1, skip2, limit; + int ch, fd1, fd2, oflag; + bool special; + const char *file1, *file2; + int ret; + + limit = skip1 = skip2 = ret = 0; + oflag = O_RDONLY; + while ((ch = getopt_long(argc, argv, "+bhi:ln:sxz", long_opts, NULL)) != -1) + switch (ch) { + case 'b': /* Print bytes */ + bflag = true; + break; + case 'h': /* Don't follow symlinks */ + oflag |= O_NOFOLLOW; + break; + case 'i': + if (!parse_iskipspec(optarg, &skip1, &skip2)) { + fprintf(stderr, + "Invalid --ignore-initial: %s\n", + optarg); + usage(); + } + break; + case 'l': /* print all differences */ + lflag = true; + break; + case 'n': /* Limit */ + if (expand_number(optarg, &limit) < 0 || limit < 0) { + fprintf(stderr, "Invalid --bytes: %s\n", + optarg); + usage(); + } + break; + case 's': /* silent run */ + sflag = true; + break; + case 'x': /* hex output */ + lflag = true; + xflag = true; + break; + case 'z': /* compare size first */ + zflag = true; + break; + case '?': + default: + usage(); + } + argv += optind; + argc -= optind; + + if (lflag && sflag) + errx(ERR_EXIT, "specifying -s with -l or -x is not permitted"); + + if (argc < 2 || argc > 4) + usage(); + + /* Don't limit rights on stdin since it may be one of the inputs. */ + if (caph_limit_stream(STDOUT_FILENO, CAPH_WRITE | CAPH_IGNORE_EBADF)) + err(ERR_EXIT, "unable to limit rights on stdout"); + if (caph_limit_stream(STDERR_FILENO, CAPH_WRITE | CAPH_IGNORE_EBADF)) + err(ERR_EXIT, "unable to limit rights on stderr"); + + /* Backward compatibility -- handle "-" meaning stdin. */ + special = false; + if (strcmp(file1 = argv[0], "-") == 0) { + special = true; + fd1 = STDIN_FILENO; + file1 = "stdin"; + } else if ((fd1 = open(file1, oflag, 0)) < 0 && errno != EMLINK) { + if (!sflag) + err(ERR_EXIT, "%s", file1); + else + exit(ERR_EXIT); + } + if (strcmp(file2 = argv[1], "-") == 0) { + if (special) + errx(ERR_EXIT, + "standard input may only be specified once"); + special = true; + fd2 = STDIN_FILENO; + file2 = "stdin"; + } else if ((fd2 = open(file2, oflag, 0)) < 0 && errno != EMLINK) { + if (!sflag) + err(ERR_EXIT, "%s", file2); + else + exit(ERR_EXIT); + } + + if (argc > 2 && expand_number(argv[2], &skip1) < 0) { + fprintf(stderr, "Invalid skip1: %s\n", argv[2]); + usage(); + } + + if (argc == 4 && expand_number(argv[3], &skip2) < 0) { + fprintf(stderr, "Invalid skip2: %s\n", argv[3]); + usage(); + } + + if (sflag && skip1 == 0 && skip2 == 0) + zflag = true; + + if (fd1 == -1) { + if (fd2 == -1) { + ret = c_link(file1, skip1, file2, skip2, limit); + goto end; + } else if (!sflag) + errx(ERR_EXIT, "%s: Not a symbolic link", file2); + else + exit(ERR_EXIT); + } else if (fd2 == -1) { + if (!sflag) + errx(ERR_EXIT, "%s: Not a symbolic link", file1); + else + exit(ERR_EXIT); + } + + /* FD rights are limited in c_special() and c_regular(). */ + caph_cache_catpages(); + + if (!special) { + if (fstat(fd1, &sb1)) { + if (!sflag) + err(ERR_EXIT, "%s", file1); + else + exit(ERR_EXIT); + } + if (!S_ISREG(sb1.st_mode)) + special = true; + else { + if (fstat(fd2, &sb2)) { + if (!sflag) + err(ERR_EXIT, "%s", file2); + else + exit(ERR_EXIT); + } + if (!S_ISREG(sb2.st_mode)) + special = true; + } + } + +#ifdef SIGINFO + (void)signal(SIGINFO, siginfo); +#endif + if (special) { + ret = c_special(fd1, file1, skip1, fd2, file2, skip2, limit); + } else { + if (zflag && sb1.st_size != sb2.st_size) { + if (!sflag) + (void)printf("%s %s differ: size\n", + file1, file2); + ret = DIFF_EXIT; + } else { + ret = c_regular(fd1, file1, skip1, sb1.st_size, + fd2, file2, skip2, sb2.st_size, limit); + } + } +end: + if (!sflag && fflush(stdout) != 0) + err(ERR_EXIT, "stdout"); + exit(ret); +} + +static void +usage(void) +{ + + (void)fprintf(stderr, + "usage: cmp [-l | -s | -x] [-hz] file1 file2 [skip1 [skip2]]\n"); + exit(ERR_EXIT); +} diff --git a/src/cmp/extern.h b/src/cmp/extern.h new file mode 100644 index 0000000..c2d811c --- /dev/null +++ b/src/cmp/extern.h @@ -0,0 +1,57 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1991, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + * + * @(#)extern.h 8.3 (Berkeley) 4/2/94 + * + */ + +/* + * Namespace flag globals to avoid collisions in the vx multicall binary. + * diff/diff.h defines identically-named globals. + */ +#define lflag cmp_lflag +#define sflag cmp_sflag + +#define OK_EXIT 0 +#define DIFF_EXIT 1 +#define ERR_EXIT 2 /* error exit code */ + +int c_link(const char *, off_t, const char *, off_t, off_t); +int c_regular(int, const char *, off_t, off_t, int, const char *, off_t, + off_t, off_t); +int c_special(int, const char *, off_t, int, const char *, off_t, off_t); +void diffmsg(const char *, const char *, off_t, off_t, int, int); +void eofmsg(const char *); + +extern bool bflag, lflag, sflag, xflag, zflag; + +#ifdef SIGINFO +extern volatile sig_atomic_t info; +#endif diff --git a/src/cmp/link.c b/src/cmp/link.c new file mode 100644 index 0000000..dfa2f95 --- /dev/null +++ b/src/cmp/link.c @@ -0,0 +1,103 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2005 Brian Somers + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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 + +#include +#include +#include +#include +#include +#include + +#include "extern.h" + +int +c_link(const char *file1, off_t skip1, const char *file2, off_t skip2, + off_t limit) +{ + char buf1[PATH_MAX], *p1; + char buf2[PATH_MAX], *p2; + ssize_t len1, len2; + int dfound; + off_t byte; + u_char ch; + + if ((len1 = readlink(file1, buf1, sizeof(buf1) - 1)) < 0) { + if (!sflag) + err(ERR_EXIT, "%s", file1); + else + exit(ERR_EXIT); + } + + if ((len2 = readlink(file2, buf2, sizeof(buf2) - 1)) < 0) { + if (!sflag) + err(ERR_EXIT, "%s", file2); + else + exit(ERR_EXIT); + } + + if (skip1 > len1) + skip1 = len1; + buf1[len1] = '\0'; + + if (skip2 > len2) + skip2 = len2; + buf2[len2] = '\0'; + + dfound = 0; + byte = 1; + for (p1 = buf1 + skip1, p2 = buf2 + skip2; + *p1 && *p2 && (limit == 0 || byte <= limit); p1++, p2++) { + if ((ch = *p1) != *p2) { + if (xflag) { + dfound = 1; + (void)printf("%08llx %02x %02x\n", + (long long)byte - 1, ch, *p2); + } else if (lflag) { + dfound = 1; + if (bflag) + (void)printf("%6lld %3o %c %3o %c\n", + (long long)byte, ch, ch, *p2, *p2); + else + (void)printf("%6lld %3o %3o\n", + (long long)byte, ch, *p2); + } else { + diffmsg(file1, file2, byte, 1, ch, *p2); + return (DIFF_EXIT); + } + } + byte++; + } + + if (*p1 || *p2) { + eofmsg (*p1 ? file2 : file1); + return (DIFF_EXIT); + } + return (dfound ? DIFF_EXIT : 0); +} diff --git a/src/cmp/misc.c b/src/cmp/misc.c new file mode 100644 index 0000000..48d6161 --- /dev/null +++ b/src/cmp/misc.c @@ -0,0 +1,68 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1991, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + */ + +#if 0 +#ifndef lint +static char sccsid[] = "@(#)misc.c 8.3 (Berkeley) 4/2/94"; +#endif +#endif + +#include + +#include +#include +#include +#include + +#include "extern.h" + +void +eofmsg(const char *file) +{ + if (!sflag) + warnx("EOF on %s", file); +} + +void +diffmsg(const char *file1, const char *file2, off_t byte, off_t line, + int b1, int b2) +{ + if (sflag) { + /* nothing */ + } else if (bflag) { + (void)printf("%s %s differ: char %lld, line %lld is %3o %c %3o %c\n", + file1, file2, (long long)byte, (long long)line, b1, b1, + b2, b2); + } else { + (void)printf("%s %s differ: char %lld, line %lld\n", + file1, file2, (long long)byte, (long long)line); + } +} diff --git a/src/cmp/regular.c b/src/cmp/regular.c new file mode 100644 index 0000000..9b04823 --- /dev/null +++ b/src/cmp/regular.c @@ -0,0 +1,195 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1991, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + */ + +#if 0 +#ifndef lint +static char sccsid[] = "@(#)regular.c 8.3 (Berkeley) 4/2/94"; +#endif +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "extern.h" + +static u_char *remmap(u_char *, int, off_t); +static void segv_handler(int); +#define MMAP_CHUNK (8*1024*1024) + +#define ROUNDPAGE(i) ((i) & ~pagemask) + +int +c_regular(int fd1, const char *file1, off_t skip1, off_t len1, + int fd2, const char *file2, off_t skip2, off_t len2, off_t limit) +{ + struct sigaction act, oact; + u_char ch, *p1, *p2, *m1, *m2, *e1, *e2; + off_t byte, length, line; + off_t pagemask, off1, off2; + size_t pagesize; + int dfound; + + if (skip1 > len1) { + eofmsg(file1); + return (DIFF_EXIT); + } + len1 -= skip1; + if (skip2 > len2) { + eofmsg(file2); + return (DIFF_EXIT); + } + len2 -= skip2; + + if (sflag && len1 != len2) + return (DIFF_EXIT); + + pagesize = getpagesize(); + pagemask = (off_t)pagesize - 1; + off1 = ROUNDPAGE(skip1); + off2 = ROUNDPAGE(skip2); + + length = MIN(len1, len2); + if (limit > 0) + length = MIN(length, limit); + + if ((m1 = remmap(NULL, fd1, off1)) == NULL) { + return (c_special(fd1, file1, skip1, fd2, file2, skip2, limit)); + } + + if ((m2 = remmap(NULL, fd2, off2)) == NULL) { + munmap(m1, MMAP_CHUNK); + return (c_special(fd1, file1, skip1, fd2, file2, skip2, limit)); + } + + if (caph_rights_limit(fd1, cap_rights_init(&rights, CAP_MMAP_R)) < 0) + err(1, "unable to limit rights for %s", file1); + if (caph_rights_limit(fd2, cap_rights_init(&rights, CAP_MMAP_R)) < 0) + err(1, "unable to limit rights for %s", file2); + if (caph_enter() < 0) + err(ERR_EXIT, "unable to enter capability mode"); + + sigemptyset(&act.sa_mask); + act.sa_flags = SA_NODEFER; + act.sa_handler = segv_handler; + if (sigaction(SIGSEGV, &act, &oact)) + err(ERR_EXIT, "sigaction()"); + + dfound = 0; + e1 = m1 + MMAP_CHUNK; + e2 = m2 + MMAP_CHUNK; + p1 = m1 + (skip1 - off1); + p2 = m2 + (skip2 - off2); + + for (byte = line = 1; length--; ++byte) { +#ifdef SIGINFO + if (info) { + (void)fprintf(stderr, "%s %s char %zu line %zu\n", + file1, file2, (size_t)byte, (size_t)line); + info = 0; + } +#endif + if ((ch = *p1) != *p2) { + dfound = 1; + if (xflag) { + (void)printf("%08llx %02x %02x\n", + (long long)byte - 1, ch, *p2); + } else if (lflag) { + if (bflag) + (void)printf("%6lld %3o %c %3o %c\n", + (long long)byte, ch, ch, *p2, *p2); + else + (void)printf("%6lld %3o %3o\n", + (long long)byte, ch, *p2); + } else { + diffmsg(file1, file2, byte, line, ch, *p2); + return (DIFF_EXIT); + } + } + if (ch == '\n') + ++line; + if (++p1 == e1) { + off1 += MMAP_CHUNK; + if ((p1 = m1 = remmap(m1, fd1, off1)) == NULL) { + munmap(m2, MMAP_CHUNK); + err(ERR_EXIT, "remmap %s", file1); + } + e1 = m1 + MMAP_CHUNK; + } + if (++p2 == e2) { + off2 += MMAP_CHUNK; + if ((p2 = m2 = remmap(m2, fd2, off2)) == NULL) { + munmap(m1, MMAP_CHUNK); + err(ERR_EXIT, "remmap %s", file2); + } + e2 = m2 + MMAP_CHUNK; + } + } + munmap(m1, MMAP_CHUNK); + munmap(m2, MMAP_CHUNK); + + if (sigaction(SIGSEGV, &oact, NULL)) + err(ERR_EXIT, "sigaction()"); + + if (len1 != len2) { + eofmsg(len1 > len2 ? file2 : file1); + return (DIFF_EXIT); + } + return (dfound ? DIFF_EXIT : 0); +} + +static u_char * +remmap(u_char *mem, int fd, off_t offset) +{ + if (mem != NULL) + munmap(mem, MMAP_CHUNK); + mem = mmap(NULL, MMAP_CHUNK, PROT_READ, MAP_SHARED, fd, offset); + if (mem == MAP_FAILED) + return (NULL); + madvise(mem, MMAP_CHUNK, MADV_SEQUENTIAL); + return (mem); +} + +static void +segv_handler(int sig __unused) { + static const char msg[] = "cmp: Input/output error (caught SIGSEGV)\n"; + + write(STDERR_FILENO, msg, sizeof(msg)); + _exit(EXIT_FAILURE); +} diff --git a/src/cmp/special.c b/src/cmp/special.c new file mode 100644 index 0000000..c92b6fd --- /dev/null +++ b/src/cmp/special.c @@ -0,0 +1,131 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1991, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + */ + +#if 0 +#ifndef lint +static char sccsid[] = "@(#)special.c 8.3 (Berkeley) 4/2/94"; +#endif +#endif + +#include + +#include +#include +#include +#include + +#include "extern.h" + +int +c_special(int fd1, const char *file1, off_t skip1, + int fd2, const char *file2, off_t skip2, off_t limit) +{ + int ch1, ch2; + off_t byte, line; + FILE *fp1, *fp2; + int dfound; + + if (caph_limit_stream(fd1, CAPH_READ) < 0) + err(ERR_EXIT, "caph_limit_stream(%s)", file1); + if (caph_limit_stream(fd2, CAPH_READ) < 0) + err(ERR_EXIT, "caph_limit_stream(%s)", file2); + if (caph_enter() < 0) + err(ERR_EXIT, "unable to enter capability mode"); + + if ((fp1 = fdopen(fd1, "r")) == NULL) + err(ERR_EXIT, "%s", file1); + (void)setvbuf(fp1, NULL, _IOFBF, 65536); + if ((fp2 = fdopen(fd2, "r")) == NULL) + err(ERR_EXIT, "%s", file2); + (void)setvbuf(fp2, NULL, _IOFBF, 65536); + + dfound = 0; + while (skip1--) + if (getc(fp1) == EOF) + goto eof; + while (skip2--) + if (getc(fp2) == EOF) + goto eof; + + for (byte = line = 1; limit == 0 || byte <= limit; ++byte) { +#ifdef SIGINFO + if (info) { + (void)fprintf(stderr, "%s %s char %zu line %zu\n", + file1, file2, (size_t)byte, (size_t)line); + info = 0; + } +#endif + ch1 = getc(fp1); + ch2 = getc(fp2); + if (ch1 == EOF || ch2 == EOF) + break; + if (ch1 != ch2) { + if (xflag) { + dfound = 1; + (void)printf("%08llx %02x %02x\n", + (long long)byte - 1, ch1, ch2); + } else if (lflag) { + dfound = 1; + if (bflag) + (void)printf("%6lld %3o %c %3o %c\n", + (long long)byte, ch1, ch1, ch2, + ch2); + else + (void)printf("%6lld %3o %3o\n", + (long long)byte, ch1, ch2); + } else { + diffmsg(file1, file2, byte, line, ch1, ch2); + return (DIFF_EXIT); + } + } + if (ch1 == '\n') + ++line; + } + +eof: if (ferror(fp1)) + err(ERR_EXIT, "%s", file1); + if (ferror(fp2)) + err(ERR_EXIT, "%s", file2); + if (feof(fp1)) { + if (!feof(fp2)) { + eofmsg(file1); + return (DIFF_EXIT); + } + } else { + if (feof(fp2)) { + eofmsg(file2); + return (DIFF_EXIT); + } + } + fclose(fp2); + fclose(fp1); + return (dfound ? DIFF_EXIT : 0); +} diff --git a/src/cmp/tests/Makefile b/src/cmp/tests/Makefile new file mode 100644 index 0000000..6f00ecd --- /dev/null +++ b/src/cmp/tests/Makefile @@ -0,0 +1,14 @@ + +.include + +PACKAGE= tests + +ATF_TESTS_SH+= cmp_test2 +NETBSD_ATF_TESTS_SH= cmp_test + +${PACKAGE}FILES+= b_flag.out +${PACKAGE}FILES+= bl_flag.out + +.include + +.include diff --git a/src/cmp/tests/Makefile.depend b/src/cmp/tests/Makefile.depend new file mode 100644 index 0000000..11aba52 --- /dev/null +++ b/src/cmp/tests/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/src/cmp/tests/b_flag.out b/src/cmp/tests/b_flag.out new file mode 100644 index 0000000..bb3d288 --- /dev/null +++ b/src/cmp/tests/b_flag.out @@ -0,0 +1 @@ +a b differ: char 3, line 1 is 143 c 144 d diff --git a/src/cmp/tests/bl_flag.out b/src/cmp/tests/bl_flag.out new file mode 100644 index 0000000..8ec9007 --- /dev/null +++ b/src/cmp/tests/bl_flag.out @@ -0,0 +1 @@ + 3 143 c 144 d diff --git a/src/cmp/tests/cmp_test2.sh b/src/cmp/tests/cmp_test2.sh new file mode 100755 index 0000000..bca8fc1 --- /dev/null +++ b/src/cmp/tests/cmp_test2.sh @@ -0,0 +1,175 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2017 Alan Somers +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. 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 FOUNDATION 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. +# + +atf_test_case special +special_head() { + atf_set "descr" "Test cmp(1)'s handling of non-regular files" +} +special_body() { + echo 0123456789abcdef > a + echo 0123456789abcdeg > b + atf_check -s exit:0 -o empty -e empty cmp a - a + echo 0123456789abcdeg > b + ln -s a a.lnk + ln -s b b.lnk + ln -s a a2.lnk + cp a adup + ln -s adup adup.lnk + atf_check -s exit:0 cmp a a.lnk + atf_check -s exit:0 cmp a.lnk a + atf_check -s not-exit:0 -o ignore cmp a b.lnk + atf_check -s not-exit:0 -o ignore cmp b.lnk a + atf_check -s not-exit:0 -o ignore -e ignore cmp -h a a.lnk + atf_check -s not-exit:0 -o ignore -e ignore cmp -h a.lnk a + atf_check -s exit:0 cmp -h a.lnk a2.lnk + atf_check -s not-exit:0 -o ignore -e ignore cmp -h a.lnk adup.lnk +} + +atf_test_case pr252542 +pr252542_head() +{ + atf_set "descr" "Test cmp(1) -s with file offset skips" +} +pr252542_body() +{ + echo -n '1234567890' > a + echo -n 'abc567890' > b + echo -n 'xbc567890' > c + atf_check -s exit:0 cmp -s a b 4 3 + atf_check -s exit:0 cmp -i 4:3 -s a b + atf_check -s exit:0 cmp -i 1 -s b c + atf_check -s exit:1 -o ignore cmp -z a b 4 3 + atf_check -s exit:1 -o ignore cmp -i 4:3 -z a b + atf_check -s exit:1 -o ignore cmp -i 1 -z a b +} + +atf_test_case skipsuff +skipsuff_head() +{ + atf_set "descr" "Test cmp(1) accepting SI suffixes on skips" +} +skipsuff_body() +{ + + jot -nb a -s '' 1028 > a + jot -nb b -s '' 1024 > b + jot -nb a -s '' 4 >> b + + atf_check -s exit:1 -o ignore cmp -s a b + atf_check -s exit:0 cmp -s a b 1k 1k +} + +atf_test_case limit +limit_head() +{ + atf_set "descr" "Test cmp(1) -n (limit)" +} +limit_body() +{ + echo -n "aaaabbbb" > a + echo -n "aaaaxxxx" > b + + atf_check -s exit:1 -o ignore cmp -s a b + atf_check -s exit:0 cmp -sn 4 a b + atf_check -s exit:0 cmp -sn 3 a b + atf_check -s exit:1 -o ignore cmp -sn 5 a b + + # Test special, too. The implementation for link is effectively + # identical. + atf_check -s exit:0 -e empty cmp -sn 4 b - a + echo -n "abdd" > b + + atf_check -s exit:1 -o file:$(atf_get_srcdir)/b_flag.out \ + cmp -b a b + atf_check -s exit:1 -o file:$(atf_get_srcdir)/bl_flag.out \ + cmp -bl a b +} + +# Helper for stdout test case +atf_check_stdout() +{ + ( + trap "" PIPE + sleep 1 + cmp "$@" 2>stderr + echo $? >result + ) | true + atf_check -o inline:"2\n" cat result + atf_check -o match:"stdout" cat stderr +} + +atf_test_case stdout +stdout_head() +{ + atf_set descr "Failure to write to stdout" +} +stdout_body() +{ + echo a >a + echo b >b + atf_check_stdout a b + atf_check_stdout - b +#include +#include + +/* ------------------------------------------------------------------ */ +/* sys/cdefs.h compat -- BSD compiler attribute macros */ +/* ------------------------------------------------------------------ */ + +#ifndef __dead2 +#define __dead2 __attribute__((noreturn)) +#endif + +#ifndef __unused +#define __unused __attribute__((unused)) +#endif + +#ifndef __DECONST +#define __DECONST(type, var) ((type)(unsigned long)(const void *)(var)) +#endif + +/* ------------------------------------------------------------------ */ +/* Capsicum / sandbox stubs -- Linux has no Capsicum */ +/* ------------------------------------------------------------------ */ +#ifndef __FreeBSD__ +typedef struct { int cr_rights; } cap_rights_t; +#define cap_rights_init(r, ...) ((void)0) +#define cap_rights_set(r, ...) ((void)0) +#define caph_rights_limit(fd, r) (0) +#define caph_limit_stream(fd, r) (0) +#define caph_limit_stdio() (0) +#define caph_limit_stdin() (0) +#define caph_limit_stdout() (0) +#define caph_limit_stderr() (0) +#define caph_cache_catpages() (0) +#define caph_cache_tzdata() (0) +#define caph_enter() (0) +#define CAP_READ 0 +#define CAP_WRITE 0 +#define CAP_FSTAT 0 +#define CAP_SEEK 0 +#define CAP_MMAP_R 0 +#define CAP_FCNTL 0 +#define CAP_EVENT 0 +#define CAPH_READ 0 +#define CAPH_WRITE 0 +#define CAPH_IGNORE_EBADF 0 +#endif + +/* ------------------------------------------------------------------ */ +/* SIGINFO -- BSD-only signal; not available on Linux */ +/* ------------------------------------------------------------------ */ +#ifndef SIGINFO +#define SIGINFO SIGUSR1 +#endif + +/* ------------------------------------------------------------------ */ +/* sig_t -- BSD typedef for signal handler function pointer */ +/* ------------------------------------------------------------------ */ +#ifndef __OpenBSD__ +#include +#ifndef __FreeBSD__ +typedef void (*sig_t)(int); +#endif +#endif + +/* ------------------------------------------------------------------ */ +/* d_namlen -- BSD struct dirent member; Linux lacks it */ +/* ------------------------------------------------------------------ */ +#include +#include +#ifndef _DIRENT_HAVE_D_NAMLEN +#define DIRENT_NAMLEN(dp) strlen((dp)->d_name) +#else +#define DIRENT_NAMLEN(dp) ((dp)->d_namlen) +#endif + +/* + * d_fileno -- BSD name for inode number in struct dirent. + * Linux uses d_ino. + */ +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) +#define d_fileno d_ino +#endif + +/* ------------------------------------------------------------------ */ +/* fgetln -- BSD line-reading function; see fgetln.c */ +/* ------------------------------------------------------------------ */ +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) +char *fgetln(FILE *fp, size_t *lenp); +#endif + +/* ------------------------------------------------------------------ */ +/* strlcpy / strlcat -- bounded string ops from OpenBSD */ +/* ------------------------------------------------------------------ */ +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) +#if !defined(__GLIBC__) || (__GLIBC__ < 2) || \ + (__GLIBC__ == 2 && __GLIBC_MINOR__ < 38) +size_t strlcpy(char *dst, const char *src, size_t dsize); +size_t strlcat(char *dst, const char *src, size_t dsize); +#define VX_NEED_STRLCPY 1 +#endif +#endif + +/* ------------------------------------------------------------------ */ +/* reallocf -- frees original on failure; see reallocf.c */ +/* ------------------------------------------------------------------ */ +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) +void *reallocf(void *ptr, size_t size); +#endif + +/* ------------------------------------------------------------------ */ +/* strtonum -- safe string-to-number; see strtonum.c */ +/* ------------------------------------------------------------------ */ +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) +long long strtonum(const char *nptr, long long minval, + long long maxval, const char **errstrp); +#endif + +/* ------------------------------------------------------------------ */ +/* errc / warnc -- BSD err.h variants with explicit errno */ +/* ------------------------------------------------------------------ */ +#include +#include +#if !defined(__FreeBSD__) +#define errc(eval, code, ...) do { errno = (code); err((eval), __VA_ARGS__); } while (0) +#define warnc(code, ...) do { errno = (code); warn(__VA_ARGS__); } while (0) +#endif + +/* ------------------------------------------------------------------ */ +/* expand_number -- FreeBSD libutil; see expand_number.c */ +/* ------------------------------------------------------------------ */ +#if !defined(__FreeBSD__) +#include +int expand_number(const char *buf, uint64_t *num); +#endif + +/* ------------------------------------------------------------------ */ +/* stdckdint.h -- C23 checked integer arithmetic */ +/* Provide fallback using GCC/Clang builtins. */ +/* ------------------------------------------------------------------ */ +#if defined(__has_include) +#if __has_include() +#include +#define VX_HAVE_STDCKDINT 1 +#endif +#endif +#ifndef VX_HAVE_STDCKDINT +#define ckd_add(result, a, b) __builtin_add_overflow((a), (b), (result)) +#define ckd_sub(result, a, b) __builtin_sub_overflow((a), (b), (result)) +#define ckd_mul(result, a, b) __builtin_mul_overflow((a), (b), (result)) +#endif + +/* ------------------------------------------------------------------ */ +/* roundup macro -- BSD sys/param.h; may be missing */ +/* ------------------------------------------------------------------ */ +#ifndef roundup +#define roundup(x, y) ((((x) + ((y) - 1)) / (y)) * (y)) +#endif + +/* ------------------------------------------------------------------ */ +/* optreset -- BSD getopt extension */ +/* ------------------------------------------------------------------ */ +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) +#include +static int vx_optreset_dummy_ __attribute__((unused)); +#define optreset vx_optreset_dummy_ +#endif + +/* ------------------------------------------------------------------ */ +/* strmode -- convert mode_t to ls-style string; see strmode.c */ +/* ------------------------------------------------------------------ */ +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) +void strmode(mode_t mode, char *p); +#endif + +/* ------------------------------------------------------------------ */ +/* user_from_uid / group_from_gid -- cached passwd/group lookups */ +/* see ugid.c */ +/* ------------------------------------------------------------------ */ +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) +const char *user_from_uid(uid_t uid, int nouser); +const char *group_from_gid(gid_t gid, int nogroup); +#endif + +/* ------------------------------------------------------------------ */ +/* MAXLOGNAME -- BSD login name limit; Linux uses LOGIN_NAME_MAX */ +/* ------------------------------------------------------------------ */ +#ifndef MAXLOGNAME +#include +#ifdef LOGIN_NAME_MAX +#define MAXLOGNAME LOGIN_NAME_MAX +#else +#define MAXLOGNAME 17 +#endif +#endif + +/* ------------------------------------------------------------------ */ +/* D_MD_ORDER -- BSD langinfo constant for month/day ordering */ +/* Not available on Linux; define a harmless fallback. */ +/* ------------------------------------------------------------------ */ +#include +#ifndef D_MD_ORDER +#define D_MD_ORDER D_FMT +#endif + +/* ------------------------------------------------------------------ */ +/* MAXPATHLEN -- BSD path limit constant; Linux uses PATH_MAX */ +/* ------------------------------------------------------------------ */ +#ifndef MAXPATHLEN +#include +#ifdef PATH_MAX +#define MAXPATHLEN PATH_MAX +#else +#define MAXPATHLEN 4096 +#endif +#endif + +/* ------------------------------------------------------------------ */ +/* REG_BASIC -- BSD regex constant; 0 on Linux (default) */ +/* ------------------------------------------------------------------ */ +#ifndef REG_BASIC +#define REG_BASIC 0 +#endif + +/* ------------------------------------------------------------------ */ +/* S_ISTXT -- BSD sticky bit name; Linux uses S_ISVTX */ +/* ------------------------------------------------------------------ */ +#ifndef S_ISTXT +#define S_ISTXT S_ISVTX +#endif + +/* ------------------------------------------------------------------ */ +/* QUAD_MAX -- BSD quad_t limit; use LLONG_MAX */ +/* ------------------------------------------------------------------ */ +#ifndef QUAD_MAX +#include +#define QUAD_MAX LLONG_MAX +#endif + +/* ------------------------------------------------------------------ */ +/* setmode / getmode -- BSD file mode parsing */ +/* ------------------------------------------------------------------ */ +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) +void *setmode(const char *p); +mode_t getmode(const void *set, mode_t omode); +#endif + +/* ------------------------------------------------------------------ */ +/* makedev -- Linux needs sys/sysmacros.h */ +/* ------------------------------------------------------------------ */ +#ifdef __linux__ +#include +#endif + +#endif /* VX_COMPAT_H */ diff --git a/src/compat-headers/expand_number.c b/src/compat-headers/expand_number.c new file mode 100644 index 0000000..d0cc58c --- /dev/null +++ b/src/compat-headers/expand_number.c @@ -0,0 +1,83 @@ +/* + * expand_number.c - FreeBSD libutil expand_number() for Linux. + * + * expand_number(buf, num) + * Parses a string containing a number with an optional suffix + * denoting a multiplier. Supported suffixes: + * k/K = 1024, m/M = 1048576, g/G = 2^30, t/T = 2^40, + * p/P = 2^50, e/E = 2^60 + * Returns 0 on success, -1 on error (with errno set). + */ + +#include "compat.h" + +#include +#include +#include +#include + +#if !defined(__FreeBSD__) + +int +expand_number(const char *buf, uint64_t *num) +{ + char *endptr; + unsigned long long val; + uint64_t mult; + + errno = 0; + val = strtoull(buf, &endptr, 0); + if (buf == endptr || errno != 0) { + if (errno == 0) + errno = EINVAL; + return -1; + } + + mult = 1; + switch (*endptr) { + case 'e': case 'E': + mult = (uint64_t)1 << 60; + endptr++; + break; + case 'p': case 'P': + mult = (uint64_t)1 << 50; + endptr++; + break; + case 't': case 'T': + mult = (uint64_t)1 << 40; + endptr++; + break; + case 'g': case 'G': + mult = (uint64_t)1 << 30; + endptr++; + break; + case 'm': case 'M': + mult = (uint64_t)1 << 20; + endptr++; + break; + case 'k': case 'K': + mult = (uint64_t)1 << 10; + endptr++; + break; + case '\0': + break; + default: + errno = EINVAL; + return -1; + } + + if (*endptr != '\0') { + errno = EINVAL; + return -1; + } + + if (val > UINT64_MAX / mult) { + errno = ERANGE; + return -1; + } + + *num = val * mult; + return 0; +} + +#endif /* !__FreeBSD__ */ diff --git a/src/compat-headers/fgetln.c b/src/compat-headers/fgetln.c new file mode 100644 index 0000000..1c6c442 --- /dev/null +++ b/src/compat-headers/fgetln.c @@ -0,0 +1,46 @@ +/* + * fgetln.c - compatibility implementation for Linux. + * + * fgetln() is a BSD extension (OpenBSD, FreeBSD, macOS) that reads the + * next line from a stream and returns a pointer to an internal buffer. + * The caller must NOT free the returned pointer. The buffer is only + * valid until the next fgetln() call on the same stream or until the + * stream is closed. + * + * Semantics: + * char *fgetln(FILE *fp, size_t *lenp); + * + * - Returns a pointer to the next line, INCLUDING the trailing '\n' + * (if present; the last line of a file may lack one). + * - *lenp is set to the number of bytes in the returned line. + * - The returned buffer is NOT NUL-terminated by the original BSD + * contract, but our implementation NUL-terminates for safety. + * - Returns NULL on EOF or error. + * + * This implementation uses POSIX getline() internally. + */ + +#include "compat.h" + +#include +#include + +#ifndef __OpenBSD__ + +char * +fgetln(FILE *fp, size_t *lenp) +{ + static char *buf = NULL; + static size_t bufsz = 0; + ssize_t n; + + n = getline(&buf, &bufsz, fp); + if (n < 0) { + *lenp = 0; + return NULL; + } + *lenp = (size_t)n; + return buf; +} + +#endif /* !__OpenBSD__ */ diff --git a/src/compat-headers/reallocf.c b/src/compat-headers/reallocf.c new file mode 100644 index 0000000..c07d833 --- /dev/null +++ b/src/compat-headers/reallocf.c @@ -0,0 +1,28 @@ +/* + * reallocf.c - FreeBSD/OpenBSD reallocf() for Linux. + * + * reallocf(ptr, size) + * Identical to realloc(), except that on failure it frees the + * original allocation before returning NULL. This prevents + * memory leaks in the common pattern: + * p = realloc(p, newsize); // leaks old p on failure + */ + +#include "compat.h" + +#include + +#ifndef __OpenBSD__ + +void * +reallocf(void *ptr, size_t size) +{ + void *nptr; + + nptr = realloc(ptr, size); + if (nptr == NULL && size != 0) + free(ptr); + return nptr; +} + +#endif /* !__OpenBSD__ */ diff --git a/src/compat-headers/setmode.c b/src/compat-headers/setmode.c new file mode 100644 index 0000000..c37f9fb --- /dev/null +++ b/src/compat-headers/setmode.c @@ -0,0 +1,213 @@ +/* + * setmode / getmode - BSD file mode string parsing. + * + * Derived from FreeBSD's lib/libc/gen/setmode.c. + * SPDX-License-Identifier: BSD-3-Clause + * + * setmode() parses a symbolic mode string (e.g., "u+rw,g+r,o-w") + * and returns an opaque set. getmode() applies that set to a base mode. + */ +#include +#include +#include +#include +#include +#include + +#define SET_LEN 64 + +typedef struct { + mode_t bits; /* mode bits to set/clear */ + char cmd; /* '+', '-', '=' */ + char who; /* 'u', 'g', 'o', 'a' (mask selector) */ +} BITCMD; + +/* + * Given a symbolic mode string, compile into an array of BITCMD. + * Returns a malloc'd opaque pointer, or NULL on error. + */ +void * +setmode(const char *p) +{ + BITCMD *set, *saveset; + char op, who; + mode_t mask, bits; + int nsets; + + if (!p || *p == '\0') { + /* Numeric mode */ + if (p && *p >= '0' && *p <= '7') { + goto numeric; + } + errno = EINVAL; + return NULL; + } + + /* Check for numeric mode */ + if (*p >= '0' && *p <= '7') { +numeric:; + unsigned long val; + char *ep; + val = strtoul(p, &ep, 8); + if (*ep != '\0' || val > 07777) { + errno = EINVAL; + return NULL; + } + set = calloc(2, sizeof(BITCMD)); + if (set == NULL) + return NULL; + set[0].bits = (mode_t)val; + set[0].cmd = '='; + set[0].who = 'a'; + set[1].cmd = '\0'; + return (void *)set; + } + + set = calloc(SET_LEN, sizeof(BITCMD)); + if (set == NULL) + return NULL; + saveset = set; + nsets = 0; + + for (;;) { + /* Parse "who" part */ + who = 'a'; + mask = S_IRWXU | S_IRWXG | S_IRWXO | S_ISUID | S_ISGID | S_ISVTX; + for (;; ++p) { + switch (*p) { + case 'u': + who = 'u'; + mask = S_IRWXU | S_ISUID; + continue; + case 'g': + who = 'g'; + mask = S_IRWXG | S_ISGID; + continue; + case 'o': + who = 'o'; + mask = S_IRWXO | S_ISVTX; + continue; + case 'a': + who = 'a'; + mask = S_IRWXU | S_IRWXG | S_IRWXO | + S_ISUID | S_ISGID | S_ISVTX; + continue; + default: + break; + } + break; + } + + /* Parse operator */ + op = *p++; + if (op != '+' && op != '-' && op != '=') { + free(saveset); + errno = EINVAL; + return NULL; + } + + /* Parse permissions */ + bits = 0; + for (;; ++p) { + switch (*p) { + case 'r': + if (who == 'u' || who == 'a') + bits |= S_IRUSR; + if (who == 'g' || who == 'a') + bits |= S_IRGRP; + if (who == 'o' || who == 'a') + bits |= S_IROTH; + continue; + case 'w': + if (who == 'u' || who == 'a') + bits |= S_IWUSR; + if (who == 'g' || who == 'a') + bits |= S_IWGRP; + if (who == 'o' || who == 'a') + bits |= S_IWOTH; + continue; + case 'x': + if (who == 'u' || who == 'a') + bits |= S_IXUSR; + if (who == 'g' || who == 'a') + bits |= S_IXGRP; + if (who == 'o' || who == 'a') + bits |= S_IXOTH; + continue; + case 'X': + /* Execute if directory or any exec bit set */ + bits |= S_IXUSR | S_IXGRP | S_IXOTH; + continue; + case 's': + if (who == 'u' || who == 'a') + bits |= S_ISUID; + if (who == 'g' || who == 'a') + bits |= S_ISGID; + continue; + case 't': + bits |= S_ISVTX; + continue; + default: + break; + } + break; + } + + if (nsets >= SET_LEN - 1) { + free(saveset); + errno = ERANGE; + return NULL; + } + set->bits = bits & mask; + set->cmd = op; + set->who = who; + set++; + nsets++; + + if (*p == ',') { + p++; + continue; + } + if (*p == '\0') + break; + free(saveset); + errno = EINVAL; + return NULL; + } + + set->cmd = '\0'; + return (void *)saveset; +} + +mode_t +getmode(const void *bbox, mode_t omode) +{ + const BITCMD *set = (const BITCMD *)bbox; + mode_t newmode = omode; + + for (; set->cmd; set++) { + switch (set->cmd) { + case '+': + newmode |= set->bits; + break; + case '-': + newmode &= ~set->bits; + break; + case '=': + if (set->who == 'a') { + newmode = set->bits; + } else { + mode_t mask; + switch (set->who) { + case 'u': mask = S_IRWXU | S_ISUID; break; + case 'g': mask = S_IRWXG | S_ISGID; break; + case 'o': mask = S_IRWXO | S_ISVTX; break; + default: mask = ~(mode_t)0; break; + } + newmode = (newmode & ~mask) | (set->bits & mask); + } + break; + } + } + return newmode; +} diff --git a/src/compat-headers/strlcpy.c b/src/compat-headers/strlcpy.c new file mode 100644 index 0000000..79f6210 --- /dev/null +++ b/src/compat-headers/strlcpy.c @@ -0,0 +1,81 @@ +/* + * strlcpy.c / strlcat.c - OpenBSD-compatible bounded string operations. + * + * Origin: Todd C. Miller's canonical implementation, placed in the + * public domain. These are the reference implementations used by + * OpenBSD, FreeBSD, and many other projects. + * + * --- + * + * strlcpy(dst, src, dsize) + * Copy string src to buffer dst of size dsize. At most dsize-1 chars + * will be copied; dst is always NUL-terminated (unless dsize == 0). + * Returns strlen(src) -- if retval >= dsize, truncation occurred. + * + * strlcat(dst, src, dsize) + * Appends string src to buffer dst of size dsize (NUL-terminated). + * Appends at most dsize - strlen(dst) - 1 chars, NUL-terminating + * the result. Returns strlen(src) + MIN(dsize, strlen(dst)) -- if + * retval >= dsize, truncation occurred. + */ + +#include "compat.h" + +#ifdef VX_NEED_STRLCPY + +#include + +size_t +strlcpy(char *dst, const char *src, size_t dsize) +{ + const char *osrc = src; + size_t nleft = dsize; + + /* Copy as many bytes as will fit. */ + if (nleft != 0) { + while (--nleft != 0) { + if ((*dst++ = *src++) == '\0') + break; + } + } + + /* Not enough room -- NUL-terminate and traverse rest of src. */ + if (nleft == 0) { + if (dsize != 0) + *dst = '\0'; /* NUL-terminate dst */ + while (*src++) + ; + } + + return (size_t)(src - osrc - 1); /* count excludes NUL */ +} + +size_t +strlcat(char *dst, const char *src, size_t dsize) +{ + const char *odst = dst; + const char *osrc = src; + size_t n = dsize; + size_t dlen; + + /* Find the end of dst and adjust bytes left but don't go past end. */ + while (n-- != 0 && *dst != '\0') + dst++; + dlen = (size_t)(dst - odst); + n = dsize - dlen; + + if (n-- == 0) + return dlen + strlen(src); + while (*src != '\0') { + if (n != 0) { + *dst++ = *src; + n--; + } + src++; + } + *dst = '\0'; + + return dlen + (size_t)(src - osrc); /* count excludes NUL */ +} + +#endif /* VX_NEED_STRLCPY */ diff --git a/src/compat-headers/strmode.c b/src/compat-headers/strmode.c new file mode 100644 index 0000000..13754e9 --- /dev/null +++ b/src/compat-headers/strmode.c @@ -0,0 +1,53 @@ +/* + * strmode - convert file mode to ls-style string. + * + * Derived from FreeBSD's lib/libc/string/strmode.c. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include +#include +#include + +void +strmode(mode_t mode, char *p) +{ + /* file type */ + switch (mode & S_IFMT) { + case S_IFDIR: *p++ = 'd'; break; + case S_IFCHR: *p++ = 'c'; break; + case S_IFBLK: *p++ = 'b'; break; + case S_IFREG: *p++ = '-'; break; + case S_IFLNK: *p++ = 'l'; break; + case S_IFSOCK: *p++ = 's'; break; +#ifdef S_IFIFO + case S_IFIFO: *p++ = 'p'; break; +#endif +#ifdef S_IFWHT + case S_IFWHT: *p++ = 'w'; break; +#endif + default: *p++ = '?'; break; + } + /* owner */ + *p++ = (mode & S_IRUSR) ? 'r' : '-'; + *p++ = (mode & S_IWUSR) ? 'w' : '-'; + if (mode & S_ISUID) + *p++ = (mode & S_IXUSR) ? 's' : 'S'; + else + *p++ = (mode & S_IXUSR) ? 'x' : '-'; + /* group */ + *p++ = (mode & S_IRGRP) ? 'r' : '-'; + *p++ = (mode & S_IWGRP) ? 'w' : '-'; + if (mode & S_ISGID) + *p++ = (mode & S_IXGRP) ? 's' : 'S'; + else + *p++ = (mode & S_IXGRP) ? 'x' : '-'; + /* other */ + *p++ = (mode & S_IROTH) ? 'r' : '-'; + *p++ = (mode & S_IWOTH) ? 'w' : '-'; + if (mode & S_ISVTX) + *p++ = (mode & S_IXOTH) ? 't' : 'T'; + else + *p++ = (mode & S_IXOTH) ? 'x' : '-'; + *p++ = ' '; + *p = '\0'; +} diff --git a/src/compat-headers/strtonum.c b/src/compat-headers/strtonum.c new file mode 100644 index 0000000..748ea76 --- /dev/null +++ b/src/compat-headers/strtonum.c @@ -0,0 +1,67 @@ +/* + * strtonum.c - OpenBSD strtonum() for Linux. + * + * strtonum(nptr, minval, maxval, errstrp) + * Converts the string nptr to a long long integer in [minval, maxval]. + * + * On success, *errstrp is set to NULL and the converted value is + * returned. + * + * On failure, 0 is returned and *errstrp points to one of: + * "invalid" -- the string is not a valid number + * "too small" -- the value is less than minval + * "too large" -- the value is greater than maxval + * + * Unlike strtol, strtonum rejects trailing garbage, leading + * whitespace past the first char, octal, and hex prefixes. + * + * Based on the OpenBSD reference implementation (ISC license). + */ + +#include "compat.h" + +#include +#include +#include + +#ifndef __OpenBSD__ + +long long +strtonum(const char *nptr, long long minval, long long maxval, + const char **errstrp) +{ + long long ll; + char *ep; + int saved_errno = errno; + + if (minval > maxval) { + if (errstrp != NULL) + *errstrp = "invalid"; + errno = EINVAL; + return 0; + } + + errno = 0; + ll = strtoll(nptr, &ep, 10); + if (nptr == ep || *ep != '\0') { + if (errstrp != NULL) + *errstrp = "invalid"; + errno = EINVAL; + return 0; + } + if ((errno == ERANGE && (ll == LLONG_MIN || ll == LLONG_MAX)) || + ll < minval || ll > maxval) { + if (errstrp != NULL) + *errstrp = (ll < minval) ? "too small" : "too large"; + if (errno != ERANGE) + errno = ERANGE; + return 0; + } + + if (errstrp != NULL) + *errstrp = NULL; + errno = saved_errno; + return ll; +} + +#endif /* !__OpenBSD__ */ diff --git a/src/compat-headers/sys/tree.h b/src/compat-headers/sys/tree.h new file mode 100644 index 0000000..6043424 --- /dev/null +++ b/src/compat-headers/sys/tree.h @@ -0,0 +1,1069 @@ +/* $NetBSD: tree.h,v 1.8 2004/03/28 19:38:30 provos Exp $ */ +/* $OpenBSD: tree.h,v 1.7 2002/10/17 21:51:54 art Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright 2002 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. + */ + +#ifndef _SYS_TREE_H_ +#define _SYS_TREE_H_ + +#include + +/* + * Linux does not define __uintptr_t. Use uintptr_t from . + */ +#ifndef __uintptr_t +#define __uintptr_t uintptr_t +#endif + +/* + * This file defines data structures for different types of trees: + * splay trees and rank-balanced trees. + * + * A splay tree is a self-organizing data structure. Every operation + * on the tree causes a splay to happen. The splay moves the requested + * node to the root of the tree and partly rebalances it. + * + * This has the benefit that request locality causes faster lookups as + * the requested nodes move to the top of the tree. On the other hand, + * every lookup causes memory writes. + * + * The Balance Theorem bounds the total access time for m operations + * and n inserts on an initially empty tree as O((m + n)lg n). The + * amortized cost for a sequence of m accesses to a splay tree is O(lg n); + * + * A rank-balanced tree is a binary search tree with an integer + * rank-difference as an attribute of each pointer from parent to child. + * The sum of the rank-differences on any path from a node down to null is + * the same, and defines the rank of that node. The rank of the null node + * is -1. + * + * Different additional conditions define different sorts of balanced trees, + * including "red-black" and "AVL" trees. The set of conditions applied here + * are the "weak-AVL" conditions of Haeupler, Sen and Tarjan presented in in + * "Rank Balanced Trees", ACM Transactions on Algorithms Volume 11 Issue 4 June + * 2015 Article No.: 30pp 1–26 https://doi.org/10.1145/2689412 (the HST paper): + * - every rank-difference is 1 or 2. + * - the rank of any leaf is 1. + * + * For historical reasons, rank differences that are even are associated + * with the color red (Rank-Even-Difference), and the child that a red edge + * points to is called a red child. + * + * Every operation on a rank-balanced tree is bounded as O(lg n). + * The maximum height of a rank-balanced tree is 2lg (n+1). + */ + +#define SPLAY_HEAD(name, type) \ +struct name { \ + struct type *sph_root; /* root of the tree */ \ +} + +#define SPLAY_INITIALIZER(root) \ + { NULL } + +#define SPLAY_INIT(root) do { \ + (root)->sph_root = NULL; \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_ENTRY(type) \ +struct { \ + struct type *spe_left; /* left element */ \ + struct type *spe_right; /* right element */ \ +} + +#define SPLAY_LEFT(elm, field) (elm)->field.spe_left +#define SPLAY_RIGHT(elm, field) (elm)->field.spe_right +#define SPLAY_ROOT(head) (head)->sph_root +#define SPLAY_EMPTY(head) (SPLAY_ROOT(head) == NULL) + +/* SPLAY_ROTATE_{LEFT,RIGHT} expect that tmp hold SPLAY_{RIGHT,LEFT} */ +#define SPLAY_ROTATE_RIGHT(head, tmp, field) do { \ + SPLAY_LEFT((head)->sph_root, field) = SPLAY_RIGHT(tmp, field); \ + SPLAY_RIGHT(tmp, field) = (head)->sph_root; \ + (head)->sph_root = tmp; \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_ROTATE_LEFT(head, tmp, field) do { \ + SPLAY_RIGHT((head)->sph_root, field) = SPLAY_LEFT(tmp, field); \ + SPLAY_LEFT(tmp, field) = (head)->sph_root; \ + (head)->sph_root = tmp; \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_LINKLEFT(head, tmp, field) do { \ + SPLAY_LEFT(tmp, field) = (head)->sph_root; \ + tmp = (head)->sph_root; \ + (head)->sph_root = SPLAY_LEFT((head)->sph_root, field); \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_LINKRIGHT(head, tmp, field) do { \ + SPLAY_RIGHT(tmp, field) = (head)->sph_root; \ + tmp = (head)->sph_root; \ + (head)->sph_root = SPLAY_RIGHT((head)->sph_root, field); \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_ASSEMBLE(head, node, left, right, field) do { \ + SPLAY_RIGHT(left, field) = SPLAY_LEFT((head)->sph_root, field); \ + SPLAY_LEFT(right, field) = SPLAY_RIGHT((head)->sph_root, field);\ + SPLAY_LEFT((head)->sph_root, field) = SPLAY_RIGHT(node, field); \ + SPLAY_RIGHT((head)->sph_root, field) = SPLAY_LEFT(node, field); \ +} while (/*CONSTCOND*/ 0) + +/* Generates prototypes and inline functions */ + +#define SPLAY_PROTOTYPE(name, type, field, cmp) \ +void name##_SPLAY(struct name *, struct type *); \ +void name##_SPLAY_MINMAX(struct name *, int); \ +struct type *name##_SPLAY_INSERT(struct name *, struct type *); \ +struct type *name##_SPLAY_REMOVE(struct name *, struct type *); \ + \ +/* Finds the node with the same key as elm */ \ +static __unused __inline struct type * \ +name##_SPLAY_FIND(struct name *head, struct type *elm) \ +{ \ + if (SPLAY_EMPTY(head)) \ + return(NULL); \ + name##_SPLAY(head, elm); \ + if ((cmp)(elm, (head)->sph_root) == 0) \ + return (head->sph_root); \ + return (NULL); \ +} \ + \ +static __unused __inline struct type * \ +name##_SPLAY_NEXT(struct name *head, struct type *elm) \ +{ \ + name##_SPLAY(head, elm); \ + if (SPLAY_RIGHT(elm, field) != NULL) { \ + elm = SPLAY_RIGHT(elm, field); \ + while (SPLAY_LEFT(elm, field) != NULL) { \ + elm = SPLAY_LEFT(elm, field); \ + } \ + } else \ + elm = NULL; \ + return (elm); \ +} \ + \ +static __unused __inline struct type * \ +name##_SPLAY_MIN_MAX(struct name *head, int val) \ +{ \ + name##_SPLAY_MINMAX(head, val); \ + return (SPLAY_ROOT(head)); \ +} + +/* Main splay operation. + * Moves node close to the key of elm to top + */ +#define SPLAY_GENERATE(name, type, field, cmp) \ +struct type * \ +name##_SPLAY_INSERT(struct name *head, struct type *elm) \ +{ \ + if (SPLAY_EMPTY(head)) { \ + SPLAY_LEFT(elm, field) = SPLAY_RIGHT(elm, field) = NULL; \ + } else { \ + __typeof(cmp(NULL, NULL)) __comp; \ + name##_SPLAY(head, elm); \ + __comp = (cmp)(elm, (head)->sph_root); \ + if (__comp < 0) { \ + SPLAY_LEFT(elm, field) = SPLAY_LEFT((head)->sph_root, field);\ + SPLAY_RIGHT(elm, field) = (head)->sph_root; \ + SPLAY_LEFT((head)->sph_root, field) = NULL; \ + } else if (__comp > 0) { \ + SPLAY_RIGHT(elm, field) = SPLAY_RIGHT((head)->sph_root, field);\ + SPLAY_LEFT(elm, field) = (head)->sph_root; \ + SPLAY_RIGHT((head)->sph_root, field) = NULL; \ + } else \ + return ((head)->sph_root); \ + } \ + (head)->sph_root = (elm); \ + return (NULL); \ +} \ + \ +struct type * \ +name##_SPLAY_REMOVE(struct name *head, struct type *elm) \ +{ \ + struct type *__tmp; \ + if (SPLAY_EMPTY(head)) \ + return (NULL); \ + name##_SPLAY(head, elm); \ + if ((cmp)(elm, (head)->sph_root) == 0) { \ + if (SPLAY_LEFT((head)->sph_root, field) == NULL) { \ + (head)->sph_root = SPLAY_RIGHT((head)->sph_root, field);\ + } else { \ + __tmp = SPLAY_RIGHT((head)->sph_root, field); \ + (head)->sph_root = SPLAY_LEFT((head)->sph_root, field);\ + name##_SPLAY(head, elm); \ + SPLAY_RIGHT((head)->sph_root, field) = __tmp; \ + } \ + return (elm); \ + } \ + return (NULL); \ +} \ + \ +void \ +name##_SPLAY(struct name *head, struct type *elm) \ +{ \ + struct type __node, *__left, *__right, *__tmp; \ + __typeof(cmp(NULL, NULL)) __comp; \ +\ + SPLAY_LEFT(&__node, field) = SPLAY_RIGHT(&__node, field) = NULL;\ + __left = __right = &__node; \ +\ + while ((__comp = (cmp)(elm, (head)->sph_root)) != 0) { \ + if (__comp < 0) { \ + __tmp = SPLAY_LEFT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if ((cmp)(elm, __tmp) < 0){ \ + SPLAY_ROTATE_RIGHT(head, __tmp, field); \ + if (SPLAY_LEFT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKLEFT(head, __right, field); \ + } else if (__comp > 0) { \ + __tmp = SPLAY_RIGHT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if ((cmp)(elm, __tmp) > 0){ \ + SPLAY_ROTATE_LEFT(head, __tmp, field); \ + if (SPLAY_RIGHT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKRIGHT(head, __left, field); \ + } \ + } \ + SPLAY_ASSEMBLE(head, &__node, __left, __right, field); \ +} \ + \ +/* Splay with either the minimum or the maximum element \ + * Used to find minimum or maximum element in tree. \ + */ \ +void name##_SPLAY_MINMAX(struct name *head, int __comp) \ +{ \ + struct type __node, *__left, *__right, *__tmp; \ +\ + SPLAY_LEFT(&__node, field) = SPLAY_RIGHT(&__node, field) = NULL;\ + __left = __right = &__node; \ +\ + while (1) { \ + if (__comp < 0) { \ + __tmp = SPLAY_LEFT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if (__comp < 0){ \ + SPLAY_ROTATE_RIGHT(head, __tmp, field); \ + if (SPLAY_LEFT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKLEFT(head, __right, field); \ + } else if (__comp > 0) { \ + __tmp = SPLAY_RIGHT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if (__comp > 0) { \ + SPLAY_ROTATE_LEFT(head, __tmp, field); \ + if (SPLAY_RIGHT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKRIGHT(head, __left, field); \ + } \ + } \ + SPLAY_ASSEMBLE(head, &__node, __left, __right, field); \ +} + +#define SPLAY_NEGINF -1 +#define SPLAY_INF 1 + +#define SPLAY_INSERT(name, x, y) name##_SPLAY_INSERT(x, y) +#define SPLAY_REMOVE(name, x, y) name##_SPLAY_REMOVE(x, y) +#define SPLAY_FIND(name, x, y) name##_SPLAY_FIND(x, y) +#define SPLAY_NEXT(name, x, y) name##_SPLAY_NEXT(x, y) +#define SPLAY_MIN(name, x) (SPLAY_EMPTY(x) ? NULL \ + : name##_SPLAY_MIN_MAX(x, SPLAY_NEGINF)) +#define SPLAY_MAX(name, x) (SPLAY_EMPTY(x) ? NULL \ + : name##_SPLAY_MIN_MAX(x, SPLAY_INF)) + +#define SPLAY_FOREACH(x, name, head) \ + for ((x) = SPLAY_MIN(name, head); \ + (x) != NULL; \ + (x) = SPLAY_NEXT(name, head, x)) + +/* Macros that define a rank-balanced tree */ +#define RB_HEAD(name, type) \ +struct name { \ + struct type *rbh_root; /* root of the tree */ \ +} + +#define RB_INITIALIZER(root) \ + { NULL } + +#define RB_INIT(root) do { \ + (root)->rbh_root = NULL; \ +} while (/*CONSTCOND*/ 0) + +#define RB_ENTRY(type) \ +struct { \ + struct type *rbe_link[3]; \ +} + +/* + * With the expectation that any object of struct type has an + * address that is a multiple of 4, and that therefore the + * 2 least significant bits of a pointer to struct type are + * always zero, this implementation sets those bits to indicate + * that the left or right child of the tree node is "red". + */ +#define _RB_LINK(elm, dir, field) (elm)->field.rbe_link[dir] +#define _RB_UP(elm, field) _RB_LINK(elm, 0, field) +#define _RB_L ((__uintptr_t)1) +#define _RB_R ((__uintptr_t)2) +#define _RB_LR ((__uintptr_t)3) +#define _RB_BITS(elm) (*(__uintptr_t *)&elm) +#define _RB_BITSUP(elm, field) _RB_BITS(_RB_UP(elm, field)) +#define _RB_PTR(elm) (__typeof(elm)) \ + ((__uintptr_t)elm & ~_RB_LR) + +#define RB_PARENT(elm, field) _RB_PTR(_RB_UP(elm, field)) +#define RB_LEFT(elm, field) _RB_LINK(elm, _RB_L, field) +#define RB_RIGHT(elm, field) _RB_LINK(elm, _RB_R, field) +#define RB_ROOT(head) (head)->rbh_root +#define RB_EMPTY(head) (RB_ROOT(head) == NULL) + +#define RB_SET_PARENT(dst, src, field) do { \ + _RB_BITSUP(dst, field) = (__uintptr_t)src | \ + (_RB_BITSUP(dst, field) & _RB_LR); \ +} while (/*CONSTCOND*/ 0) + +#define RB_SET(elm, parent, field) do { \ + _RB_UP(elm, field) = parent; \ + RB_LEFT(elm, field) = RB_RIGHT(elm, field) = NULL; \ +} while (/*CONSTCOND*/ 0) + +/* + * Either RB_AUGMENT or RB_AUGMENT_CHECK is invoked in a loop at the root of + * every modified subtree, from the bottom up to the root, to update augmented + * node data. RB_AUGMENT_CHECK returns true only when the update changes the + * node data, so that updating can be stopped short of the root when it returns + * false. + */ +#ifndef RB_AUGMENT_CHECK +#ifndef RB_AUGMENT +#define RB_AUGMENT_CHECK(x) 0 +#else +#define RB_AUGMENT_CHECK(x) (RB_AUGMENT(x), 1) +#endif +#endif + +#define RB_UPDATE_AUGMENT(elm, field) do { \ + __typeof(elm) rb_update_tmp = (elm); \ + while (RB_AUGMENT_CHECK(rb_update_tmp) && \ + (rb_update_tmp = RB_PARENT(rb_update_tmp, field)) != NULL) \ + ; \ +} while (0) + +#define RB_SWAP_CHILD(head, par, out, in, field) do { \ + if (par == NULL) \ + RB_ROOT(head) = (in); \ + else if ((out) == RB_LEFT(par, field)) \ + RB_LEFT(par, field) = (in); \ + else \ + RB_RIGHT(par, field) = (in); \ +} while (/*CONSTCOND*/ 0) + +/* + * RB_ROTATE macro partially restructures the tree to improve balance. In the + * case when dir is _RB_L, tmp is a right child of elm. After rotation, elm + * is a left child of tmp, and the subtree that represented the items between + * them, which formerly hung to the left of tmp now hangs to the right of elm. + * The parent-child relationship between elm and its former parent is not + * changed; where this macro once updated those fields, that is now left to the + * caller of RB_ROTATE to clean up, so that a pair of rotations does not twice + * update the same pair of pointer fields with distinct values. + */ +#define RB_ROTATE(elm, tmp, dir, field) do { \ + if ((_RB_LINK(elm, dir ^ _RB_LR, field) = \ + _RB_LINK(tmp, dir, field)) != NULL) \ + RB_SET_PARENT(_RB_LINK(tmp, dir, field), elm, field); \ + _RB_LINK(tmp, dir, field) = (elm); \ + RB_SET_PARENT(elm, tmp, field); \ +} while (/*CONSTCOND*/ 0) + +/* Generates prototypes and inline functions */ +#define RB_PROTOTYPE(name, type, field, cmp) \ + RB_PROTOTYPE_INTERNAL(name, type, field, cmp,) +#define RB_PROTOTYPE_STATIC(name, type, field, cmp) \ + RB_PROTOTYPE_INTERNAL(name, type, field, cmp, __unused static) +#define RB_PROTOTYPE_INTERNAL(name, type, field, cmp, attr) \ + RB_PROTOTYPE_RANK(name, type, attr) \ + RB_PROTOTYPE_INSERT_COLOR(name, type, attr); \ + RB_PROTOTYPE_REMOVE_COLOR(name, type, attr); \ + RB_PROTOTYPE_INSERT_FINISH(name, type, attr); \ + RB_PROTOTYPE_INSERT(name, type, attr); \ + RB_PROTOTYPE_REMOVE(name, type, attr); \ + RB_PROTOTYPE_FIND(name, type, attr); \ + RB_PROTOTYPE_NFIND(name, type, attr); \ + RB_PROTOTYPE_NEXT(name, type, attr); \ + RB_PROTOTYPE_INSERT_NEXT(name, type, attr); \ + RB_PROTOTYPE_PREV(name, type, attr); \ + RB_PROTOTYPE_INSERT_PREV(name, type, attr); \ + RB_PROTOTYPE_MINMAX(name, type, attr); \ + RB_PROTOTYPE_REINSERT(name, type, attr); +#ifdef _RB_DIAGNOSTIC +#define RB_PROTOTYPE_RANK(name, type, attr) \ + attr int name##_RB_RANK(struct type *); +#else +#define RB_PROTOTYPE_RANK(name, type, attr) +#endif +#define RB_PROTOTYPE_INSERT_COLOR(name, type, attr) \ + attr struct type *name##_RB_INSERT_COLOR(struct name *, \ + struct type *, struct type *) +#define RB_PROTOTYPE_REMOVE_COLOR(name, type, attr) \ + attr struct type *name##_RB_REMOVE_COLOR(struct name *, \ + struct type *, struct type *) +#define RB_PROTOTYPE_REMOVE(name, type, attr) \ + attr struct type *name##_RB_REMOVE(struct name *, struct type *) +#define RB_PROTOTYPE_INSERT_FINISH(name, type, attr) \ + attr struct type *name##_RB_INSERT_FINISH(struct name *, \ + struct type *, struct type **, struct type *) +#define RB_PROTOTYPE_INSERT(name, type, attr) \ + attr struct type *name##_RB_INSERT(struct name *, struct type *) +#define RB_PROTOTYPE_FIND(name, type, attr) \ + attr struct type *name##_RB_FIND(struct name *, struct type *) +#define RB_PROTOTYPE_NFIND(name, type, attr) \ + attr struct type *name##_RB_NFIND(struct name *, struct type *) +#define RB_PROTOTYPE_NEXT(name, type, attr) \ + attr struct type *name##_RB_NEXT(struct type *) +#define RB_PROTOTYPE_INSERT_NEXT(name, type, attr) \ + attr struct type *name##_RB_INSERT_NEXT(struct name *, \ + struct type *, struct type *) +#define RB_PROTOTYPE_PREV(name, type, attr) \ + attr struct type *name##_RB_PREV(struct type *) +#define RB_PROTOTYPE_INSERT_PREV(name, type, attr) \ + attr struct type *name##_RB_INSERT_PREV(struct name *, \ + struct type *, struct type *) +#define RB_PROTOTYPE_MINMAX(name, type, attr) \ + attr struct type *name##_RB_MINMAX(struct name *, int) +#define RB_PROTOTYPE_REINSERT(name, type, attr) \ + attr struct type *name##_RB_REINSERT(struct name *, struct type *) + +/* Main rb operation. + * Moves node close to the key of elm to top + */ +#define RB_GENERATE(name, type, field, cmp) \ + RB_GENERATE_INTERNAL(name, type, field, cmp,) +#define RB_GENERATE_STATIC(name, type, field, cmp) \ + RB_GENERATE_INTERNAL(name, type, field, cmp, __unused static) +#define RB_GENERATE_INTERNAL(name, type, field, cmp, attr) \ + RB_GENERATE_RANK(name, type, field, attr) \ + RB_GENERATE_INSERT_COLOR(name, type, field, attr) \ + RB_GENERATE_REMOVE_COLOR(name, type, field, attr) \ + RB_GENERATE_INSERT_FINISH(name, type, field, attr) \ + RB_GENERATE_INSERT(name, type, field, cmp, attr) \ + RB_GENERATE_REMOVE(name, type, field, attr) \ + RB_GENERATE_FIND(name, type, field, cmp, attr) \ + RB_GENERATE_NFIND(name, type, field, cmp, attr) \ + RB_GENERATE_NEXT(name, type, field, attr) \ + RB_GENERATE_INSERT_NEXT(name, type, field, cmp, attr) \ + RB_GENERATE_PREV(name, type, field, attr) \ + RB_GENERATE_INSERT_PREV(name, type, field, cmp, attr) \ + RB_GENERATE_MINMAX(name, type, field, attr) \ + RB_GENERATE_REINSERT(name, type, field, cmp, attr) + +#ifdef _RB_DIAGNOSTIC +#ifndef RB_AUGMENT +#define _RB_AUGMENT_VERIFY(x) RB_AUGMENT_CHECK(x) +#else +#define _RB_AUGMENT_VERIFY(x) 0 +#endif +#define RB_GENERATE_RANK(name, type, field, attr) \ +/* \ + * Return the rank of the subtree rooted at elm, or -1 if the subtree \ + * is not rank-balanced, or has inconsistent augmentation data. + */ \ +attr int \ +name##_RB_RANK(struct type *elm) \ +{ \ + struct type *left, *right, *up; \ + int left_rank, right_rank; \ + \ + if (elm == NULL) \ + return (0); \ + up = _RB_UP(elm, field); \ + left = RB_LEFT(elm, field); \ + left_rank = ((_RB_BITS(up) & _RB_L) ? 2 : 1) + \ + name##_RB_RANK(left); \ + right = RB_RIGHT(elm, field); \ + right_rank = ((_RB_BITS(up) & _RB_R) ? 2 : 1) + \ + name##_RB_RANK(right); \ + if (left_rank != right_rank || \ + (left_rank == 2 && left == NULL && right == NULL) || \ + _RB_AUGMENT_VERIFY(elm)) \ + return (-1); \ + return (left_rank); \ +} +#else +#define RB_GENERATE_RANK(name, type, field, attr) +#endif + +#define RB_GENERATE_INSERT_COLOR(name, type, field, attr) \ +attr struct type * \ +name##_RB_INSERT_COLOR(struct name *head, \ + struct type *parent, struct type *elm) \ +{ \ + /* \ + * Initially, elm is a leaf. Either its parent was previously \ + * a leaf, with two black null children, or an interior node \ + * with a black non-null child and a red null child. The \ + * balance criterion "the rank of any leaf is 1" precludes the \ + * possibility of two red null children for the initial parent. \ + * So the first loop iteration cannot lead to accessing an \ + * uninitialized 'child', and a later iteration can only happen \ + * when a value has been assigned to 'child' in the previous \ + * one. \ + */ \ + struct type *child, *child_up, *gpar; \ + __uintptr_t elmdir, sibdir; \ + \ + do { \ + /* the rank of the tree rooted at elm grew */ \ + gpar = _RB_UP(parent, field); \ + elmdir = RB_RIGHT(parent, field) == elm ? _RB_R : _RB_L; \ + if (_RB_BITS(gpar) & elmdir) { \ + /* shorten the parent-elm edge to rebalance */ \ + _RB_BITSUP(parent, field) ^= elmdir; \ + return (NULL); \ + } \ + sibdir = elmdir ^ _RB_LR; \ + /* the other edge must change length */ \ + _RB_BITSUP(parent, field) ^= sibdir; \ + if ((_RB_BITS(gpar) & _RB_LR) == 0) { \ + /* both edges now short, retry from parent */ \ + child = elm; \ + elm = parent; \ + continue; \ + } \ + _RB_UP(parent, field) = gpar = _RB_PTR(gpar); \ + if (_RB_BITSUP(elm, field) & elmdir) { \ + /* \ + * Exactly one of the edges descending from elm \ + * is long. The long one is in the same \ + * direction as the edge from parent to elm, \ + * so change that by rotation. The edge from \ + * parent to z was shortened above. Shorten \ + * the long edge down from elm, and adjust \ + * other edge lengths based on the downward \ + * edges from 'child'. \ + * \ + * par par \ + * / \ / \ \ + * elm z / z \ + * / \ child \ + * / child / \ \ + * / / \ elm \ \ + * w / \ / \ y \ + * x y w \ \ + * x \ + */ \ + RB_ROTATE(elm, child, elmdir, field); \ + child_up = _RB_UP(child, field); \ + if (_RB_BITS(child_up) & sibdir) \ + _RB_BITSUP(parent, field) ^= elmdir; \ + if (_RB_BITS(child_up) & elmdir) \ + _RB_BITSUP(elm, field) ^= _RB_LR; \ + else \ + _RB_BITSUP(elm, field) ^= elmdir; \ + /* if child is a leaf, don't augment elm, \ + * since it is restored to be a leaf again. */ \ + if ((_RB_BITS(child_up) & _RB_LR) == 0) \ + elm = child; \ + } else \ + child = elm; \ + \ + /* \ + * The long edge descending from 'child' points back \ + * in the direction of 'parent'. Rotate to make \ + * 'parent' a child of 'child', then make both edges \ + * of 'child' short to rebalance. \ + * \ + * par child \ + * / \ / \ \ + * / z x par \ + * child / \ \ + * / \ / z \ + * x \ y \ + * y \ + */ \ + RB_ROTATE(parent, child, sibdir, field); \ + _RB_UP(child, field) = gpar; \ + RB_SWAP_CHILD(head, gpar, parent, child, field); \ + /* \ + * Elements rotated down have new, smaller subtrees, \ + * so update augmentation for them. \ + */ \ + if (elm != child) \ + (void)RB_AUGMENT_CHECK(elm); \ + (void)RB_AUGMENT_CHECK(parent); \ + return (child); \ + } while ((parent = gpar) != NULL); \ + return (NULL); \ +} + +#ifndef RB_STRICT_HST +/* + * In REMOVE_COLOR, the HST paper, in figure 3, in the single-rotate case, has + * 'parent' with one higher rank, and then reduces its rank if 'parent' has + * become a leaf. This implementation always has the parent in its new position + * with lower rank, to avoid the leaf check. Define RB_STRICT_HST to 1 to get + * the behavior that HST describes. + */ +#define RB_STRICT_HST 0 +#endif + +#define RB_GENERATE_REMOVE_COLOR(name, type, field, attr) \ +attr struct type * \ +name##_RB_REMOVE_COLOR(struct name *head, \ + struct type *parent, struct type *elm) \ +{ \ + struct type *gpar, *sib, *up; \ + __uintptr_t elmdir, sibdir; \ + \ + if (RB_RIGHT(parent, field) == elm && \ + RB_LEFT(parent, field) == elm) { \ + /* Deleting a leaf that is an only-child creates a \ + * rank-2 leaf. Demote that leaf. */ \ + _RB_UP(parent, field) = _RB_PTR(_RB_UP(parent, field)); \ + elm = parent; \ + if ((parent = _RB_UP(elm, field)) == NULL) \ + return (NULL); \ + } \ + do { \ + /* the rank of the tree rooted at elm shrank */ \ + gpar = _RB_UP(parent, field); \ + elmdir = RB_RIGHT(parent, field) == elm ? _RB_R : _RB_L; \ + _RB_BITS(gpar) ^= elmdir; \ + if (_RB_BITS(gpar) & elmdir) { \ + /* lengthen the parent-elm edge to rebalance */ \ + _RB_UP(parent, field) = gpar; \ + return (NULL); \ + } \ + if (_RB_BITS(gpar) & _RB_LR) { \ + /* shorten other edge, retry from parent */ \ + _RB_BITS(gpar) ^= _RB_LR; \ + _RB_UP(parent, field) = gpar; \ + gpar = _RB_PTR(gpar); \ + continue; \ + } \ + sibdir = elmdir ^ _RB_LR; \ + sib = _RB_LINK(parent, sibdir, field); \ + up = _RB_UP(sib, field); \ + _RB_BITS(up) ^= _RB_LR; \ + if ((_RB_BITS(up) & _RB_LR) == 0) { \ + /* shorten edges descending from sib, retry */ \ + _RB_UP(sib, field) = up; \ + continue; \ + } \ + if ((_RB_BITS(up) & sibdir) == 0) { \ + /* \ + * The edge descending from 'sib' away from \ + * 'parent' is long. The short edge descending \ + * from 'sib' toward 'parent' points to 'elm*' \ + * Rotate to make 'sib' a child of 'elm*' \ + * then adjust the lengths of the edges \ + * descending from 'sib' and 'elm*'. \ + * \ + * par par \ + * / \ / \ \ + * / sib elm \ \ + * / / \ elm* \ + * elm elm* \ / \ \ + * / \ \ / \ \ + * / \ z / \ \ + * x y x sib \ + * / \ \ + * / z \ + * y \ + */ \ + elm = _RB_LINK(sib, elmdir, field); \ + /* elm is a 1-child. First rotate at elm. */ \ + RB_ROTATE(sib, elm, sibdir, field); \ + up = _RB_UP(elm, field); \ + _RB_BITSUP(parent, field) ^= \ + (_RB_BITS(up) & elmdir) ? _RB_LR : elmdir; \ + _RB_BITSUP(sib, field) ^= \ + (_RB_BITS(up) & sibdir) ? _RB_LR : sibdir; \ + _RB_BITSUP(elm, field) |= _RB_LR; \ + } else { \ + if ((_RB_BITS(up) & elmdir) == 0 && \ + RB_STRICT_HST && elm != NULL) { \ + /* if parent does not become a leaf, \ + do not demote parent yet. */ \ + _RB_BITSUP(parent, field) ^= sibdir; \ + _RB_BITSUP(sib, field) ^= _RB_LR; \ + } else if ((_RB_BITS(up) & elmdir) == 0) { \ + /* demote parent. */ \ + _RB_BITSUP(parent, field) ^= elmdir; \ + _RB_BITSUP(sib, field) ^= sibdir; \ + } else \ + _RB_BITSUP(sib, field) ^= sibdir; \ + elm = sib; \ + } \ + \ + /* \ + * The edge descending from 'elm' away from 'parent' \ + * is short. Rotate to make 'parent' a child of 'elm', \ + * then lengthen the short edges descending from \ + * 'parent' and 'elm' to rebalance. \ + * \ + * par elm \ + * / \ / \ \ + * e \ / \ \ + * elm / \ \ + * / \ par s \ + * / \ / \ \ + * / \ e \ \ + * x s x \ + */ \ + RB_ROTATE(parent, elm, elmdir, field); \ + RB_SET_PARENT(elm, gpar, field); \ + RB_SWAP_CHILD(head, gpar, parent, elm, field); \ + /* \ + * An element rotated down, but not into the search \ + * path has a new, smaller subtree, so update \ + * augmentation for it. \ + */ \ + if (sib != elm) \ + (void)RB_AUGMENT_CHECK(sib); \ + return (parent); \ + } while (elm = parent, (parent = gpar) != NULL); \ + return (NULL); \ +} + +#define _RB_AUGMENT_WALK(elm, match, field) \ +do { \ + if (match == elm) \ + match = NULL; \ +} while (RB_AUGMENT_CHECK(elm) && \ + (elm = RB_PARENT(elm, field)) != NULL) + +#define RB_GENERATE_REMOVE(name, type, field, attr) \ +attr struct type * \ +name##_RB_REMOVE(struct name *head, struct type *out) \ +{ \ + struct type *child, *in, *opar, *parent; \ + \ + child = RB_LEFT(out, field); \ + in = RB_RIGHT(out, field); \ + opar = _RB_UP(out, field); \ + if (in == NULL || child == NULL) { \ + in = child = (in == NULL ? child : in); \ + parent = opar = _RB_PTR(opar); \ + } else { \ + parent = in; \ + while (RB_LEFT(in, field)) \ + in = RB_LEFT(in, field); \ + RB_SET_PARENT(child, in, field); \ + RB_LEFT(in, field) = child; \ + child = RB_RIGHT(in, field); \ + if (parent != in) { \ + RB_SET_PARENT(parent, in, field); \ + RB_RIGHT(in, field) = parent; \ + parent = RB_PARENT(in, field); \ + RB_LEFT(parent, field) = child; \ + } \ + _RB_UP(in, field) = opar; \ + opar = _RB_PTR(opar); \ + } \ + RB_SWAP_CHILD(head, opar, out, in, field); \ + if (child != NULL) \ + _RB_UP(child, field) = parent; \ + if (parent != NULL) { \ + opar = name##_RB_REMOVE_COLOR(head, parent, child); \ + /* if rotation has made 'parent' the root of the same \ + * subtree as before, don't re-augment it. */ \ + if (parent == in && RB_LEFT(parent, field) == NULL) { \ + opar = NULL; \ + parent = RB_PARENT(parent, field); \ + } \ + _RB_AUGMENT_WALK(parent, opar, field); \ + if (opar != NULL) { \ + /* \ + * Elements rotated into the search path have \ + * changed subtrees, so update augmentation for \ + * them if AUGMENT_WALK didn't. \ + */ \ + (void)RB_AUGMENT_CHECK(opar); \ + (void)RB_AUGMENT_CHECK(RB_PARENT(opar, field)); \ + } \ + } \ + return (out); \ +} + +#define RB_GENERATE_INSERT_FINISH(name, type, field, attr) \ +/* Inserts a node into the RB tree */ \ +attr struct type * \ +name##_RB_INSERT_FINISH(struct name *head, struct type *parent, \ + struct type **pptr, struct type *elm) \ +{ \ + struct type *tmp = NULL; \ + \ + RB_SET(elm, parent, field); \ + *pptr = elm; \ + if (parent != NULL) \ + tmp = name##_RB_INSERT_COLOR(head, parent, elm); \ + _RB_AUGMENT_WALK(elm, tmp, field); \ + if (tmp != NULL) \ + /* \ + * An element rotated into the search path has a \ + * changed subtree, so update augmentation for it if \ + * AUGMENT_WALK didn't. \ + */ \ + (void)RB_AUGMENT_CHECK(tmp); \ + return (NULL); \ +} + +#define RB_GENERATE_INSERT(name, type, field, cmp, attr) \ +/* Inserts a node into the RB tree */ \ +attr struct type * \ +name##_RB_INSERT(struct name *head, struct type *elm) \ +{ \ + struct type *tmp; \ + struct type **tmpp = &RB_ROOT(head); \ + struct type *parent = NULL; \ + \ + while ((tmp = *tmpp) != NULL) { \ + parent = tmp; \ + __typeof(cmp(NULL, NULL)) comp = (cmp)(elm, parent); \ + if (comp < 0) \ + tmpp = &RB_LEFT(parent, field); \ + else if (comp > 0) \ + tmpp = &RB_RIGHT(parent, field); \ + else \ + return (parent); \ + } \ + return (name##_RB_INSERT_FINISH(head, parent, tmpp, elm)); \ +} + +#define RB_GENERATE_FIND(name, type, field, cmp, attr) \ +/* Finds the node with the same key as elm */ \ +attr struct type * \ +name##_RB_FIND(struct name *head, struct type *elm) \ +{ \ + struct type *tmp = RB_ROOT(head); \ + __typeof(cmp(NULL, NULL)) comp; \ + while (tmp) { \ + comp = cmp(elm, tmp); \ + if (comp < 0) \ + tmp = RB_LEFT(tmp, field); \ + else if (comp > 0) \ + tmp = RB_RIGHT(tmp, field); \ + else \ + return (tmp); \ + } \ + return (NULL); \ +} + +#define RB_GENERATE_NFIND(name, type, field, cmp, attr) \ +/* Finds the first node greater than or equal to the search key */ \ +attr struct type * \ +name##_RB_NFIND(struct name *head, struct type *elm) \ +{ \ + struct type *tmp = RB_ROOT(head); \ + struct type *res = NULL; \ + __typeof(cmp(NULL, NULL)) comp; \ + while (tmp) { \ + comp = cmp(elm, tmp); \ + if (comp < 0) { \ + res = tmp; \ + tmp = RB_LEFT(tmp, field); \ + } \ + else if (comp > 0) \ + tmp = RB_RIGHT(tmp, field); \ + else \ + return (tmp); \ + } \ + return (res); \ +} + +#define RB_GENERATE_NEXT(name, type, field, attr) \ +/* ARGSUSED */ \ +attr struct type * \ +name##_RB_NEXT(struct type *elm) \ +{ \ + if (RB_RIGHT(elm, field)) { \ + elm = RB_RIGHT(elm, field); \ + while (RB_LEFT(elm, field)) \ + elm = RB_LEFT(elm, field); \ + } else { \ + while (RB_PARENT(elm, field) && \ + (elm == RB_RIGHT(RB_PARENT(elm, field), field))) \ + elm = RB_PARENT(elm, field); \ + elm = RB_PARENT(elm, field); \ + } \ + return (elm); \ +} + +#if defined(_KERNEL) && defined(DIAGNOSTIC) +#define _RB_ORDER_CHECK(cmp, lo, hi) do { \ + KASSERT((cmp)(lo, hi) < 0, ("out of order insertion")); \ +} while (0) +#else +#define _RB_ORDER_CHECK(cmp, lo, hi) do {} while (0) +#endif + +#define RB_GENERATE_INSERT_NEXT(name, type, field, cmp, attr) \ +/* Inserts a node into the next position in the RB tree */ \ +attr struct type * \ +name##_RB_INSERT_NEXT(struct name *head, \ + struct type *elm, struct type *next) \ +{ \ + struct type *tmp; \ + struct type **tmpp = &RB_RIGHT(elm, field); \ + \ + _RB_ORDER_CHECK(cmp, elm, next); \ + if (name##_RB_NEXT(elm) != NULL) \ + _RB_ORDER_CHECK(cmp, next, name##_RB_NEXT(elm)); \ + while ((tmp = *tmpp) != NULL) { \ + elm = tmp; \ + tmpp = &RB_LEFT(elm, field); \ + } \ + return (name##_RB_INSERT_FINISH(head, elm, tmpp, next)); \ +} + +#define RB_GENERATE_PREV(name, type, field, attr) \ +/* ARGSUSED */ \ +attr struct type * \ +name##_RB_PREV(struct type *elm) \ +{ \ + if (RB_LEFT(elm, field)) { \ + elm = RB_LEFT(elm, field); \ + while (RB_RIGHT(elm, field)) \ + elm = RB_RIGHT(elm, field); \ + } else { \ + while (RB_PARENT(elm, field) && \ + (elm == RB_LEFT(RB_PARENT(elm, field), field))) \ + elm = RB_PARENT(elm, field); \ + elm = RB_PARENT(elm, field); \ + } \ + return (elm); \ +} + +#define RB_GENERATE_INSERT_PREV(name, type, field, cmp, attr) \ +/* Inserts a node into the prev position in the RB tree */ \ +attr struct type * \ +name##_RB_INSERT_PREV(struct name *head, \ + struct type *elm, struct type *prev) \ +{ \ + struct type *tmp; \ + struct type **tmpp = &RB_LEFT(elm, field); \ + \ + _RB_ORDER_CHECK(cmp, prev, elm); \ + if (name##_RB_PREV(elm) != NULL) \ + _RB_ORDER_CHECK(cmp, name##_RB_PREV(elm), prev); \ + while ((tmp = *tmpp) != NULL) { \ + elm = tmp; \ + tmpp = &RB_RIGHT(elm, field); \ + } \ + return (name##_RB_INSERT_FINISH(head, elm, tmpp, prev)); \ +} + +#define RB_GENERATE_MINMAX(name, type, field, attr) \ +attr struct type * \ +name##_RB_MINMAX(struct name *head, int val) \ +{ \ + struct type *tmp = RB_ROOT(head); \ + struct type *parent = NULL; \ + while (tmp) { \ + parent = tmp; \ + if (val < 0) \ + tmp = RB_LEFT(tmp, field); \ + else \ + tmp = RB_RIGHT(tmp, field); \ + } \ + return (parent); \ +} + +#define RB_GENERATE_REINSERT(name, type, field, cmp, attr) \ +attr struct type * \ +name##_RB_REINSERT(struct name *head, struct type *elm) \ +{ \ + struct type *cmpelm; \ + if (((cmpelm = RB_PREV(name, head, elm)) != NULL && \ + cmp(cmpelm, elm) >= 0) || \ + ((cmpelm = RB_NEXT(name, head, elm)) != NULL && \ + cmp(elm, cmpelm) >= 0)) { \ + /* XXXLAS: Remove/insert is heavy handed. */ \ + RB_REMOVE(name, head, elm); \ + return (RB_INSERT(name, head, elm)); \ + } \ + return (NULL); \ +} \ + +#define RB_NEGINF -1 +#define RB_INF 1 + +#define RB_INSERT(name, x, y) name##_RB_INSERT(x, y) +#define RB_INSERT_NEXT(name, x, y, z) name##_RB_INSERT_NEXT(x, y, z) +#define RB_INSERT_PREV(name, x, y, z) name##_RB_INSERT_PREV(x, y, z) +#define RB_REMOVE(name, x, y) name##_RB_REMOVE(x, y) +#define RB_FIND(name, x, y) name##_RB_FIND(x, y) +#define RB_NFIND(name, x, y) name##_RB_NFIND(x, y) +#define RB_NEXT(name, x, y) name##_RB_NEXT(y) +#define RB_PREV(name, x, y) name##_RB_PREV(y) +#define RB_MIN(name, x) name##_RB_MINMAX(x, RB_NEGINF) +#define RB_MAX(name, x) name##_RB_MINMAX(x, RB_INF) +#define RB_REINSERT(name, x, y) name##_RB_REINSERT(x, y) + +#define RB_FOREACH(x, name, head) \ + for ((x) = RB_MIN(name, head); \ + (x) != NULL; \ + (x) = name##_RB_NEXT(x)) + +#define RB_FOREACH_FROM(x, name, y) \ + for ((x) = (y); \ + ((x) != NULL) && ((y) = name##_RB_NEXT(x), (x) != NULL); \ + (x) = (y)) + +#define RB_FOREACH_SAFE(x, name, head, y) \ + for ((x) = RB_MIN(name, head); \ + ((x) != NULL) && ((y) = name##_RB_NEXT(x), (x) != NULL); \ + (x) = (y)) + +#define RB_FOREACH_REVERSE(x, name, head) \ + for ((x) = RB_MAX(name, head); \ + (x) != NULL; \ + (x) = name##_RB_PREV(x)) + +#define RB_FOREACH_REVERSE_FROM(x, name, y) \ + for ((x) = (y); \ + ((x) != NULL) && ((y) = name##_RB_PREV(x), (x) != NULL); \ + (x) = (y)) + +#define RB_FOREACH_REVERSE_SAFE(x, name, head, y) \ + for ((x) = RB_MAX(name, head); \ + ((x) != NULL) && ((y) = name##_RB_PREV(x), (x) != NULL); \ + (x) = (y)) + +#endif /* _SYS_TREE_H_ */ diff --git a/src/compat-headers/ugid.c b/src/compat-headers/ugid.c new file mode 100644 index 0000000..bb8abaf --- /dev/null +++ b/src/compat-headers/ugid.c @@ -0,0 +1,41 @@ +/* + * user_from_uid / group_from_gid - cached UID/GID to name lookups. + * + * BSD libc provides these; Linux does not. + * SPDX-License-Identifier: BSD-2-Clause + */ +#include +#include +#include +#include +#include + +const char * +user_from_uid(uid_t uid, int nouser) +{ + static char buf[32]; + struct passwd *pw; + + pw = getpwuid(uid); + if (pw != NULL) + return pw->pw_name; + if (nouser) + return NULL; + (void)snprintf(buf, sizeof(buf), "%u", (unsigned)uid); + return buf; +} + +const char * +group_from_gid(gid_t gid, int nogroup) +{ + static char buf[32]; + struct group *gr; + + gr = getgrgid(gid); + if (gr != NULL) + return gr->gr_name; + if (nogroup) + return NULL; + (void)snprintf(buf, sizeof(buf), "%u", (unsigned)gid); + return buf; +} diff --git a/src/diff/diff.1 b/src/diff/diff.1 new file mode 100644 index 0000000..3996f03 --- /dev/null +++ b/src/diff/diff.1 @@ -0,0 +1,760 @@ +.\" $OpenBSD: diff.1,v 1.47 2015/11/24 19:35:41 jmc Exp $ +.\" +.\" Copyright (c) 1980, 1990, 1993 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. +.\" +.\" @(#)diff.1 8.1 (Berkeley) 6/30/93 +.\" +.Dd July 26, 2024 +.Dt DIFF 1 +.Os +.Sh NAME +.Nm diff +.Nd differential file and directory comparator +.Sh SYNOPSIS +.Nm diff +.Op Fl aBbdipTtw +.Oo +.Fl c | e | f | +.Fl n | q | u | y +.Oc +.Op Fl -brief +.Op Fl -color Ns = Ns Ar when +.Op Fl -changed-group-format Ar GFMT +.Op Fl -ed +.Op Fl -expand-tabs +.Op Fl -forward-ed +.Op Fl -ignore-all-space +.Op Fl -ignore-case +.Op Fl -ignore-space-change +.Op Fl -initial-tab +.Op Fl -minimal +.Op Fl -no-dereference +.Op Fl -no-ignore-file-name-case +.Op Fl -normal +.Op Fl -rcs +.Op Fl -show-c-function +.Op Fl -starting-file +.Op Fl -speed-large-files +.Op Fl -strip-trailing-cr +.Op Fl -tabsize Ar number +.Op Fl -text +.Op Fl -unified +.Op Fl I Ar pattern | Fl -ignore-matching-lines Ar pattern +.Op Fl F Ar pattern | Fl -show-function-line Ar pattern +.Op Fl L Ar label | Fl -label Ar label +.Ar file1 file2 +.Nm diff +.Op Fl aBbdilpTtw +.Op Fl I Ar pattern | Fl -ignore-matching-lines Ar pattern +.Op Fl F Ar pattern | Fl -show-function-line Ar pattern +.Op Fl L Ar label | Fl -label Ar label +.Op Fl -brief +.Op Fl -color Ns = Ns Ar when +.Op Fl -changed-group-format Ar GFMT +.Op Fl -ed +.Op Fl -expand-tabs +.Op Fl -forward-ed +.Op Fl -ignore-all-space +.Op Fl -ignore-case +.Op Fl -ignore-space-change +.Op Fl -initial-tab +.Op Fl -minimal +.Op Fl -no-dereference +.Op Fl -no-ignore-file-name-case +.Op Fl -normal +.Op Fl -paginate +.Op Fl -rcs +.Op Fl -show-c-function +.Op Fl -speed-large-files +.Op Fl -starting-file +.Op Fl -strip-trailing-cr +.Op Fl -tabsize Ar number +.Op Fl -text +.Fl C Ar number | -context Ar number +.Ar file1 file2 +.Nm diff +.Op Fl aBbdiltw +.Op Fl I Ar pattern | Fl -ignore-matching-lines Ar pattern +.Op Fl -brief +.Op Fl -color Ns = Ns Ar when +.Op Fl -changed-group-format Ar GFMT +.Op Fl -ed +.Op Fl -expand-tabs +.Op Fl -forward-ed +.Op Fl -ignore-all-space +.Op Fl -ignore-case +.Op Fl -ignore-space-change +.Op Fl -initial-tab +.Op Fl -minimal +.Op Fl -no-dereference +.Op Fl -no-ignore-file-name-case +.Op Fl -normal +.Op Fl -paginate +.Op Fl -rcs +.Op Fl -show-c-function +.Op Fl -speed-large-files +.Op Fl -starting-file +.Op Fl -strip-trailing-cr +.Op Fl -tabsize Ar number +.Op Fl -text +.Fl D Ar string | Fl -ifdef Ar string +.Ar file1 file2 +.Nm diff +.Op Fl aBbdilpTtw +.Op Fl I Ar pattern | Fl -ignore-matching-lines Ar pattern +.Op Fl F Ar pattern | Fl -show-function-line Ar pattern +.Op Fl L Ar label | Fl -label Ar label +.Op Fl -brief +.Op Fl -color Ns = Ns Ar when +.Op Fl -changed-group-format Ar GFMT +.Op Fl -ed +.Op Fl -expand-tabs +.Op Fl -forward-ed +.Op Fl -ignore-all-space +.Op Fl -ignore-case +.Op Fl -ignore-space-change +.Op Fl -initial-tab +.Op Fl -minimal +.Op Fl -no-dereference +.Op Fl -no-ignore-file-name-case +.Op Fl -normal +.Op Fl -paginate +.Op Fl -rcs +.Op Fl -show-c-function +.Op Fl -speed-large-files +.Op Fl -starting-file +.Op Fl -strip-trailing-cr +.Op Fl -tabsize Ar number +.Op Fl -text +.Fl U Ar number | Fl -unified Ar number +.Ar file1 file2 +.Nm diff +.Op Fl aBbdilNPprsTtw +.Oo +.Fl c | e | f | +.Fl n | q | u +.Oc +.Op Fl -brief +.Op Fl -color Ns = Ns Ar when +.Op Fl -changed-group-format Ar GFMT +.Op Fl -context +.Op Fl -ed +.Op Fl -expand-tabs +.Op Fl -forward-ed +.Op Fl -ignore-all-space +.Op Fl -ignore-case +.Op Fl -ignore-space-change +.Op Fl -initial-tab +.Op Fl -minimal +.Op Fl -new-file +.Op Fl -no-dereference +.Op Fl -no-ignore-file-name-case +.Op Fl -normal +.Op Fl -paginate +.Op Fl -rcs +.Op Fl -recursive +.Op Fl -report-identical-files +.Op Fl -show-c-function +.Op Fl -speed-large-files +.Op Fl -strip-trailing-cr +.Op Fl -tabsize Ar number +.Op Fl -text +.Op Fl -unidirectional-new-file +.Op Fl -unified +.Op Fl I Ar pattern | Fl -ignore-matching-lines Ar pattern +.Op Fl F Ar pattern | Fl -show-function-line Ar pattern +.Bk -words +.Op Fl L Ar label | Fl -label Ar label +.Op Fl S Ar name | Fl -starting-file Ar name +.Op Fl X Ar file | Fl -exclude-from Ar file +.Op Fl x Ar pattern | Fl -exclude Ar pattern +.Ek +.Ar dir1 dir2 +.Nm diff +.Op Fl aBbditwW +.Op Fl -color Ns = Ns Ar when +.Op Fl -expand-tabs +.Op Fl -ignore-all-space +.Op Fl -ignore-blank-lines +.Op Fl -ignore-case +.Op Fl -minimal +.Op Fl -no-dereference +.Op Fl -no-ignore-file-name-case +.Op Fl -strip-trailing-cr +.Op Fl -suppress-common-lines +.Op Fl -tabsize Ar number +.Op Fl -text +.Op Fl -width +.Fl y | Fl -side-by-side +.Ar file1 file2 +.Nm diff +.Op Fl -help +.Op Fl -version +.Sh DESCRIPTION +The +.Nm +utility compares the contents of +.Ar file1 +and +.Ar file2 +and writes to the standard output the list of changes necessary to +convert one file into the other. +No output is produced if the files are identical. +.Pp +Output options (mutually exclusive): +.Bl -tag -width Ds +.It Fl C Ar number Fl -context Ar number +Like +.Fl c +but produces a diff with +.Ar number +lines of context. +.It Fl c +Produces a diff with 3 lines of context. +With +.Fl c +the output format is modified slightly: +the output begins with identification of the files involved and +their creation dates and then each change is separated +by a line with fifteen +.Li * Ns 's . +The lines removed from +.Ar file1 +are marked with +.Sq \&-\ \& ; +those added to +.Ar file2 +are marked +.Sq +\ \& . +Lines which are changed from one file to the other are marked in +both files with +.Sq !\ \& . +Changes which lie within 3 lines of each other are grouped together on +output. +.It Fl D Ar string Fl -ifdef Ar string +Creates a merged version of +.Ar file1 +and +.Ar file2 +on the standard output, with C preprocessor controls included so that +a compilation of the result without defining +.Ar string +is equivalent to compiling +.Ar file1 , +while defining +.Ar string +will yield +.Ar file2 . +.It Fl e -ed +Produces output in a form suitable as input for the editor utility, +.Xr ed 1 , +which can then be used to convert file1 into file2. +.Pp +Extra commands are added to the output when comparing directories with +.Fl e , +so that the result is a +.Xr sh 1 +script for converting text files which are common to the two directories +from their state in +.Ar dir1 +to their state in +.Ar dir2 . +.It Fl f -forward-ed +Identical output to that of the +.Fl e +flag, but in reverse order. +It cannot be digested by +.Xr ed 1 . +.It Fl -help +This option prints a summary to stdout and exits with status 0. +.It Fl n +Produces a script similar to that of +.Fl e , +but in the opposite order and with a count of changed lines on each +insert or delete command. +This is the form used by rcsdiff. +.It Fl q -brief +Just print a line when the files differ. +Does not output a list of changes. +.It Fl U Ar number Fl -unified Ar number +Like +.Fl u +but produces a diff with +.Ar number +lines of context. +.It Fl u +Produces a +.Em unified +diff with 3 lines of context. +A unified diff is similar to the context diff produced by the +.Fl c +option. +However, unlike with +.Fl c , +all lines to be changed (added and/or removed) are present in +a single section. +.It Fl -version +This option prints a version string to stdout and exits with status 0. +.It Fl y Fl -side-by-side +Output in two columns with a marker between them. +The marker can be one +of the following: +.Pp +.Bl -tag -width Ds -offset indent -compact +.It space +Corresponding lines are identical. +.It '|' +Corresponding lines are different. +.It '<' +Files differ and only the first file contains the line. +.It '>' +Files differ and only the second file contains the line. +.El +.El +.Pp +Comparison options: +.Bl -tag -width Ds +.It Fl a -text +Treat all files as ASCII text. +Normally +.Nm +will simply print +.Dq Binary files ... differ +if files contain binary characters. +Use of this option forces +.Nm +to produce a diff. +.It Fl B Fl -ignore-blank-lines +Causes chunks that include only blank lines to be ignored. +.It Fl b -ignore-space-change +Causes trailing blanks (spaces and tabs) to be ignored, and other +strings of blanks to compare equal. +.It Fl -color= Ns Oo Ar when Oc +Color the additions green, and removals red, or the value in the +.Ev DIFFCOLORS +environment variable. +The possible values of +.Ar when +are +.Dq Cm never , +.Dq Cm always +and +.Dq Cm auto . +.Cm auto +will use color if the output is a tty and the +.Ev COLORTERM +environment variable is set to a non-empty string. +.It Fl d -minimal +Try very hard to produce a diff as small as possible. +This may consume a lot of processing power and memory when processing +large files with many changes. +.It Fl F Ar pattern, Fl -show-function-line Ar pattern +Like +.Fl p, +but display the last line that matches provided pattern. +.It Fl I Ar pattern Fl -ignore-matching-lines Ar pattern +Ignores changes, insertions, and deletions whose lines match the +extended regular expression +.Ar pattern . +Multiple +.Fl I +patterns may be specified. +All lines in the change must match some pattern for the change to be +ignored. +See +.Xr re_format 7 +for more information on regular expression patterns. +.It Fl i -ignore-case +Ignores the case of letters. +E.g., +.Dq A +will compare equal to +.Dq a . +.It Fl l -paginate +Pass the output through +.Xr pr 1 +to paginate it. +.It Fl L Ar label Fl -label Ar label +Print +.Ar label +instead of the first (and second, if this option is specified twice) +file name and time in the context or unified diff header. +.It Fl p -show-c-function +With unified and context diffs, show with each change +the first 40 characters of the last line before the context beginning +with a letter, an underscore or a dollar sign. +For C and Objective-C source code following standard layout conventions, this +will show the prototype of the function the change applies to. +.It Fl T -initial-tab +Print a tab rather than a space before the rest of the line for the +normal, context or unified output formats. +This makes the alignment of tabs in the line consistent. +.It Fl t -expand-tabs +Will expand tabs in output lines. +Normal or +.Fl c +output adds character(s) to the front of each line which may screw up +the indentation of the original source lines and make the output listing +difficult to interpret. +This option will preserve the original source's indentation. +.It Fl w -ignore-all-space +Is similar to +.Fl b -ignore-space-change +but causes whitespace (blanks and tabs) to be totally ignored. +E.g., +.Dq if (\ \&a == b \&) +will compare equal to +.Dq if(a==b) . +.It Fl W Ar number Fl -width Ar number +Output at most +.Ar number +columns when using side by side format. +The default value is 130. +Note that unless +.It Fl t +was specified, +.Nm +will always align the second column to a tab stop, so values of +.Fl -width +smaller than approximately five times the value of +.Fl -tabsize +may yield surprising results. +.It Fl -changed-group-format Ar GFMT +Format input groups in the provided +.Pp +the format is a string with special keywords: +.Bl -tag -width %< +.It %< +lines from FILE1 +.It %< +lines from FILE2 +.El +.It Fl -ignore-file-name-case +ignore case when comparing file names +.It Fl -no-dereference +do not follow symbolic links +.It Fl -no-ignore-file-name-case +do not ignore case wen comparing file names (default) +.It Fl -normal +default diff output +.It Fl -speed-large-files +stub option for compatibility with GNU diff +.It Fl -strip-trailing-cr +strip carriage return on input files +.It Fl -suppress-common-lines +Do not output common lines when using the side by side format +.It Fl -tabsize Ar number +Number of spaces representing a tab (default 8) +.El +.Pp +Directory comparison options: +.Bl -tag -width Ds +.It Fl N -new-file +If a file is found in only one directory, act as if it was found in the +other directory too but was of zero size. +.It Fl P -unidirectional-new-file +If a file is found only in +.Ar dir2 , +act as if it was found in +.Ar dir1 +too but was of zero size. +.It Fl r -recursive +Causes application of +.Nm +recursively to common subdirectories encountered. +.It Fl S Ar name Fl -starting-file Ar name +Re-starts a directory +.Nm +in the middle, beginning with file +.Ar name . +.It Fl s -report-identical-files +Causes +.Nm +to report files which are the same, which are otherwise not mentioned. +.It Fl X Ar file Fl -exclude-from Ar file +Exclude files and subdirectories from comparison whose basenames match +lines in +.Ar file . +Multiple +.Fl X +options may be specified. +.It Fl x Ar pattern Fl -exclude Ar pattern +Exclude files and subdirectories from comparison whose basenames match +.Ar pattern . +Patterns are matched using shell-style globbing via +.Xr fnmatch 3 . +Multiple +.Fl x +options may be specified. +.El +.Pp +If both arguments are directories, +.Nm +sorts the contents of the directories by name, and then runs the +regular file +.Nm +algorithm, producing a change list, +on text files which are different. +Binary files which differ, +common subdirectories, and files which appear in only one directory +are described as such. +In directory mode only regular files and directories are compared. +If a non-regular file such as a device special file or FIFO is encountered, +a diagnostic message is printed. +.Pp +If only one of +.Ar file1 +and +.Ar file2 +is a directory, +.Nm +is applied to the non-directory file and the file contained in +the directory file with a filename that is the same as the +last component of the non-directory file. +.Pp +If either +.Ar file1 +or +.Ar file2 +is +.Sq - , +the standard input is +used in its place. +.Ss Output Style +The default (without +.Fl e , +.Fl c , +or +.Fl n -rcs +.\" -C +options) +output contains lines of these forms, where +.Va XX , YY , ZZ , QQ +are line numbers respective of file order. +.Pp +.Bl -tag -width "XX,YYcZZ,QQ" -compact +.It Li XX Ns Ic a Ns Li YY +At (the end of) line +.Va XX +of +.Ar file1 , +append the contents +of line +.Va YY +of +.Ar file2 +to make them equal. +.It Li XX Ns Ic a Ns Li YY,ZZ +Same as above, but append the range of lines, +.Va YY +through +.Va ZZ +of +.Ar file2 +to line +.Va XX +of file1. +.It Li XX Ns Ic d Ns Li YY +At line +.Va XX +delete +the line. +The value +.Va YY +tells to which line the change would bring +.Ar file1 +in line with +.Ar file2 . +.It Li XX,YY Ns Ic d Ns Li ZZ +Delete the range of lines +.Va XX +through +.Va YY +in +.Ar file1 . +.It Li XX Ns Ic c Ns Li YY +Change the line +.Va XX +in +.Ar file1 +to the line +.Va YY +in +.Ar file2 . +.It Li XX,YY Ns Ic c Ns Li ZZ +Replace the range of specified lines with the line +.Va ZZ . +.It Li XX,YY Ns Ic c Ns Li ZZ,QQ +Replace the range +.Va XX , Ns Va YY +from +.Ar file1 +with the range +.Va ZZ , Ns Va QQ +from +.Ar file2 . +.El +.Pp +These lines resemble +.Xr ed 1 +subcommands to convert +.Ar file1 +into +.Ar file2 . +The line numbers before the action letters pertain to +.Ar file1 ; +those after pertain to +.Ar file2 . +Thus, by exchanging +.Ic a +for +.Ic d +and reading the line in reverse order, one can also +determine how to convert +.Ar file2 +into +.Ar file1 . +As in +.Xr ed 1 , +identical +pairs (where num1 = num2) are abbreviated as a single +number. +.Sh ENVIRONMENT +.Bl -tag -width DIFFCOLORS +.It Ev DIFFCOLORS +The value of this variable is the form +.Ar add Ns : Ns Ar rm , +where +.Ar add +is the ASCII escape sequence for additions and +.Ar rm +is the ASCII escape sequence for deletions. +If this is unset, +.Nm +uses green for additions and red for removals. +.El +.Sh FILES +.Bl -tag -width /tmp/diff.XXXXXXXX -compact +.It Pa /tmp/diff.XXXXXXXX +Temporary file used when comparing a device or the standard input. +Note that the temporary file is unlinked as soon as it is created +so it will not show up in a directory listing. +.El +.Sh EXIT STATUS +The +.Nm +utility exits with one of the following values: +.Pp +.Bl -tag -width Ds -offset indent -compact +.It 0 +No differences were found. +.It 1 +Differences were found. +.It >1 +An error occurred. +.El +.Pp +The +.Fl -help +and +.Fl -version +options exit with a status of 0. +.Sh EXAMPLES +Compare +.Pa old_dir +and +.Pa new_dir +recursively generating an unified diff and treating files found only in one +of those directories as new files: +.Bd -literal -offset indent +$ diff -ruN /path/to/old_dir /path/to/new_dir +.Ed +.Pp +Same as above but excluding files matching the expressions +.Dq *.h +and +.Dq *.c : +.Bd -literal -offset indent +$ diff -ruN -x '*.h' -x '*.c' /path/to/old_dir /path/to/new_dir +.Ed +.Pp +Show a single line indicating if the files differ: +.Bd -literal -offset indent +$ diff -q /boot/loader.conf /boot/defaults/loader.conf +Files /boot/loader.conf and /boot/defaults/loader.conf differ +.Ed +.Pp +Assuming a file named +.Pa example.txt +with the following contents: +.Bd -literal -offset indent +FreeBSD is an operating system +Linux is a kernel +OpenBSD is an operating system +.Ed +.Pp +Compare stdin with +.Pa example.txt +excluding from the comparison those lines containing either +.Qq Linux +or +.Qq Open : +.Bd -literal -offset indent +$ echo "FreeBSD is an operating system" | diff -q -I 'Linux|Open' example.txt - +.Ed +.Sh SEE ALSO +.Xr cmp 1 , +.Xr comm 1 , +.Xr diff3 1 , +.Xr ed 1 , +.Xr patch 1 , +.Xr pr 1 , +.Xr sdiff 1 +.Rs +.%A James W. Hunt +.%A M. Douglas McIlroy +.%T "An Algorithm for Differential File Comparison" +.%J Computing Science Technical Report +.%Q Bell Laboratories 41 +.%D June 1976 +.Re +.Sh STANDARDS +The +.Nm +utility is compliant with the +.St -p1003.1-2008 +specification. +.Pp +The flags +.Op Fl aDdIiLlNnPpqSsTtwXxy +are extensions to that specification. +.Sh HISTORY +A +.Nm +command appeared in +.At v6 . diff --git a/src/diff/diff.c b/src/diff/diff.c new file mode 100644 index 0000000..37b3767 --- /dev/null +++ b/src/diff/diff.c @@ -0,0 +1,651 @@ +/* $OpenBSD: diff.c,v 1.67 2019/06/28 13:35:00 deraadt Exp $ */ + +/* + * Copyright (c) 2003 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "diff.h" +#include "xmalloc.h" + +static const char diff_version[] = "FreeBSD diff 20220309"; +bool lflag, Nflag, Pflag, rflag, sflag, Tflag, cflag; +bool ignore_file_case, suppress_common, color, noderef; +static bool help = false; +int diff_format, diff_context, status; +int tabsize = 8, width = 130; +static int colorflag = COLORFLAG_NEVER; +char *start, *ifdefname, *diffargs, *label[2]; +char *ignore_pats, *most_recent_pat; +char *group_format = NULL; +const char *add_code, *del_code; +struct stat stb1, stb2; +struct excludes *excludes_list; +regex_t ignore_re, most_recent_re; + +#define OPTIONS "0123456789aBbC:cdD:efF:HhI:iL:lnNPpqrS:sTtU:uwW:X:x:y" +enum { + OPT_TSIZE = CHAR_MAX + 1, + OPT_STRIPCR, + OPT_IGN_FN_CASE, + OPT_NO_IGN_FN_CASE, + OPT_NORMAL, + OPT_HELP, + OPT_HORIZON_LINES, + OPT_CHANGED_GROUP_FORMAT, + OPT_SUPPRESS_COMMON, + OPT_COLOR, + OPT_NO_DEREFERENCE, + OPT_VERSION, +}; + +static struct option longopts[] = { + { "text", no_argument, 0, 'a' }, + { "ignore-space-change", no_argument, 0, 'b' }, + { "context", optional_argument, 0, 'C' }, + { "ifdef", required_argument, 0, 'D' }, + { "minimal", no_argument, 0, 'd' }, + { "ed", no_argument, 0, 'e' }, + { "forward-ed", no_argument, 0, 'f' }, + { "show-function-line", required_argument, 0, 'F' }, + { "speed-large-files", no_argument, NULL, 'H' }, + { "ignore-blank-lines", no_argument, 0, 'B' }, + { "ignore-matching-lines", required_argument, 0, 'I' }, + { "ignore-case", no_argument, 0, 'i' }, + { "paginate", no_argument, NULL, 'l' }, + { "label", required_argument, 0, 'L' }, + { "new-file", no_argument, 0, 'N' }, + { "rcs", no_argument, 0, 'n' }, + { "unidirectional-new-file", no_argument, 0, 'P' }, + { "show-c-function", no_argument, 0, 'p' }, + { "brief", no_argument, 0, 'q' }, + { "recursive", no_argument, 0, 'r' }, + { "report-identical-files", no_argument, 0, 's' }, + { "starting-file", required_argument, 0, 'S' }, + { "expand-tabs", no_argument, 0, 't' }, + { "initial-tab", no_argument, 0, 'T' }, + { "unified", optional_argument, 0, 'U' }, + { "ignore-all-space", no_argument, 0, 'w' }, + { "width", required_argument, 0, 'W' }, + { "exclude", required_argument, 0, 'x' }, + { "exclude-from", required_argument, 0, 'X' }, + { "side-by-side", no_argument, NULL, 'y' }, + { "ignore-file-name-case", no_argument, NULL, OPT_IGN_FN_CASE }, + { "help", no_argument, NULL, OPT_HELP}, + { "horizon-lines", required_argument, NULL, OPT_HORIZON_LINES }, + { "no-dereference", no_argument, NULL, OPT_NO_DEREFERENCE}, + { "no-ignore-file-name-case", no_argument, NULL, OPT_NO_IGN_FN_CASE }, + { "normal", no_argument, NULL, OPT_NORMAL }, + { "strip-trailing-cr", no_argument, NULL, OPT_STRIPCR }, + { "tabsize", required_argument, NULL, OPT_TSIZE }, + { "changed-group-format", required_argument, NULL, OPT_CHANGED_GROUP_FORMAT}, + { "suppress-common-lines", no_argument, NULL, OPT_SUPPRESS_COMMON }, + { "color", optional_argument, NULL, OPT_COLOR }, + { "version", no_argument, NULL, OPT_VERSION}, + { NULL, 0, 0, '\0'} +}; + +static void checked_regcomp(char const *, regex_t *); +static void usage(void) __dead2; +static void conflicting_format(void) __dead2; +static void push_excludes(char *); +static void push_ignore_pats(char *); +static void read_excludes_file(char *file); +static void set_argstr(char **, char **); +static char *splice(char *, char *); +static bool do_color(void); + +int +diff_main(int argc, char **argv) +{ + const char *errstr = NULL; + char *ep, **oargv; + long l; + int ch, dflags, lastch, gotstdin, prevoptind, newarg; + + oargv = argv; + gotstdin = 0; + dflags = 0; + lastch = '\0'; + prevoptind = 1; + newarg = 1; + diff_context = 3; + diff_format = D_UNSET; +#define FORMAT_MISMATCHED(type) \ + (diff_format != D_UNSET && diff_format != (type)) + while ((ch = getopt_long(argc, argv, OPTIONS, longopts, NULL)) != -1) { + switch (ch) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + if (newarg) + usage(); /* disallow -[0-9]+ */ + else if (lastch == 'c' || lastch == 'u') + diff_context = 0; + else if (!isdigit(lastch) || diff_context > INT_MAX / 10) + usage(); + diff_context = (diff_context * 10) + (ch - '0'); + break; + case 'a': + dflags |= D_FORCEASCII; + break; + case 'b': + dflags |= D_FOLDBLANKS; + break; + case 'C': + case 'c': + if (FORMAT_MISMATCHED(D_CONTEXT)) + conflicting_format(); + cflag = true; + diff_format = D_CONTEXT; + if (optarg != NULL) { + l = strtol(optarg, &ep, 10); + if (*ep != '\0' || l < 0 || l >= INT_MAX) + usage(); + diff_context = (int)l; + } + break; + case 'd': + dflags |= D_MINIMAL; + break; + case 'D': + if (FORMAT_MISMATCHED(D_IFDEF)) + conflicting_format(); + diff_format = D_IFDEF; + ifdefname = optarg; + break; + case 'e': + if (FORMAT_MISMATCHED(D_EDIT)) + conflicting_format(); + diff_format = D_EDIT; + break; + case 'f': + if (FORMAT_MISMATCHED(D_REVERSE)) + conflicting_format(); + diff_format = D_REVERSE; + break; + case 'H': + /* ignore but needed for compatibility with GNU diff */ + break; + case 'h': + /* silently ignore for backwards compatibility */ + break; + case 'B': + dflags |= D_SKIPBLANKLINES; + break; + case 'F': + if (dflags & D_PROTOTYPE) + conflicting_format(); + dflags |= D_MATCHLAST; + most_recent_pat = xstrdup(optarg); + break; + case 'I': + push_ignore_pats(optarg); + break; + case 'i': + dflags |= D_IGNORECASE; + break; + case 'L': + if (label[0] == NULL) + label[0] = optarg; + else if (label[1] == NULL) + label[1] = optarg; + else + usage(); + break; + case 'l': + lflag = true; + break; + case 'N': + Nflag = true; + break; + case 'n': + if (FORMAT_MISMATCHED(D_NREVERSE)) + conflicting_format(); + diff_format = D_NREVERSE; + break; + case 'p': + if (dflags & D_MATCHLAST) + conflicting_format(); + dflags |= D_PROTOTYPE; + break; + case 'P': + Pflag = true; + break; + case 'r': + rflag = true; + break; + case 'q': + if (FORMAT_MISMATCHED(D_BRIEF)) + conflicting_format(); + diff_format = D_BRIEF; + break; + case 'S': + start = optarg; + break; + case 's': + sflag = true; + break; + case 'T': + Tflag = true; + break; + case 't': + dflags |= D_EXPANDTABS; + break; + case 'U': + case 'u': + if (FORMAT_MISMATCHED(D_UNIFIED)) + conflicting_format(); + diff_format = D_UNIFIED; + if (optarg != NULL) { + l = strtol(optarg, &ep, 10); + if (*ep != '\0' || l < 0 || l >= INT_MAX) + usage(); + diff_context = (int)l; + } + break; + case 'w': + dflags |= D_IGNOREBLANKS; + break; + case 'W': + width = (int) strtonum(optarg, 1, INT_MAX, &errstr); + if (errstr) + errx(1, "width is %s: %s", errstr, optarg); + break; + case 'X': + read_excludes_file(optarg); + break; + case 'x': + push_excludes(optarg); + break; + case 'y': + if (FORMAT_MISMATCHED(D_SIDEBYSIDE)) + conflicting_format(); + diff_format = D_SIDEBYSIDE; + break; + case OPT_CHANGED_GROUP_FORMAT: + if (FORMAT_MISMATCHED(D_GFORMAT)) + conflicting_format(); + diff_format = D_GFORMAT; + group_format = optarg; + break; + case OPT_HELP: + help = true; + usage(); + break; + case OPT_HORIZON_LINES: + break; /* XXX TODO for compatibility with GNU diff3 */ + case OPT_IGN_FN_CASE: + ignore_file_case = true; + break; + case OPT_NO_IGN_FN_CASE: + ignore_file_case = false; + break; + case OPT_NORMAL: + if (FORMAT_MISMATCHED(D_NORMAL)) + conflicting_format(); + diff_format = D_NORMAL; + break; + case OPT_TSIZE: + tabsize = (int) strtonum(optarg, 1, INT_MAX, &errstr); + if (errstr) + errx(1, "tabsize is %s: %s", errstr, optarg); + break; + case OPT_STRIPCR: + dflags |= D_STRIPCR; + break; + case OPT_SUPPRESS_COMMON: + suppress_common = 1; + break; + case OPT_COLOR: + if (optarg == NULL || strncmp(optarg, "auto", 4) == 0) + colorflag = COLORFLAG_AUTO; + else if (strncmp(optarg, "always", 6) == 0) + colorflag = COLORFLAG_ALWAYS; + else if (strncmp(optarg, "never", 5) == 0) + colorflag = COLORFLAG_NEVER; + else + errx(2, "unsupported --color value '%s' (must be always, auto, or never)", + optarg); + break; + case OPT_NO_DEREFERENCE: + noderef = true; + break; + case OPT_VERSION: + printf("%s\n", diff_version); + exit(0); + default: + usage(); + break; + } + lastch = ch; + newarg = optind != prevoptind; + prevoptind = optind; + } + if (diff_format == D_UNSET && (dflags & D_PROTOTYPE) != 0) + diff_format = D_CONTEXT; + if (diff_format == D_UNSET) + diff_format = D_NORMAL; + argc -= optind; + argv += optind; + + if (do_color()) { + char *p; + const char *env; + + color = true; + add_code = "32"; + del_code = "31"; + env = getenv("DIFFCOLORS"); + if (env != NULL && *env != '\0' && (p = strdup(env))) { + add_code = p; + strsep(&p, ":"); + if (p != NULL) + del_code = p; + } + } + +#ifdef __OpenBSD__ + if (pledge("stdio rpath tmppath", NULL) == -1) + err(2, "pledge"); +#endif + + /* + * Do sanity checks, fill in stb1 and stb2 and call the appropriate + * driver routine. Both drivers use the contents of stb1 and stb2. + */ + if (argc != 2) + usage(); + checked_regcomp(ignore_pats, &ignore_re); + checked_regcomp(most_recent_pat, &most_recent_re); + if (strcmp(argv[0], "-") == 0) { + fstat(STDIN_FILENO, &stb1); + gotstdin = 1; + } else if (stat(argv[0], &stb1) != 0) { + if (!Nflag || errno != ENOENT) + err(2, "%s", argv[0]); + dflags |= D_EMPTY1; + memset(&stb1, 0, sizeof(struct stat)); + } + + if (strcmp(argv[1], "-") == 0) { + fstat(STDIN_FILENO, &stb2); + gotstdin = 1; + } else if (stat(argv[1], &stb2) != 0) { + if (!Nflag || errno != ENOENT) + err(2, "%s", argv[1]); + dflags |= D_EMPTY2; + memset(&stb2, 0, sizeof(stb2)); + stb2.st_mode = stb1.st_mode; + } + + if (dflags & D_EMPTY1 && dflags & D_EMPTY2){ + warn("%s", argv[0]); + warn("%s", argv[1]); + exit(2); + } + + if (stb1.st_mode == 0) + stb1.st_mode = stb2.st_mode; + + if (gotstdin && (S_ISDIR(stb1.st_mode) || S_ISDIR(stb2.st_mode))) + errx(2, "can't compare - to a directory"); + set_argstr(oargv, argv); + if (S_ISDIR(stb1.st_mode) && S_ISDIR(stb2.st_mode)) { + if (diff_format == D_IFDEF) + errx(2, "-D option not supported with directories"); + diffdir(argv[0], argv[1], dflags); + } else { + if (S_ISDIR(stb1.st_mode)) { + argv[0] = splice(argv[0], argv[1]); + if (stat(argv[0], &stb1) == -1) + err(2, "%s", argv[0]); + } + if (S_ISDIR(stb2.st_mode)) { + argv[1] = splice(argv[1], argv[0]); + if (stat(argv[1], &stb2) == -1) + err(2, "%s", argv[1]); + } + print_status(diffreg(argv[0], argv[1], dflags, 1), argv[0], + argv[1], ""); + } + exit(status); +} + +static void +checked_regcomp(char const *pattern, regex_t *comp) +{ + char buf[BUFSIZ]; + int error; + + if (pattern == NULL) + return; + + error = regcomp(comp, pattern, REG_NEWLINE | REG_EXTENDED); + if (error != 0) { + regerror(error, comp, buf, sizeof(buf)); + if (*pattern != '\0') + errx(2, "%s: %s", pattern, buf); + else + errx(2, "%s", buf); + } +} + +static void +set_argstr(char **av, char **ave) +{ + size_t argsize; + char **ap; + + argsize = 4 + *ave - *av + 1; + diffargs = xmalloc(argsize); + strlcpy(diffargs, "diff", argsize); + for (ap = av + 1; ap < ave; ap++) { + if (strcmp(*ap, "--") != 0) { + strlcat(diffargs, " ", argsize); + strlcat(diffargs, *ap, argsize); + } + } +} + +/* + * Read in an excludes file and push each line. + */ +static void +read_excludes_file(char *file) +{ + FILE *fp; + char *buf, *pattern; + size_t len; + + if (strcmp(file, "-") == 0) + fp = stdin; + else if ((fp = fopen(file, "r")) == NULL) + err(2, "%s", file); + while ((buf = fgetln(fp, &len)) != NULL) { + if (buf[len - 1] == '\n') + len--; + if ((pattern = strndup(buf, len)) == NULL) + err(2, "xstrndup"); + push_excludes(pattern); + } + if (strcmp(file, "-") != 0) + fclose(fp); +} + +/* + * Push a pattern onto the excludes list. + */ +static void +push_excludes(char *pattern) +{ + struct excludes *entry; + + entry = xmalloc(sizeof(*entry)); + entry->pattern = pattern; + entry->next = excludes_list; + excludes_list = entry; +} + +static void +push_ignore_pats(char *pattern) +{ + size_t len; + + if (ignore_pats == NULL) + ignore_pats = xstrdup(pattern); + else { + /* old + "|" + new + NUL */ + len = strlen(ignore_pats) + strlen(pattern) + 2; + ignore_pats = xreallocarray(ignore_pats, 1, len); + strlcat(ignore_pats, "|", len); + strlcat(ignore_pats, pattern, len); + } +} + +void +print_status(int val, char *path1, char *path2, const char *entry) +{ + if (label[0] != NULL) + path1 = label[0]; + if (label[1] != NULL) + path2 = label[1]; + + switch (val) { + case D_BINARY: + printf("Binary files %s%s and %s%s differ\n", + path1, entry, path2, entry); + break; + case D_DIFFER: + if (diff_format == D_BRIEF) + printf("Files %s%s and %s%s differ\n", + path1, entry, path2, entry); + break; + case D_SAME: + if (sflag) + printf("Files %s%s and %s%s are identical\n", + path1, entry, path2, entry); + break; + case D_MISMATCH1: + printf("File %s%s is a directory while file %s%s is a regular file\n", + path1, entry, path2, entry); + break; + case D_MISMATCH2: + printf("File %s%s is a regular file while file %s%s is a directory\n", + path1, entry, path2, entry); + break; + case D_SKIPPED1: + printf("File %s%s is not a regular file or directory and was skipped\n", + path1, entry); + break; + case D_SKIPPED2: + printf("File %s%s is not a regular file or directory and was skipped\n", + path2, entry); + break; + case D_ERROR: + break; + } +} + +static void +usage(void) +{ + (void)fprintf(help ? stdout : stderr, + "usage: diff [-aBbdilpTtw] [-c | -e | -f | -n | -q | -u] [--ignore-case]\n" + " [--no-ignore-case] [--normal] [--strip-trailing-cr] [--tabsize]\n" + " [-I pattern] [-F pattern] [-L label] file1 file2\n" + " diff [-aBbdilpTtw] [-I pattern] [-L label] [--ignore-case]\n" + " [--no-ignore-case] [--normal] [--strip-trailing-cr] [--tabsize]\n" + " [-F pattern] -C number file1 file2\n" + " diff [-aBbdiltw] [-I pattern] [--ignore-case] [--no-ignore-case]\n" + " [--normal] [--strip-trailing-cr] [--tabsize] -D string file1 file2\n" + " diff [-aBbdilpTtw] [-I pattern] [-L label] [--ignore-case]\n" + " [--no-ignore-case] [--normal] [--tabsize] [--strip-trailing-cr]\n" + " [-F pattern] -U number file1 file2\n" + " diff [-aBbdilNPprsTtw] [-c | -e | -f | -n | -q | -u] [--ignore-case]\n" + " [--no-ignore-case] [--normal] [--tabsize] [-I pattern] [-L label]\n" + " [-F pattern] [-S name] [-X file] [-x pattern] dir1 dir2\n" + " diff [-aBbditwW] [--expand-tabs] [--ignore-all-space]\n" + " [--ignore-blank-lines] [--ignore-case] [--minimal]\n" + " [--no-ignore-file-name-case] [--strip-trailing-cr]\n" + " [--suppress-common-lines] [--tabsize] [--text] [--width]\n" + " -y | --side-by-side file1 file2\n" + " diff [--help] [--version]\n"); + + if (help) + exit(0); + else + exit(2); +} + +static void +conflicting_format(void) +{ + + fprintf(stderr, "error: conflicting output format options.\n"); + usage(); +} + +static bool +do_color(void) +{ + const char *p, *p2; + + switch (colorflag) { + case COLORFLAG_AUTO: + p = getenv("CLICOLOR"); + p2 = getenv("COLORTERM"); + if ((p != NULL && *p != '\0') || (p2 != NULL && *p2 != '\0')) + return isatty(STDOUT_FILENO); + break; + case COLORFLAG_ALWAYS: + return (true); + case COLORFLAG_NEVER: + return (false); + } + + return (false); +} + +static char * +splice(char *dir, char *path) +{ + char *tail, *buf; + size_t dirlen; + + dirlen = strlen(dir); + while (dirlen != 0 && dir[dirlen - 1] == '/') + dirlen--; + if ((tail = strrchr(path, '/')) == NULL) + tail = path; + else + tail++; + xasprintf(&buf, "%.*s/%s", (int)dirlen, dir, tail); + return (buf); +} diff --git a/src/diff/diff.h b/src/diff/diff.h new file mode 100644 index 0000000..a23abbf --- /dev/null +++ b/src/diff/diff.h @@ -0,0 +1,123 @@ +/* $OpenBSD: diff.h,v 1.34 2020/11/01 18:16:08 jcs Exp $ */ + +/*- + * Copyright (c) 1991, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + * + * @(#)diff.h 8.1 (Berkeley) 6/6/93 + */ + +#include + +#include +#include + +/* + * Namespace flag globals to avoid collisions in the vx multicall binary. + * cmp/extern.h defines identically-named globals. + */ +#define lflag diff_lflag +#define sflag diff_sflag + +/* + * Output format options + */ +#define D_NORMAL 0 /* Normal output */ +#define D_EDIT -1 /* Editor script out */ +#define D_REVERSE 1 /* Reverse editor script */ +#define D_CONTEXT 2 /* Diff with context */ +#define D_UNIFIED 3 /* Unified context diff */ +#define D_IFDEF 4 /* Diff with merged #ifdef's */ +#define D_NREVERSE 5 /* Reverse ed script with numbered + lines and no trailing . */ +#define D_BRIEF 6 /* Say if the files differ */ +#define D_GFORMAT 7 /* Diff with defined changed group format */ +#define D_SIDEBYSIDE 8 /* Side by side */ + +#define D_UNSET -2 + + +/* + * Output flags + */ +#define D_HEADER 0x001 /* Print a header/footer between files */ +#define D_EMPTY1 0x002 /* Treat first file as empty (/dev/null) */ +#define D_EMPTY2 0x004 /* Treat second file as empty (/dev/null) */ + +/* + * Command line flags + */ +#define D_FORCEASCII 0x008 /* Treat file as ascii regardless of content */ +#define D_FOLDBLANKS 0x010 /* Treat all white space as equal */ +#define D_MINIMAL 0x020 /* Make diff as small as possible */ +#define D_IGNORECASE 0x040 /* Case-insensitive matching */ +#define D_PROTOTYPE 0x080 /* Display C function prototype */ +#define D_EXPANDTABS 0x100 /* Expand tabs to spaces */ +#define D_IGNOREBLANKS 0x200 /* Ignore white space changes */ +#define D_STRIPCR 0x400 /* Strip trailing cr */ +#define D_SKIPBLANKLINES 0x800 /* Skip blank lines */ +#define D_MATCHLAST 0x1000 /* Display last line matching provided regex */ + +/* + * Status values for print_status() and diffreg() return values + */ +#define D_SAME 0 /* Files are the same */ +#define D_DIFFER 1 /* Files are different */ +#define D_BINARY 2 /* Binary files are different */ +#define D_MISMATCH1 3 /* path1 was a dir, path2 a file */ +#define D_MISMATCH2 4 /* path1 was a file, path2 a dir */ +#define D_SKIPPED1 5 /* path1 was a special file */ +#define D_SKIPPED2 6 /* path2 was a special file */ +#define D_ERROR 7 /* A file access error occurred */ + +/* + * Color options + */ +#define COLORFLAG_NEVER 0 +#define COLORFLAG_AUTO 1 +#define COLORFLAG_ALWAYS 2 + +struct excludes { + char *pattern; + struct excludes *next; +}; + +extern bool lflag, Nflag, Pflag, rflag, sflag, Tflag, cflag; +extern bool ignore_file_case, suppress_common, color, noderef; +extern int diff_format, diff_context, status; +extern int tabsize, width; +extern char *start, *ifdefname, *diffargs, *label[2]; +extern char *ignore_pats, *most_recent_pat; +extern char *group_format; +extern const char *add_code, *del_code; +extern struct stat stb1, stb2; +extern struct excludes *excludes_list; +extern regex_t ignore_re, most_recent_re; + +int diffreg(char *, char *, int, int); +void diffdir(char *, char *, int); +void print_status(int, char *, char *, const char *); diff --git a/src/diff/diffdir.c b/src/diff/diffdir.c new file mode 100644 index 0000000..c83716c --- /dev/null +++ b/src/diff/diffdir.c @@ -0,0 +1,384 @@ +/* $OpenBSD: diffdir.c,v 1.45 2015/10/05 20:15:00 millert Exp $ */ + +/* + * Copyright (c) 2003, 2010 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "diff.h" + +static int selectfile(const struct dirent *); +static void diffit(struct dirent *, char *, size_t, struct dirent *, + char *, size_t, int); +static void print_only(const char *, size_t, const char *); + +struct inode { + dev_t dev; + ino_t ino; + RB_ENTRY(inode) entry; +}; + +static int +inodecmp(struct inode *a, struct inode *b) +{ + return (a->dev < b->dev ? -1 : a->dev > b->dev ? 1 : + a->ino < b->ino ? -1 : a->ino > b->ino ? 1 : 0); +} + +RB_HEAD(inodetree, inode); +static struct inodetree v1 = RB_INITIALIZER(&v1); +static struct inodetree v2 = RB_INITIALIZER(&v2); +RB_GENERATE_STATIC(inodetree, inode, entry, inodecmp); + +static int +vscandir(struct inodetree *tree, const char *path, struct dirent ***dirp, + int (*selectf)(const struct dirent *), + int (*comparf)(const struct dirent **, const struct dirent **)) +{ + struct stat sb; + struct inode *ino = NULL; + int fd = -1, ret, serrno; + + if ((fd = open(path, O_DIRECTORY | O_RDONLY)) < 0 || + (ino = calloc(1, sizeof(*ino))) == NULL || + fstat(fd, &sb) != 0) + goto fail; + ino->dev = sb.st_dev; + ino->ino = sb.st_ino; + if (RB_FIND(inodetree, tree, ino)) { + free(ino); + close(fd); + warnx("%s: Directory loop detected", path); + *dirp = NULL; + return (0); + } + /* + * fdscandir() is FreeBSD-specific. Use scandir() on the path + * instead, since we have already verified the fd is valid. + */ + close(fd); + fd = -1; + if ((ret = scandir(path, dirp, selectf, comparf)) < 0) + goto fail; + RB_INSERT(inodetree, tree, ino); + return (ret); +fail: + serrno = errno; + if (ino != NULL) + free(ino); + if (fd >= 0) + close(fd); + errno = serrno; + return (-1); +} + +/* + * Diff directory traversal. Will be called recursively if -r was specified. + */ +void +diffdir(char *p1, char *p2, int flags) +{ + struct dirent *dent1, **dp1, **edp1, **dirp1 = NULL; + struct dirent *dent2, **dp2, **edp2, **dirp2 = NULL; + size_t dirlen1, dirlen2; + char path1[PATH_MAX], path2[PATH_MAX]; + int pos; + + edp1 = edp2 = NULL; + + dirlen1 = strlcpy(path1, *p1 ? p1 : ".", sizeof(path1)); + if (dirlen1 >= sizeof(path1) - 1) { + warnc(ENAMETOOLONG, "%s", p1); + status |= 2; + return; + } + while (dirlen1 > 1 && path1[dirlen1 - 1] == '/') + path1[--dirlen1] = '\0'; + dirlen2 = strlcpy(path2, *p2 ? p2 : ".", sizeof(path2)); + if (dirlen2 >= sizeof(path2) - 1) { + warnc(ENAMETOOLONG, "%s", p2); + status |= 2; + return; + } + while (dirlen2 > 1 && path2[dirlen2 - 1] == '/') + path2[--dirlen2] = '\0'; + + /* + * Get a list of entries in each directory, skipping "excluded" files + * and sorting alphabetically. + */ + pos = vscandir(&v1, path1, &dirp1, selectfile, alphasort); + if (pos == -1) { + if (errno == ENOENT && (Nflag || Pflag)) { + pos = 0; + } else { + warn("%s", path1); + goto closem; + } + } + dp1 = dirp1; + edp1 = dirp1 + pos; + + pos = vscandir(&v2, path2, &dirp2, selectfile, alphasort); + if (pos == -1) { + if (errno == ENOENT && Nflag) { + pos = 0; + } else { + warn("%s", path2); + goto closem; + } + } + dp2 = dirp2; + edp2 = dirp2 + pos; + + /* + * If we were given a starting point, find it. + */ + if (start != NULL) { + while (dp1 != edp1 && strcmp((*dp1)->d_name, start) < 0) + dp1++; + while (dp2 != edp2 && strcmp((*dp2)->d_name, start) < 0) + dp2++; + } + + /* + * Append separator so children's names can be appended directly. + */ + if (path1[dirlen1 - 1] != '/') { + path1[dirlen1++] = '/'; + path1[dirlen1] = '\0'; + } + if (path2[dirlen2 - 1] != '/') { + path2[dirlen2++] = '/'; + path2[dirlen2] = '\0'; + } + + /* + * Iterate through the two directory lists, diffing as we go. + */ + while (dp1 != edp1 || dp2 != edp2) { + dent1 = dp1 != edp1 ? *dp1 : NULL; + dent2 = dp2 != edp2 ? *dp2 : NULL; + + pos = dent1 == NULL ? 1 : dent2 == NULL ? -1 : + ignore_file_case ? strcasecmp(dent1->d_name, dent2->d_name) : + strcmp(dent1->d_name, dent2->d_name) ; + if (pos == 0) { + /* file exists in both dirs, diff it */ + diffit(dent1, path1, dirlen1, dent2, path2, dirlen2, flags); + dp1++; + dp2++; + } else if (pos < 0) { + /* file only in first dir, only diff if -N */ + if (Nflag) { + diffit(dent1, path1, dirlen1, dent2, path2, + dirlen2, flags); + } else { + print_only(path1, dirlen1, dent1->d_name); + status |= 1; + } + dp1++; + } else { + /* file only in second dir, only diff if -N or -P */ + if (Nflag || Pflag) + diffit(dent2, path1, dirlen1, dent1, path2, + dirlen2, flags); + else { + print_only(path2, dirlen2, dent2->d_name); + status |= 1; + } + dp2++; + } + } + +closem: + if (dirp1 != NULL) { + for (dp1 = dirp1; dp1 < edp1; dp1++) + free(*dp1); + free(dirp1); + } + if (dirp2 != NULL) { + for (dp2 = dirp2; dp2 < edp2; dp2++) + free(*dp2); + free(dirp2); + } +} + +/* + * Do the actual diff by calling either diffreg() or diffdir(). + */ +static void +diffit(struct dirent *dp, char *path1, size_t plen1, struct dirent *dp2, + char *path2, size_t plen2, int flags) +{ + int rc; + + flags |= D_HEADER; + strlcpy(path1 + plen1, dp->d_name, PATH_MAX - plen1); + + /* + * If we are ignoring file case, use dent2s name here if both names are + * the same apart from case. + */ + if (ignore_file_case && strcasecmp(dp2->d_name, dp2->d_name) == 0) + strlcpy(path2 + plen2, dp2->d_name, PATH_MAX - plen2); + else + strlcpy(path2 + plen2, dp->d_name, PATH_MAX - plen2); + + if (noderef) { + if (lstat(path1, &stb1) != 0) { + if (!(Nflag || Pflag) || errno != ENOENT) { + warn("%s", path1); + return; + } + flags |= D_EMPTY1; + memset(&stb1, 0, sizeof(stb1)); + } + if (lstat(path2, &stb2) != 0) { + if (!Nflag || errno != ENOENT) { + warn("%s", path2); + return; + } + flags |= D_EMPTY2; + memset(&stb2, 0, sizeof(stb2)); + stb2.st_mode = stb1.st_mode; + } + if (stb1.st_mode == 0) + stb1.st_mode = stb2.st_mode; + if (S_ISLNK(stb1.st_mode) || S_ISLNK(stb2.st_mode)) { + if (S_ISLNK(stb1.st_mode) && S_ISLNK(stb2.st_mode)) { + char buf1[PATH_MAX]; + char buf2[PATH_MAX]; + ssize_t len1 = 0; + ssize_t len2 = 0; + + len1 = readlink(path1, buf1, sizeof(buf1)); + len2 = readlink(path2, buf2, sizeof(buf2)); + + if (len1 < 0 || len2 < 0) { + perror("reading links"); + return; + } + buf1[len1] = '\0'; + buf2[len2] = '\0'; + + if (len1 != len2 || strncmp(buf1, buf2, len1) != 0) { + printf("Symbolic links %s and %s differ\n", + path1, path2); + status |= 1; + } + + return; + } + + printf("File %s is a %s while file %s is a %s\n", + path1, S_ISLNK(stb1.st_mode) ? "symbolic link" : + (S_ISDIR(stb1.st_mode) ? "directory" : + (S_ISREG(stb1.st_mode) ? "file" : "error")), + path2, S_ISLNK(stb2.st_mode) ? "symbolic link" : + (S_ISDIR(stb2.st_mode) ? "directory" : + (S_ISREG(stb2.st_mode) ? "file" : "error"))); + status |= 1; + return; + } + } else { + if (stat(path1, &stb1) != 0) { + if (!(Nflag || Pflag) || errno != ENOENT) { + warn("%s", path1); + return; + } + flags |= D_EMPTY1; + memset(&stb1, 0, sizeof(stb1)); + } + if (stat(path2, &stb2) != 0) { + if (!Nflag || errno != ENOENT) { + warn("%s", path2); + return; + } + flags |= D_EMPTY2; + memset(&stb2, 0, sizeof(stb2)); + stb2.st_mode = stb1.st_mode; + } + if (stb1.st_mode == 0) + stb1.st_mode = stb2.st_mode; + } + if (stb1.st_dev == stb2.st_dev && stb1.st_ino == stb2.st_ino) + return; + if (S_ISDIR(stb1.st_mode) && S_ISDIR(stb2.st_mode)) { + if (rflag) + diffdir(path1, path2, flags); + else + printf("Common subdirectories: %s and %s\n", + path1, path2); + return; + } + if (!S_ISREG(stb1.st_mode) && !S_ISDIR(stb1.st_mode)) + rc = D_SKIPPED1; + else if (!S_ISREG(stb2.st_mode) && !S_ISDIR(stb2.st_mode)) + rc = D_SKIPPED2; + else + rc = diffreg(path1, path2, flags, 0); + print_status(rc, path1, path2, ""); +} + +/* + * Returns 1 if the directory entry should be included in the + * diff, else 0. Checks the excludes list. + */ +static int +selectfile(const struct dirent *dp) +{ + struct excludes *excl; + + if (dp->d_fileno == 0) + return (0); + + /* always skip "." and ".." */ + if (dp->d_name[0] == '.' && (dp->d_name[1] == '\0' || + (dp->d_name[1] == '.' && dp->d_name[2] == '\0'))) + return (0); + + /* check excludes list */ + for (excl = excludes_list; excl != NULL; excl = excl->next) + if (fnmatch(excl->pattern, dp->d_name, FNM_PATHNAME) == 0) + return (0); + + return (1); +} + +void +print_only(const char *path, size_t dirlen, const char *entry) +{ + if (dirlen > 1) + dirlen--; + printf("Only in %.*s: %s\n", (int)dirlen, path, entry); +} diff --git a/src/diff/diffreg.c b/src/diff/diffreg.c new file mode 100644 index 0000000..3621739 --- /dev/null +++ b/src/diff/diffreg.c @@ -0,0 +1,1722 @@ +/* $OpenBSD: diffreg.c,v 1.93 2019/06/28 13:35:00 deraadt Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-4-Clause + * + * Copyright (C) Caldera International Inc. 2001-2002. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code and documentation must retain the above + * copyright notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed or owned by Caldera + * International, Inc. + * 4. Neither the name of Caldera International, Inc. nor the names of other + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * USE OF THE SOFTWARE PROVIDED FOR UNDER THIS LICENSE BY CALDERA + * INTERNATIONAL, INC. 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 CALDERA INTERNATIONAL, INC. 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. + */ +/*- + * Copyright (c) 1991, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + * + * @(#)diffreg.c 8.1 (Berkeley) 6/6/93 + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pr.h" +#include "diff.h" +#include "xmalloc.h" + +/* + * diff - compare two files. + */ + +/* + * Uses an algorithm due to Harold Stone, which finds a pair of longest + * identical subsequences in the two files. + * + * The major goal is to generate the match vector J. J[i] is the index of + * the line in file1 corresponding to line i file0. J[i] = 0 if there is no + * such line in file1. + * + * Lines are hashed so as to work in core. All potential matches are + * located by sorting the lines of each file on the hash (called + * ``value''). In particular, this collects the equivalence classes in + * file1 together. Subroutine equiv replaces the value of each line in + * file0 by the index of the first element of its matching equivalence in + * (the reordered) file1. To save space equiv squeezes file1 into a single + * array member in which the equivalence classes are simply concatenated, + * except that their first members are flagged by changing sign. + * + * Next the indices that point into member are unsorted into array class + * according to the original order of file0. + * + * The cleverness lies in routine stone. This marches through the lines of + * file0, developing a vector klist of "k-candidates". At step i + * a k-candidate is a matched pair of lines x,y (x in file0 y in file1) + * such that there is a common subsequence of length k between the first + * i lines of file0 and the first y lines of file1, but there is no such + * subsequence for any smaller y. x is the earliest possible mate to y that + * occurs in such a subsequence. + * + * Whenever any of the members of the equivalence class of lines in file1 + * matable to a line in file0 has serial number less than the y of some + * k-candidate, that k-candidate with the smallest such y is replaced. The + * new k-candidate is chained (via pred) to the current k-1 candidate so + * that the actual subsequence can be recovered. When a member has serial + * number greater that the y of all k-candidates, the klist is extended. At + * the end, the longest subsequence is pulled out and placed in the array J + * by unravel. + * + * With J in hand, the matches there recorded are check'ed against reality + * to assure that no spurious matches have crept in due to hashing. If they + * have, they are broken, and "jackpot" is recorded -- a harmless matter + * except that a true match for a spuriously mated line may now be + * unnecessarily reported as a change. + * + * Much of the complexity of the program comes simply from trying to + * minimize core utilization and maximize the range of doable problems by + * dynamically allocating what is needed and reusing what is not. The core + * requirements for problems larger than somewhat are (in words) + * 2*length(file0) + length(file1) + 3*(number of k-candidates installed), + * typically about 6n words for files of length n. + */ + +struct cand { + int x; + int y; + int pred; +}; + +static struct line { + int serial; + int value; +} *file[2]; + +/* + * The following struct is used to record change information when + * doing a "context" or "unified" diff. (see routine "change" to + * understand the highly mnemonic field names) + */ +struct context_vec { + int a; /* start line in old file */ + int b; /* end line in old file */ + int c; /* start line in new file */ + int d; /* end line in new file */ +}; + +enum readhash { RH_BINARY, RH_OK, RH_EOF }; + +static FILE *opentemp(const char *); +static void output(char *, FILE *, char *, FILE *, int); +static void check(FILE *, FILE *, int); +static void range(int, int, const char *); +static void uni_range(int, int); +static void dump_context_vec(FILE *, FILE *, int); +static void dump_unified_vec(FILE *, FILE *, int); +static bool prepare(int, FILE *, size_t, int); +static void prune(void); +static void equiv(struct line *, int, struct line *, int, int *); +static void unravel(int); +static void unsort(struct line *, int, int *); +static void change(char *, FILE *, char *, FILE *, int, int, int, int, int *); +static void sort(struct line *, int); +static void print_header(const char *, const char *); +static void print_space(int, int, int); +static bool ignoreline_pattern(char *); +static bool ignoreline(char *, bool); +static int asciifile(FILE *); +static int fetch(long *, int, int, FILE *, int, int, int); +static int newcand(int, int, int); +static int search(int *, int, int); +static int skipline(FILE *); +static int stone(int *, int, int *, int *, int); +static enum readhash readhash(FILE *, int, unsigned *); +static int files_differ(FILE *, FILE *, int); +static char *match_function(const long *, int, FILE *); +static char *preadline(int, size_t, off_t); + +static int *J; /* will be overlaid on class */ +static int *class; /* will be overlaid on file[0] */ +static int *klist; /* will be overlaid on file[0] after class */ +static int *member; /* will be overlaid on file[1] */ +static int clen; +static int inifdef; /* whether or not we are in a #ifdef block */ +static size_t len[2]; /* lengths of files in lines */ +static size_t pref, suff; /* lengths of prefix and suffix */ +static size_t slen[2]; /* lengths of files minus pref / suff */ +static int anychange; +static int hw, lpad, rpad; /* half width and padding */ +static int edoffset; +static long *ixnew; /* will be overlaid on file[1] */ +static long *ixold; /* will be overlaid on klist */ +static struct cand *clist; /* merely a free storage pot for candidates */ +static int clistlen; /* the length of clist */ +static struct line *sfile[2]; /* shortened by pruning common prefix/suffix */ +static int (*chrtran)(int); /* translation table for case-folding */ +static struct context_vec *context_vec_start; +static struct context_vec *context_vec_end; +static struct context_vec *context_vec_ptr; + +#define FUNCTION_CONTEXT_SIZE 55 +static char lastbuf[FUNCTION_CONTEXT_SIZE]; +static int lastline; +static int lastmatchline; + +static int +clow2low(int c) +{ + + return (c); +} + +static int +cup2low(int c) +{ + + return (tolower(c)); +} + +int +diffreg(char *file1, char *file2, int flags, int capsicum) +{ + FILE *f1, *f2; + int i, rval; + struct pr *pr = NULL; + cap_rights_t rights_ro; + + f1 = f2 = NULL; + rval = D_SAME; + anychange = 0; + lastline = 0; + lastmatchline = 0; + + /* + * In side-by-side mode, we need to print the left column, a + * change marker surrounded by padding, and the right column. + * + * If expanding tabs, we don't care about alignment, so we simply + * subtract 3 from the width and divide by two. + * + * If not expanding tabs, we need to ensure that the right column + * is aligned to a tab stop. We start with the same formula, then + * decrement until we reach a size that lets us tab-align the + * right column. We then adjust the width down if necessary for + * the padding calculation to work. + * + * Left padding is half the space left over, rounded down; right + * padding is whatever is needed to match the width. + */ + if (diff_format == D_SIDEBYSIDE) { + if (flags & D_EXPANDTABS) { + if (width > 3) { + hw = (width - 3) / 2; + } else { + /* not enough space */ + hw = 0; + } + } else if (width <= 3 || width <= tabsize) { + /* not enough space */ + hw = 0; + } else { + hw = (width - 3) / 2; + while (hw > 0 && roundup(hw + 3, tabsize) + hw > width) + hw--; + if (width - (roundup(hw + 3, tabsize) + hw) < tabsize) + width = roundup(hw + 3, tabsize) + hw; + } + lpad = (width - hw * 2 - 1) / 2; + rpad = (width - hw * 2 - 1) - lpad; + } + + if (flags & D_IGNORECASE) + chrtran = cup2low; + else + chrtran = clow2low; + if (S_ISDIR(stb1.st_mode) != S_ISDIR(stb2.st_mode)) + return (S_ISDIR(stb1.st_mode) ? D_MISMATCH1 : D_MISMATCH2); + if (strcmp(file1, "-") == 0 && strcmp(file2, "-") == 0) + goto closem; + + if (flags & D_EMPTY1) + f1 = fopen(_PATH_DEVNULL, "r"); + else { + if (!S_ISREG(stb1.st_mode)) { + if ((f1 = opentemp(file1)) == NULL || + fstat(fileno(f1), &stb1) == -1) { + warn("%s", file1); + rval = D_ERROR; + status |= 2; + goto closem; + } + } else if (strcmp(file1, "-") == 0) + f1 = stdin; + else + f1 = fopen(file1, "r"); + } + if (f1 == NULL) { + warn("%s", file1); + rval = D_ERROR; + status |= 2; + goto closem; + } + + if (flags & D_EMPTY2) + f2 = fopen(_PATH_DEVNULL, "r"); + else { + if (!S_ISREG(stb2.st_mode)) { + if ((f2 = opentemp(file2)) == NULL || + fstat(fileno(f2), &stb2) == -1) { + warn("%s", file2); + rval = D_ERROR; + status |= 2; + goto closem; + } + } else if (strcmp(file2, "-") == 0) + f2 = stdin; + else + f2 = fopen(file2, "r"); + } + if (f2 == NULL) { + warn("%s", file2); + rval = D_ERROR; + status |= 2; + goto closem; + } + + if (stb1.st_dev == stb2.st_dev && stb1.st_ino == stb2.st_ino) + goto closem; + + if (lflag) + pr = start_pr(file1, file2); + + if (capsicum) { + cap_rights_init(&rights_ro, CAP_READ, CAP_FSTAT, CAP_SEEK); + if (caph_rights_limit(fileno(f1), &rights_ro) < 0) + err(2, "unable to limit rights on: %s", file1); + if (caph_rights_limit(fileno(f2), &rights_ro) < 0) + err(2, "unable to limit rights on: %s", file2); + if (fileno(f1) == STDIN_FILENO || fileno(f2) == STDIN_FILENO) { + /* stdin has already been limited */ + if (caph_limit_stderr() == -1) + err(2, "unable to limit stderr"); + if (caph_limit_stdout() == -1) + err(2, "unable to limit stdout"); + } else if (caph_limit_stdio() == -1) + err(2, "unable to limit stdio"); + + caph_cache_catpages(); + caph_cache_tzdata(); + if (caph_enter() < 0) + err(2, "unable to enter capability mode"); + } + + switch (files_differ(f1, f2, flags)) { + case 0: + goto closem; + case 1: + break; + default: + /* error */ + if (ferror(f1)) + warn("%s", file1); + if (ferror(f2)) + warn("%s", file2); + rval = D_ERROR; + status |= 2; + goto closem; + } + + if (diff_format == D_BRIEF && ignore_pats == NULL && + (flags & (D_FOLDBLANKS|D_IGNOREBLANKS|D_IGNORECASE| + D_SKIPBLANKLINES|D_STRIPCR)) == 0) + { + rval = D_DIFFER; + status |= 1; + goto closem; + } + if ((flags & D_FORCEASCII) != 0) { + (void)prepare(0, f1, stb1.st_size, flags); + (void)prepare(1, f2, stb2.st_size, flags); + } else if (!asciifile(f1) || !asciifile(f2) || + !prepare(0, f1, stb1.st_size, flags) || + !prepare(1, f2, stb2.st_size, flags)) { + rval = D_BINARY; + status |= 1; + goto closem; + } + if (len[0] > INT_MAX - 2) + errc(1, EFBIG, "%s", file1); + if (len[1] > INT_MAX - 2) + errc(1, EFBIG, "%s", file2); + + prune(); + sort(sfile[0], slen[0]); + sort(sfile[1], slen[1]); + + member = (int *)file[1]; + equiv(sfile[0], slen[0], sfile[1], slen[1], member); + member = xreallocarray(member, slen[1] + 2, sizeof(*member)); + + class = (int *)file[0]; + unsort(sfile[0], slen[0], class); + class = xreallocarray(class, slen[0] + 2, sizeof(*class)); + + klist = xcalloc(slen[0] + 2, sizeof(*klist)); + clen = 0; + clistlen = 100; + clist = xcalloc(clistlen, sizeof(*clist)); + i = stone(class, slen[0], member, klist, flags); + free(member); + free(class); + + J = xreallocarray(J, len[0] + 2, sizeof(*J)); + unravel(klist[i]); + free(clist); + free(klist); + + ixold = xreallocarray(ixold, len[0] + 2, sizeof(*ixold)); + ixnew = xreallocarray(ixnew, len[1] + 2, sizeof(*ixnew)); + check(f1, f2, flags); + output(file1, f1, file2, f2, flags); + +closem: + if (pr != NULL) + stop_pr(pr); + if (anychange) { + status |= 1; + if (rval == D_SAME) + rval = D_DIFFER; + } + if (f1 != NULL) + fclose(f1); + if (f2 != NULL) + fclose(f2); + + return (rval); +} + +/* + * Check to see if the given files differ. + * Returns 0 if they are the same, 1 if different, and -1 on error. + * XXX - could use code from cmp(1) [faster] + */ +static int +files_differ(FILE *f1, FILE *f2, int flags) +{ + char buf1[BUFSIZ], buf2[BUFSIZ]; + size_t i, j; + + if ((flags & (D_EMPTY1|D_EMPTY2)) || stb1.st_size != stb2.st_size || + (stb1.st_mode & S_IFMT) != (stb2.st_mode & S_IFMT)) + return (1); + + if (stb1.st_dev == stb2.st_dev && stb1.st_ino == stb2.st_ino) + return (0); + + for (;;) { + if ((i = fread(buf1, 1, sizeof(buf1), f1)) == 0 && ferror(f1)) + return (-1); + if ((j = fread(buf2, 1, sizeof(buf2), f2)) == 0 && ferror(f2)) + return (-1); + if (i != j) + return (1); + if (i == 0) + return (0); + if (memcmp(buf1, buf2, i) != 0) + return (1); + } +} + +static FILE * +opentemp(const char *f) +{ + char buf[BUFSIZ], tempfile[PATH_MAX]; + ssize_t nread; + int ifd, ofd; + + if (strcmp(f, "-") == 0) + ifd = STDIN_FILENO; + else if ((ifd = open(f, O_RDONLY, 0644)) == -1) + return (NULL); + + (void)strlcpy(tempfile, _PATH_TMP "/diff.XXXXXXXX", sizeof(tempfile)); + + if ((ofd = mkstemp(tempfile)) == -1) { + close(ifd); + return (NULL); + } + unlink(tempfile); + while ((nread = read(ifd, buf, BUFSIZ)) > 0) { + if (write(ofd, buf, nread) != nread) { + close(ifd); + close(ofd); + return (NULL); + } + } + close(ifd); + lseek(ofd, (off_t)0, SEEK_SET); + return (fdopen(ofd, "r")); +} + +static bool +prepare(int i, FILE *fd, size_t filesize, int flags) +{ + struct line *p; + unsigned h; + size_t sz, j = 0; + enum readhash r; + + rewind(fd); + + sz = MIN(filesize, SIZE_MAX) / 25; + if (sz < 100) + sz = 100; + + p = xcalloc(sz + 3, sizeof(*p)); + while ((r = readhash(fd, flags, &h)) != RH_EOF) { + if (r == RH_BINARY) + return (false); + if (j == SIZE_MAX) + break; + if (j == sz) { + sz = sz * 3 / 2; + p = xreallocarray(p, sz + 3, sizeof(*p)); + } + p[++j].value = h; + } + + len[i] = j; + file[i] = p; + + return (true); +} + +static void +prune(void) +{ + size_t i, j; + + for (pref = 0; pref < len[0] && pref < len[1] && + file[0][pref + 1].value == file[1][pref + 1].value; + pref++) + ; + for (suff = 0; suff < len[0] - pref && suff < len[1] - pref && + file[0][len[0] - suff].value == file[1][len[1] - suff].value; + suff++) + ; + for (j = 0; j < 2; j++) { + sfile[j] = file[j] + pref; + slen[j] = len[j] - pref - suff; + for (i = 0; i <= slen[j]; i++) + sfile[j][i].serial = i; + } +} + +static void +equiv(struct line *a, int n, struct line *b, int m, int *c) +{ + int i, j; + + i = j = 1; + while (i <= n && j <= m) { + if (a[i].value < b[j].value) + a[i++].value = 0; + else if (a[i].value == b[j].value) + a[i++].value = j; + else + j++; + } + while (i <= n) + a[i++].value = 0; + b[m + 1].value = 0; + j = 0; + while (++j <= m) { + c[j] = -b[j].serial; + while (b[j + 1].value == b[j].value) { + j++; + c[j] = b[j].serial; + } + } + c[j] = -1; +} + +static int +stone(int *a, int n, int *b, int *c, int flags) +{ + int i, k, y, j, l; + int oldc, tc, oldl, sq; + unsigned numtries, bound; + + if (flags & D_MINIMAL) + bound = UINT_MAX; + else { + sq = sqrt(n); + bound = MAX(256, sq); + } + + k = 0; + c[0] = newcand(0, 0, 0); + for (i = 1; i <= n; i++) { + j = a[i]; + if (j == 0) + continue; + y = -b[j]; + oldl = 0; + oldc = c[0]; + numtries = 0; + do { + if (y <= clist[oldc].y) + continue; + l = search(c, k, y); + if (l != oldl + 1) + oldc = c[l - 1]; + if (l <= k) { + if (clist[c[l]].y <= y) + continue; + tc = c[l]; + c[l] = newcand(i, y, oldc); + oldc = tc; + oldl = l; + numtries++; + } else { + c[l] = newcand(i, y, oldc); + k++; + break; + } + } while ((y = b[++j]) > 0 && numtries < bound); + } + return (k); +} + +static int +newcand(int x, int y, int pred) +{ + struct cand *q; + + if (clen == clistlen) { + clistlen = clistlen * 11 / 10; + clist = xreallocarray(clist, clistlen, sizeof(*clist)); + } + q = clist + clen; + q->x = x; + q->y = y; + q->pred = pred; + return (clen++); +} + +static int +search(int *c, int k, int y) +{ + int i, j, l, t; + + if (clist[c[k]].y < y) /* quick look for typical case */ + return (k + 1); + i = 0; + j = k + 1; + for (;;) { + l = (i + j) / 2; + if (l <= i) + break; + t = clist[c[l]].y; + if (t > y) + j = l; + else if (t < y) + i = l; + else + return (l); + } + return (l + 1); +} + +static void +unravel(int p) +{ + struct cand *q; + size_t i; + + for (i = 0; i <= len[0]; i++) + J[i] = i <= pref ? i : + i > len[0] - suff ? i + len[1] - len[0] : 0; + for (q = clist + p; q->y != 0; q = clist + q->pred) + J[q->x + pref] = q->y + pref; +} + +/* + * Check does double duty: + * 1. ferret out any fortuitous correspondences due to confounding by + * hashing (which result in "jackpot") + * 2. collect random access indexes to the two files + */ +static void +check(FILE *f1, FILE *f2, int flags) +{ + int i, j, /* jackpot, */ c, d; + long ctold, ctnew; + + rewind(f1); + rewind(f2); + j = 1; + ixold[0] = ixnew[0] = 0; + /* jackpot = 0; */ + ctold = ctnew = 0; + for (i = 1; i <= (int)len[0]; i++) { + if (J[i] == 0) { + ixold[i] = ctold += skipline(f1); + continue; + } + while (j < J[i]) { + ixnew[j] = ctnew += skipline(f2); + j++; + } + if (flags & (D_FOLDBLANKS | D_IGNOREBLANKS | D_IGNORECASE | D_STRIPCR)) { + for (;;) { + c = getc(f1); + d = getc(f2); + /* + * GNU diff ignores a missing newline + * in one file for -b or -w. + */ + if (flags & (D_FOLDBLANKS | D_IGNOREBLANKS)) { + if (c == EOF && d == '\n') { + ctnew++; + break; + } else if (c == '\n' && d == EOF) { + ctold++; + break; + } + } + ctold++; + ctnew++; + if (flags & D_STRIPCR && (c == '\r' || d == '\r')) { + if (c == '\r') { + if ((c = getc(f1)) == '\n') { + ctold++; + } else { + ungetc(c, f1); + } + } + if (d == '\r') { + if ((d = getc(f2)) == '\n') { + ctnew++; + } else { + ungetc(d, f2); + } + } + break; + } + if ((flags & D_FOLDBLANKS) && isspace(c) && + isspace(d)) { + do { + if (c == '\n') + break; + ctold++; + } while (isspace(c = getc(f1))); + do { + if (d == '\n') + break; + ctnew++; + } while (isspace(d = getc(f2))); + } else if (flags & D_IGNOREBLANKS) { + while (isspace(c) && c != '\n') { + c = getc(f1); + ctold++; + } + while (isspace(d) && d != '\n') { + d = getc(f2); + ctnew++; + } + } + if (chrtran(c) != chrtran(d)) { + /* jackpot++; */ + J[i] = 0; + if (c != '\n' && c != EOF) + ctold += skipline(f1); + if (d != '\n' && c != EOF) + ctnew += skipline(f2); + break; + } + if (c == '\n' || c == EOF) + break; + } + } else { + for (;;) { + ctold++; + ctnew++; + if ((c = getc(f1)) != (d = getc(f2))) { + /* jackpot++; */ + J[i] = 0; + if (c != '\n' && c != EOF) + ctold += skipline(f1); + if (d != '\n' && c != EOF) + ctnew += skipline(f2); + break; + } + if (c == '\n' || c == EOF) + break; + } + } + ixold[i] = ctold; + ixnew[j] = ctnew; + j++; + } + for (; j <= (int)len[1]; j++) { + ixnew[j] = ctnew += skipline(f2); + } + /* + * if (jackpot) + * fprintf(stderr, "jackpot\n"); + */ +} + +/* shellsort CACM #201 */ +static void +sort(struct line *a, int n) +{ + struct line *ai, *aim, w; + int j, m = 0, k; + + if (n == 0) + return; + for (j = 1; j <= n; j *= 2) + m = 2 * j - 1; + for (m /= 2; m != 0; m /= 2) { + k = n - m; + for (j = 1; j <= k; j++) { + for (ai = &a[j]; ai > a; ai -= m) { + aim = &ai[m]; + if (aim < ai) + break; /* wraparound */ + if (aim->value > ai[0].value || + (aim->value == ai[0].value && + aim->serial > ai[0].serial)) + break; + w.value = ai[0].value; + ai[0].value = aim->value; + aim->value = w.value; + w.serial = ai[0].serial; + ai[0].serial = aim->serial; + aim->serial = w.serial; + } + } + } +} + +static void +unsort(struct line *f, int l, int *b) +{ + int *a, i; + + a = xcalloc(l + 1, sizeof(*a)); + for (i = 1; i <= l; i++) + a[f[i].serial] = f[i].value; + for (i = 1; i <= l; i++) + b[i] = a[i]; + free(a); +} + +static int +skipline(FILE *f) +{ + int i, c; + + for (i = 1; (c = getc(f)) != '\n' && c != EOF; i++) + continue; + return (i); +} + +static void +output(char *file1, FILE *f1, char *file2, FILE *f2, int flags) +{ + int i, j, m, i0, i1, j0, j1, nc; + + rewind(f1); + rewind(f2); + m = len[0]; + J[0] = 0; + J[m + 1] = len[1] + 1; + if (diff_format != D_EDIT) { + for (i0 = 1; i0 <= m; i0 = i1 + 1) { + while (i0 <= m && J[i0] == J[i0 - 1] + 1) { + if (diff_format == D_SIDEBYSIDE && suppress_common != 1) { + nc = fetch(ixold, i0, i0, f1, '\0', 1, flags); + print_space(nc, hw - nc + lpad + 1 + rpad, flags); + fetch(ixnew, J[i0], J[i0], f2, '\0', 0, flags); + printf("\n"); + } + i0++; + } + j0 = J[i0 - 1] + 1; + i1 = i0 - 1; + while (i1 < m && J[i1 + 1] == 0) + i1++; + j1 = J[i1 + 1] - 1; + J[i1] = j1; + + /* + * When using side-by-side, lines from both of the files are + * printed. The algorithm used by diff(1) identifies the ranges + * in which two files differ. + * See the change() function below. + * The for loop below consumes the shorter range, whereas one of + * the while loops deals with the longer one. + */ + if (diff_format == D_SIDEBYSIDE) { + for (i = i0, j = j0; i <= i1 && j <= j1; i++, j++) + change(file1, f1, file2, f2, i, i, j, j, &flags); + + while (i <= i1) { + change(file1, f1, file2, f2, i, i, j + 1, j, &flags); + i++; + } + + while (j <= j1) { + change(file1, f1, file2, f2, i + 1, i, j, j, &flags); + j++; + } + } else + change(file1, f1, file2, f2, i0, i1, j0, j1, &flags); + } + } else { + for (i0 = m; i0 >= 1; i0 = i1 - 1) { + while (i0 >= 1 && J[i0] == J[i0 + 1] - 1 && J[i0] != 0) + i0--; + j0 = J[i0 + 1] - 1; + i1 = i0 + 1; + while (i1 > 1 && J[i1 - 1] == 0) + i1--; + j1 = J[i1 - 1] + 1; + J[i1] = j1; + change(file1, f1, file2, f2, i1, i0, j1, j0, &flags); + } + } + if (m == 0) + change(file1, f1, file2, f2, 1, 0, 1, len[1], &flags); + if (diff_format == D_IFDEF || diff_format == D_GFORMAT) { + for (;;) { +#define c i0 + if ((c = getc(f1)) == EOF) + return; + printf("%c", c); + } +#undef c + } + if (anychange != 0) { + if (diff_format == D_CONTEXT) + dump_context_vec(f1, f2, flags); + else if (diff_format == D_UNIFIED) + dump_unified_vec(f1, f2, flags); + } +} + +static void +range(int a, int b, const char *separator) +{ + printf("%d", a > b ? b : a); + if (a < b) + printf("%s%d", separator, b); +} + +static void +uni_range(int a, int b) +{ + if (a < b) + printf("%d,%d", a, b - a + 1); + else if (a == b) + printf("%d", b); + else + printf("%d,0", b); +} + +static char * +preadline(int fd, size_t rlen, off_t off) +{ + char *line; + ssize_t nr; + + line = xmalloc(rlen + 1); + if ((nr = pread(fd, line, rlen, off)) == -1) + err(2, "preadline"); + if (nr > 0 && line[nr-1] == '\n') + nr--; + line[nr] = '\0'; + return (line); +} + +static bool +ignoreline_pattern(char *line) +{ + int ret; + + ret = regexec(&ignore_re, line, 0, NULL, 0); + return (ret == 0); /* if it matched, it should be ignored. */ +} + +static bool +ignoreline(char *line, bool skip_blanks) +{ + + if (skip_blanks && *line == '\0') + return (true); + if (ignore_pats != NULL && ignoreline_pattern(line)) + return (true); + return (false); +} + +/* + * Indicate that there is a difference between lines a and b of the from file + * to get to lines c to d of the to file. If a is greater then b then there + * are no lines in the from file involved and this means that there were + * lines appended (beginning at b). If c is greater than d then there are + * lines missing from the to file. + */ +static void +change(char *file1, FILE *f1, char *file2, FILE *f2, int a, int b, int c, int d, + int *pflags) +{ + static size_t max_context = 64; + long curpos; + int dist, i, nc; + const char *walk; + bool skip_blanks, ignore; + + skip_blanks = (*pflags & D_SKIPBLANKLINES); +restart: + if ((diff_format != D_IFDEF || diff_format == D_GFORMAT) && + a > b && c > d) + return; + if (ignore_pats != NULL || skip_blanks) { + char *line; + /* + * All lines in the change, insert, or delete must match an ignore + * pattern for the change to be ignored. + */ + if (a <= b) { /* Changes and deletes. */ + for (i = a; i <= b; i++) { + line = preadline(fileno(f1), + ixold[i] - ixold[i - 1], ixold[i - 1]); + ignore = ignoreline(line, skip_blanks); + free(line); + if (!ignore) + goto proceed; + } + } + if (a > b || c <= d) { /* Changes and inserts. */ + for (i = c; i <= d; i++) { + line = preadline(fileno(f2), + ixnew[i] - ixnew[i - 1], ixnew[i - 1]); + ignore = ignoreline(line, skip_blanks); + free(line); + if (!ignore) + goto proceed; + } + } + return; + } +proceed: + if (*pflags & D_HEADER && diff_format != D_BRIEF) { + printf("%s %s %s\n", diffargs, file1, file2); + *pflags &= ~D_HEADER; + } + if (diff_format == D_CONTEXT || diff_format == D_UNIFIED) { + /* + * Allocate change records as needed. + */ + if (context_vec_start == NULL || + context_vec_ptr == context_vec_end - 1) { + ptrdiff_t offset = -1; + + if (context_vec_start != NULL) + offset = context_vec_ptr - context_vec_start; + max_context <<= 1; + context_vec_start = xreallocarray(context_vec_start, + max_context, sizeof(*context_vec_start)); + context_vec_end = context_vec_start + max_context; + context_vec_ptr = context_vec_start + offset; + } + if (anychange == 0) { + /* + * Print the context/unidiff header first time through. + */ + print_header(file1, file2); + anychange = 1; + } else if (!ckd_add(&dist, diff_context, diff_context) && + a - context_vec_ptr->b - 1 > dist && + c - context_vec_ptr->d - 1 > dist) { + /* + * If this change is more than 'diff_context' lines from the + * previous change, dump the record and reset it. + */ + if (diff_format == D_CONTEXT) + dump_context_vec(f1, f2, *pflags); + else + dump_unified_vec(f1, f2, *pflags); + } + context_vec_ptr++; + context_vec_ptr->a = a; + context_vec_ptr->b = b; + context_vec_ptr->c = c; + context_vec_ptr->d = d; + return; + } + if (anychange == 0) + anychange = 1; + switch (diff_format) { + case D_BRIEF: + return; + case D_NORMAL: + case D_EDIT: + range(a, b, ","); + printf("%c", a > b ? 'a' : c > d ? 'd' : 'c'); + if (diff_format == D_NORMAL) + range(c, d, ","); + printf("\n"); + break; + case D_REVERSE: + printf("%c", a > b ? 'a' : c > d ? 'd' : 'c'); + range(a, b, " "); + printf("\n"); + break; + case D_NREVERSE: + if (a > b) + printf("a%d %d\n", b, d - c + 1); + else { + printf("d%d %d\n", a, b - a + 1); + if (!(c > d)) + /* add changed lines */ + printf("a%d %d\n", b, d - c + 1); + } + break; + } + if (diff_format == D_GFORMAT) { + curpos = ftell(f1); + /* print through if append (a>b), else to (nb: 0 vs 1 orig) */ + nc = ixold[a > b ? b : a - 1] - curpos; + for (i = 0; i < nc; i++) + printf("%c", getc(f1)); + for (walk = group_format; *walk != '\0'; walk++) { + if (*walk == '%') { + walk++; + switch (*walk) { + case '<': + fetch(ixold, a, b, f1, '<', 1, *pflags); + break; + case '>': + fetch(ixnew, c, d, f2, '>', 0, *pflags); + break; + default: + printf("%%%c", *walk); + break; + } + continue; + } + printf("%c", *walk); + } + } + if (diff_format == D_SIDEBYSIDE) { + if (color && a > b) + printf("\033[%sm", add_code); + else if (color && c > d) + printf("\033[%sm", del_code); + if (a > b) { + print_space(0, hw + lpad, *pflags); + } else { + nc = fetch(ixold, a, b, f1, '\0', 1, *pflags); + print_space(nc, hw - nc + lpad, *pflags); + } + if (color && a > b) + printf("\033[%sm", add_code); + else if (color && c > d) + printf("\033[%sm", del_code); + printf("%c", (a > b) ? '>' : ((c > d) ? '<' : '|')); + if (color && c > d) + printf("\033[m"); + print_space(hw + lpad + 1, rpad, *pflags); + fetch(ixnew, c, d, f2, '\0', 0, *pflags); + printf("\n"); + } + if (diff_format == D_NORMAL || diff_format == D_IFDEF) { + fetch(ixold, a, b, f1, '<', 1, *pflags); + if (a <= b && c <= d && diff_format == D_NORMAL) + printf("---\n"); + } + if (diff_format != D_GFORMAT && diff_format != D_SIDEBYSIDE) + fetch(ixnew, c, d, f2, diff_format == D_NORMAL ? '>' : '\0', 0, *pflags); + if (edoffset != 0 && diff_format == D_EDIT) { + /* + * A non-zero edoffset value for D_EDIT indicates that the last line + * printed was a bare dot (".") that has been escaped as ".." to + * prevent ed(1) from misinterpreting it. We have to add a + * substitute command to change this back and restart where we left + * off. + */ + printf(".\n"); + printf("%ds/.//\n", a + edoffset - 1); + b = a + edoffset - 1; + a = b + 1; + c += edoffset; + goto restart; + } + if ((diff_format == D_EDIT || diff_format == D_REVERSE) && c <= d) + printf(".\n"); + if (inifdef) { + printf("#endif /* %s */\n", ifdefname); + inifdef = 0; + } +} + +static int +fetch(long *f, int a, int b, FILE *lb, int ch, int oldfile, int flags) +{ + int i, j, c, lastc, col, nc, newcol; + + edoffset = 0; + nc = 0; + /* + * When doing #ifdef's, copy down to current line + * if this is the first file, so that stuff makes it to output. + */ + if ((diff_format == D_IFDEF) && oldfile) { + long curpos = ftell(lb); + /* print through if append (a>b), else to (nb: 0 vs 1 orig) */ + nc = f[a > b ? b : a - 1] - curpos; + for (i = 0; i < nc; i++) + printf("%c", getc(lb)); + } + if (a > b) + return (0); + if (diff_format == D_IFDEF) { + if (inifdef) { + printf("#else /* %s%s */\n", + oldfile == 1 ? "!" : "", ifdefname); + } else { + if (oldfile) + printf("#ifndef %s\n", ifdefname); + else + printf("#ifdef %s\n", ifdefname); + } + inifdef = 1 + oldfile; + } + for (i = a; i <= b; i++) { + fseek(lb, f[i - 1], SEEK_SET); + nc = f[i] - f[i - 1]; + if (diff_format == D_SIDEBYSIDE && hw < nc) + nc = hw; + if (diff_format != D_IFDEF && diff_format != D_GFORMAT && + ch != '\0') { + if (color && (ch == '>' || ch == '+')) + printf("\033[%sm", add_code); + else if (color && (ch == '<' || ch == '-')) + printf("\033[%sm", del_code); + printf("%c", ch); + if (Tflag && (diff_format == D_NORMAL || + diff_format == D_CONTEXT || + diff_format == D_UNIFIED)) + printf("\t"); + else if (diff_format != D_UNIFIED) + printf(" "); + } + col = j = 0; + lastc = '\0'; + while (j < nc && (hw == 0 || col < hw)) { + c = getc(lb); + if (flags & D_STRIPCR && c == '\r') { + if ((c = getc(lb)) == '\n') + j++; + else { + ungetc(c, lb); + c = '\r'; + } + } + if (c == EOF) { + if (diff_format == D_EDIT || + diff_format == D_REVERSE || + diff_format == D_NREVERSE) + warnx("No newline at end of file"); + else + printf("\n\\ No newline at end of file\n"); + return (col); + } + if (c == '\t') { + /* + * Calculate where the tab would bring us. + * If it would take us to the end of the + * column, either clip it (if expanding + * tabs) or return right away (if not). + */ + newcol = roundup(col + 1, tabsize); + if ((flags & D_EXPANDTABS) == 0) { + if (hw > 0 && newcol >= hw) + return (col); + printf("\t"); + } else { + if (hw > 0 && newcol > hw) + newcol = hw; + printf("%*s", newcol - col, ""); + } + col = newcol; + } else { + if (diff_format == D_EDIT && j == 1 && c == '\n' && + lastc == '.') { + /* + * Don't print a bare "." line since that will confuse + * ed(1). Print ".." instead and set the, global variable + * edoffset to an offset from which to restart. The + * caller must check the value of edoffset + */ + printf(".\n"); + edoffset = i - a + 1; + return (edoffset); + } + /* when side-by-side, do not print a newline */ + if (diff_format != D_SIDEBYSIDE || c != '\n') { + if (color && c == '\n') + printf("\033[m%c", c); + else + printf("%c", c); + col++; + } + } + + j++; + lastc = c; + } + } + if (color && diff_format == D_SIDEBYSIDE) + printf("\033[m"); + return (col); +} + +/* + * Hash function taken from Robert Sedgewick, Algorithms in C, 3d ed., p 578. + */ +static enum readhash +readhash(FILE *f, int flags, unsigned *hash) +{ + int i, t, space; + unsigned sum; + + sum = 1; + space = 0; + for (i = 0;;) { + switch (t = getc(f)) { + case '\0': + if ((flags & D_FORCEASCII) == 0) + return (RH_BINARY); + goto hashchar; + case '\r': + if (flags & D_STRIPCR) { + t = getc(f); + if (t == '\n') + break; + ungetc(t, f); + } + /* FALLTHROUGH */ + case '\t': + case '\v': + case '\f': + case ' ': + if ((flags & (D_FOLDBLANKS|D_IGNOREBLANKS)) != 0) { + space++; + continue; + } + /* FALLTHROUGH */ + default: + hashchar: + if (space && (flags & D_IGNOREBLANKS) == 0) { + i++; + space = 0; + } + sum = sum * 127 + chrtran(t); + i++; + continue; + case EOF: + if (i == 0) + return (RH_EOF); + /* FALLTHROUGH */ + case '\n': + break; + } + break; + } + *hash = sum; + return (RH_OK); +} + +static int +asciifile(FILE *f) +{ + unsigned char buf[BUFSIZ]; + size_t cnt; + + if (f == NULL) + return (1); + + rewind(f); + cnt = fread(buf, 1, sizeof(buf), f); + return (memchr(buf, '\0', cnt) == NULL); +} + +#define begins_with(s, pre) (strncmp(s, pre, sizeof(pre) - 1) == 0) + +static char * +match_function(const long *f, int pos, FILE *fp) +{ + unsigned char buf[FUNCTION_CONTEXT_SIZE]; + size_t nc; + int last = lastline; + const char *state = NULL; + + lastline = pos; + for (; pos > last; pos--) { + fseek(fp, f[pos - 1], SEEK_SET); + nc = f[pos] - f[pos - 1]; + if (nc >= sizeof(buf)) + nc = sizeof(buf) - 1; + nc = fread(buf, 1, nc, fp); + if (nc == 0) + continue; + buf[nc] = '\0'; + buf[strcspn(buf, "\n")] = '\0'; + if (most_recent_pat != NULL) { + int ret = regexec(&most_recent_re, buf, 0, NULL, 0); + + if (ret != 0) + continue; + strlcpy(lastbuf, buf, sizeof(lastbuf)); + lastmatchline = pos; + return (lastbuf); + } else if (isalpha(buf[0]) || buf[0] == '_' || buf[0] == '$' + || buf[0] == '-' || buf[0] == '+') { + if (begins_with(buf, "private:")) { + if (!state) + state = " (private)"; + } else if (begins_with(buf, "protected:")) { + if (!state) + state = " (protected)"; + } else if (begins_with(buf, "public:")) { + if (!state) + state = " (public)"; + } else { + strlcpy(lastbuf, buf, sizeof(lastbuf)); + if (state) + strlcat(lastbuf, state, sizeof(lastbuf)); + lastmatchline = pos; + return (lastbuf); + } + } + } + return (lastmatchline > 0 ? lastbuf : NULL); +} + +/* dump accumulated "context" diff changes */ +static void +dump_context_vec(FILE *f1, FILE *f2, int flags) +{ + struct context_vec *cvp = context_vec_start; + int lowa, upb, lowc, upd, do_output; + int a, b, c, d; + char ch, *f; + + if (context_vec_start > context_vec_ptr) + return; + + b = d = 0; /* gcc */ + if (ckd_sub(&lowa, cvp->a, diff_context) || lowa < 1) + lowa = 1; + if (ckd_add(&upb, context_vec_ptr->b, diff_context) || upb > (int)len[0]) + upb = (int)len[0]; + if (ckd_sub(&lowc, cvp->c, diff_context) || lowc < 1) + lowc = 1; + if (ckd_add(&upd, context_vec_ptr->d, diff_context) || upd > (int)len[1]) + upd = (int)len[1]; + + printf("***************"); + if (flags & (D_PROTOTYPE | D_MATCHLAST)) { + f = match_function(ixold, cvp->a - 1, f1); + if (f != NULL) + printf(" %s", f); + } + printf("\n*** "); + range(lowa, upb, ","); + printf(" ****\n"); + + /* + * Output changes to the "old" file. The first loop suppresses + * output if there were no changes to the "old" file (we'll see + * the "old" lines as context in the "new" list). + */ + do_output = 0; + for (; cvp <= context_vec_ptr; cvp++) + if (cvp->a <= cvp->b) { + cvp = context_vec_start; + do_output++; + break; + } + if (do_output) { + while (cvp <= context_vec_ptr) { + a = cvp->a; + b = cvp->b; + c = cvp->c; + d = cvp->d; + + if (a <= b && c <= d) + ch = 'c'; + else + ch = (a <= b) ? 'd' : 'a'; + + if (ch == 'a') + fetch(ixold, lowa, b, f1, ' ', 0, flags); + else { + fetch(ixold, lowa, a - 1, f1, ' ', 0, flags); + fetch(ixold, a, b, f1, + ch == 'c' ? '!' : '-', 0, flags); + } + lowa = b + 1; + cvp++; + } + fetch(ixold, b + 1, upb, f1, ' ', 0, flags); + } + /* output changes to the "new" file */ + printf("--- "); + range(lowc, upd, ","); + printf(" ----\n"); + + do_output = 0; + for (cvp = context_vec_start; cvp <= context_vec_ptr; cvp++) + if (cvp->c <= cvp->d) { + cvp = context_vec_start; + do_output++; + break; + } + if (do_output) { + while (cvp <= context_vec_ptr) { + a = cvp->a; + b = cvp->b; + c = cvp->c; + d = cvp->d; + + if (a <= b && c <= d) + ch = 'c'; + else + ch = (a <= b) ? 'd' : 'a'; + + if (ch == 'd') + fetch(ixnew, lowc, d, f2, ' ', 0, flags); + else { + fetch(ixnew, lowc, c - 1, f2, ' ', 0, flags); + fetch(ixnew, c, d, f2, + ch == 'c' ? '!' : '+', 0, flags); + } + lowc = d + 1; + cvp++; + } + fetch(ixnew, d + 1, upd, f2, ' ', 0, flags); + } + context_vec_ptr = context_vec_start - 1; +} + +/* dump accumulated "unified" diff changes */ +static void +dump_unified_vec(FILE *f1, FILE *f2, int flags) +{ + struct context_vec *cvp = context_vec_start; + int lowa, upb, lowc, upd; + int a, b, c, d; + char ch, *f; + + if (context_vec_start > context_vec_ptr) + return; + + b = d = 0; /* gcc */ + if (ckd_sub(&lowa, cvp->a, diff_context) || lowa < 1) + lowa = 1; + if (ckd_add(&upb, context_vec_ptr->b, diff_context) || upb > (int)len[0]) + upb = (int)len[0]; + if (ckd_sub(&lowc, cvp->c, diff_context) || lowc < 1) + lowc = 1; + if (ckd_add(&upd, context_vec_ptr->d, diff_context) || upd > (int)len[1]) + upd = (int)len[1]; + + printf("@@ -"); + uni_range(lowa, upb); + printf(" +"); + uni_range(lowc, upd); + printf(" @@"); + if (flags & (D_PROTOTYPE | D_MATCHLAST)) { + f = match_function(ixold, cvp->a - 1, f1); + if (f != NULL) + printf(" %s", f); + } + printf("\n"); + + /* + * Output changes in "unified" diff format--the old and new lines + * are printed together. + */ + for (; cvp <= context_vec_ptr; cvp++) { + a = cvp->a; + b = cvp->b; + c = cvp->c; + d = cvp->d; + + /* + * c: both new and old changes + * d: only changes in the old file + * a: only changes in the new file + */ + if (a <= b && c <= d) + ch = 'c'; + else + ch = (a <= b) ? 'd' : 'a'; + + switch (ch) { + case 'c': + fetch(ixold, lowa, a - 1, f1, ' ', 0, flags); + fetch(ixold, a, b, f1, '-', 0, flags); + fetch(ixnew, c, d, f2, '+', 0, flags); + break; + case 'd': + fetch(ixold, lowa, a - 1, f1, ' ', 0, flags); + fetch(ixold, a, b, f1, '-', 0, flags); + break; + case 'a': + fetch(ixnew, lowc, c - 1, f2, ' ', 0, flags); + fetch(ixnew, c, d, f2, '+', 0, flags); + break; + } + lowa = b + 1; + lowc = d + 1; + } + fetch(ixnew, d + 1, upd, f2, ' ', 0, flags); + + context_vec_ptr = context_vec_start - 1; +} + +static void +print_header(const char *file1, const char *file2) +{ + const char *time_format; + char buf[256]; + struct tm tm1, tm2, *tm_ptr1, *tm_ptr2; + int nsec1 = stb1.st_mtim.tv_nsec; + int nsec2 = stb2.st_mtim.tv_nsec; + + time_format = "%Y-%m-%d %H:%M:%S"; + + if (cflag) + time_format = "%c"; + tm_ptr1 = localtime_r(&stb1.st_mtime, &tm1); + tm_ptr2 = localtime_r(&stb2.st_mtime, &tm2); + if (label[0] != NULL) + printf("%s %s\n", diff_format == D_CONTEXT ? "***" : "---", + label[0]); + else { + strftime(buf, sizeof(buf), time_format, tm_ptr1); + printf("%s %s\t%s", diff_format == D_CONTEXT ? "***" : "---", + file1, buf); + if (!cflag) { + strftime(buf, sizeof(buf), "%z", tm_ptr1); + printf(".%.9d %s", nsec1, buf); + } + printf("\n"); + } + if (label[1] != NULL) + printf("%s %s\n", diff_format == D_CONTEXT ? "---" : "+++", + label[1]); + else { + strftime(buf, sizeof(buf), time_format, tm_ptr2); + printf("%s %s\t%s", diff_format == D_CONTEXT ? "---" : "+++", + file2, buf); + if (!cflag) { + strftime(buf, sizeof(buf), "%z", tm_ptr2); + printf(".%.9d %s", nsec2, buf); + } + printf("\n"); + } +} + +/* + * Prints n number of space characters either by using tab + * or single space characters. + * nc is the preceding number of characters + */ +static void +print_space(int nc, int n, int flags) +{ + int col, newcol, tabstop; + + col = nc; + newcol = nc + n; + /* first, use tabs if allowed */ + if ((flags & D_EXPANDTABS) == 0) { + while ((tabstop = roundup(col + 1, tabsize)) <= newcol) { + printf("\t"); + col = tabstop; + } + } + /* finish with spaces */ + printf("%*s", newcol - col, ""); +} diff --git a/src/diff/pr.c b/src/diff/pr.c new file mode 100644 index 0000000..ce1c511 --- /dev/null +++ b/src/diff/pr.c @@ -0,0 +1,110 @@ +/*- + * Copyright (c) 2017 Baptiste Daroussin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer + * in this position and unchanged. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``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 AUTHOR(S) 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 +#include + +#include +#include +#include +#include +#include +#include + +#include "pr.h" +#include "diff.h" +#include "xmalloc.h" + +#define _PATH_PR "/usr/bin/pr" + +struct pr * +start_pr(char *file1, char *file2) +{ + int pfd[2]; + pid_t pid; + char *header; + struct pr *pr; + + pr = xcalloc(1, sizeof(*pr)); + + xasprintf(&header, "%s %s %s", diffargs, file1, file2); + signal(SIGPIPE, SIG_IGN); + fflush(stdout); + if (pipe(pfd) == -1) + err(2, "pipe"); + switch ((pid = fork())) { + case -1: + err(2, "No more processes"); + case 0: + /* child */ + if (pfd[0] != STDIN_FILENO) { + dup2(pfd[0], STDIN_FILENO); + close(pfd[0]); + } + close(pfd[1]); + execl(_PATH_PR, _PATH_PR, "-h", header, (char *)0); + _exit(127); + default: + /* parent */ + if (pfd[1] == STDOUT_FILENO) { + pr->ostdout = STDOUT_FILENO; + } else { + if ((pr->ostdout = dup(STDOUT_FILENO)) < 0 || + dup2(pfd[1], STDOUT_FILENO) < 0) { + err(2, "stdout"); + } + close(pfd[1]); + } + close(pfd[0]); + free(header); + pr->pid = pid; + } + return (pr); +} + +/* close the pipe to pr and restore stdout */ +void +stop_pr(struct pr *pr) +{ + int wstatus; + + if (pr == NULL) + return; + + fflush(stdout); + if (pr->ostdout != STDOUT_FILENO) { + dup2(pr->ostdout, STDOUT_FILENO); + close(pr->ostdout); + } + if (waitpid(pr->pid, &wstatus, 0) == -1) + err(2, "waitpid"); + free(pr); + if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) != 0) + errx(2, "pr exited abnormally"); + else if (WIFSIGNALED(wstatus)) + errx(2, "pr killed by signal %d", + WTERMSIG(wstatus)); +} diff --git a/src/diff/pr.h b/src/diff/pr.h new file mode 100644 index 0000000..6d8f902 --- /dev/null +++ b/src/diff/pr.h @@ -0,0 +1,35 @@ +/*- + * Copyright (c) 2017 Baptiste Daroussin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer + * in this position and unchanged. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``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 AUTHOR(S) 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 + +struct pr { + int ostdout; + pid_t pid; +}; + +struct pr *start_pr(char *file1, char *file2); +void stop_pr(struct pr *); diff --git a/src/diff/tests/Bflag_C.out b/src/diff/tests/Bflag_C.out new file mode 100644 index 0000000..c7f130a --- /dev/null +++ b/src/diff/tests/Bflag_C.out @@ -0,0 +1,2 @@ +1a2 +> diff --git a/src/diff/tests/Bflag_D.out b/src/diff/tests/Bflag_D.out new file mode 100644 index 0000000..4c11703 --- /dev/null +++ b/src/diff/tests/Bflag_D.out @@ -0,0 +1,2 @@ +1a2 +> C diff --git a/src/diff/tests/Bflag_F.out b/src/diff/tests/Bflag_F.out new file mode 100644 index 0000000..9dc5e56 --- /dev/null +++ b/src/diff/tests/Bflag_F.out @@ -0,0 +1,4 @@ +7c8 +< G +--- +> X diff --git a/src/diff/tests/Makefile.depend b/src/diff/tests/Makefile.depend new file mode 100644 index 0000000..11aba52 --- /dev/null +++ b/src/diff/tests/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/src/diff/tests/diff_test.sh b/src/diff/tests/diff_test.sh new file mode 100755 index 0000000..fb6080e --- /dev/null +++ b/src/diff/tests/diff_test.sh @@ -0,0 +1,478 @@ + +atf_test_case simple +atf_test_case unified +atf_test_case header +atf_test_case header_ns +atf_test_case ifdef +atf_test_case group_format +atf_test_case side_by_side +atf_test_case side_by_side_tabbed +atf_test_case brief_format +atf_test_case b230049 +atf_test_case stripcr_o +atf_test_case b252515 +atf_test_case b278988 +atf_test_case Bflag +atf_test_case Nflag +atf_test_case tabsize +atf_test_case conflicting_format +atf_test_case label +atf_test_case report_identical +atf_test_case non_regular_file +atf_test_case binary +atf_test_case functionname +atf_test_case noderef +atf_test_case ignorecase +atf_test_case dirloop +atf_test_case bigc +atf_test_case bigu +atf_test_case prleak +atf_test_case same + +simple_body() +{ + atf_check -o file:$(atf_get_srcdir)/simple.out -s eq:1 \ + diff "$(atf_get_srcdir)/input1.in" "$(atf_get_srcdir)/input2.in" + + atf_check -o file:$(atf_get_srcdir)/simple_e.out -s eq:1 \ + diff -e "$(atf_get_srcdir)/input1.in" "$(atf_get_srcdir)/input2.in" + + atf_check -o file:$(atf_get_srcdir)/simple_u.out -s eq:1 \ + diff -u -L input1 -L input2 "$(atf_get_srcdir)/input1.in" "$(atf_get_srcdir)/input2.in" + + atf_check -o file:$(atf_get_srcdir)/simple_n.out -s eq:1 \ + diff -n "$(atf_get_srcdir)/input1.in" "$(atf_get_srcdir)/input2.in" + + atf_check -o inline:"Files $(atf_get_srcdir)/input1.in and $(atf_get_srcdir)/input2.in differ\n" -s eq:1 \ + diff -q "$(atf_get_srcdir)/input1.in" "$(atf_get_srcdir)/input2.in" + + atf_check \ + diff -q "$(atf_get_srcdir)/input1.in" "$(atf_get_srcdir)/input1.in" + + atf_check -o file:$(atf_get_srcdir)/simple_i.out -s eq:1 \ + diff -i "$(atf_get_srcdir)/input_c1.in" "$(atf_get_srcdir)/input_c2.in" + + atf_check -o file:$(atf_get_srcdir)/simple_w.out -s eq:1 \ + diff -w "$(atf_get_srcdir)/input_c1.in" "$(atf_get_srcdir)/input_c2.in" + + atf_check -o file:$(atf_get_srcdir)/simple_b.out -s eq:1 \ + diff -b "$(atf_get_srcdir)/input_c1.in" "$(atf_get_srcdir)/input_c2.in" + + atf_check -o file:$(atf_get_srcdir)/simple_p.out -s eq:1 \ + diff --label input_c1.in --label input_c2.in -p "$(atf_get_srcdir)/input_c1.in" "$(atf_get_srcdir)/input_c2.in" +} + +unified_body() +{ + atf_check -o file:$(atf_get_srcdir)/unified_p.out -s eq:1 \ + diff -up -L input_c1.in -L input_c2.in "$(atf_get_srcdir)/input_c1.in" "$(atf_get_srcdir)/input_c2.in" + atf_check -o file:$(atf_get_srcdir)/unified_9999.out -s eq:1 \ + diff -u9999 -L input_c1.in -L input_c2.in "$(atf_get_srcdir)/input_c1.in" "$(atf_get_srcdir)/input_c2.in" +} + +b230049_body() +{ + printf 'a\nb\r\nc\n' > b230049_a.in + printf 'a\r\nb\r\nc\r\n' > b230049_b.in + atf_check -o empty -s eq:0 \ + diff -up --strip-trailing-cr -L b230049_a.in -L b230049_b.in \ + b230049_a.in b230049_b.in +} + +stripcr_o_body() +{ + printf 'a\nX\nc\n' > stripcr_o_X.in + printf 'a\r\nY\r\nc\r\n' > stripcr_o_Y.in + atf_check -o "file:$(atf_get_srcdir)/strip_o.out" -s eq:1 \ + diff -L1 -L2 -u --strip-trailing-cr stripcr_o_X.in stripcr_o_Y.in +} + +b252515_body() +{ + printf 'a b\n' > b252515_a.in + printf 'a b\n' > b252515_b.in + atf_check -o empty -s eq:0 \ + diff -qw b252515_a.in b252515_b.in +} + +b278988_body() +{ + printf 'a\nb\nn' > b278988.a.in + printf 'a\n\nb\nn' > b278988.b.in + atf_check -o empty -s eq:0 \ + diff -Bw b278988.a.in b278988.b.in +} + +header_body() +{ + export TZ=UTC + : > empty + echo hello > hello + touch -d 2015-04-03T01:02:03 empty + touch -d 2016-12-22T11:22:33 hello + atf_check -o "file:$(atf_get_srcdir)/header.out" -s eq:1 \ + diff -u empty hello +} + +header_ns_body() +{ + export TZ=UTC + : > empty + echo hello > hello + touch -d 2015-04-03T01:02:03.123456789 empty + touch -d 2016-12-22T11:22:33.987654321 hello + atf_check -o "file:$(atf_get_srcdir)/header_ns.out" -s eq:1 \ + diff -u empty hello +} + +ifdef_body() +{ + atf_check -o file:$(atf_get_srcdir)/ifdef.out -s eq:1 \ + diff -D PLOP "$(atf_get_srcdir)/input_c1.in" \ + "$(atf_get_srcdir)/input_c2.in" +} + +group_format_body() +{ + atf_check -o file:$(atf_get_srcdir)/group-format.out -s eq:1 \ + diff --changed-group-format='<<<<<<< (local) +%<======= +%>>>>>>>> (stock) +' "$(atf_get_srcdir)/input_c1.in" "$(atf_get_srcdir)/input_c2.in" +} + +side_by_side_body() +{ + atf_check -o save:A printf "A\nB\nC\n" + atf_check -o save:B printf "D\nB\nE\n" + + exp_output=$(printf "A[[:space:]]+|[[:space:]]+D\nB[[:space:]]+B\nC[[:space:]]+|[[:space:]]+E") + exp_output_suppressed=$(printf "A[[:space:]]+|[[:space:]]+D\nC[[:space:]]+|[[:space:]]+E") + + atf_check -o match:"$exp_output" -s exit:1 \ + diff --side-by-side A B + atf_check -o match:"$exp_output" -s exit:1 \ + diff -y A B + atf_check -o match:"$exp_output_suppressed" -s exit:1 \ + diff -y --suppress-common-lines A B + atf_check -o match:"$exp_output_suppressed" -s exit:1 \ + diff -W 65 -y --suppress-common-lines A B +} + +side_by_side_tabbed_body() +{ + file_a=$(atf_get_srcdir)/side_by_side_tabbed_a.in + file_b=$(atf_get_srcdir)/side_by_side_tabbed_b.in + + atf_check -o save:diffout -s not-exit:0 \ + diff -y ${file_a} ${file_b} + atf_check -o save:diffout_expanded -s not-exit:0 \ + diff -yt ${file_a} ${file_b} + + atf_check -o not-empty grep -Ee 'file A.+file B' diffout + atf_check -o not-empty grep -Ee 'file A.+file B' diffout_expanded + + atf_check -o not-empty grep -Ee 'tabs.+tabs' diffout + atf_check -o not-empty grep -Ee 'tabs.+tabs' diffout_expanded +} + +brief_format_body() +{ + atf_check mkdir A B + + atf_check -x "echo 1 > A/test-file" + atf_check -x "echo 2 > B/test-file" + + atf_check cp -Rf A C + atf_check cp -Rf A D + + atf_check -x "echo 3 > D/another-test-file" + + atf_check \ + -s exit:1 \ + -o inline:"Files A/test-file and B/test-file differ\n" \ + diff -rq A B + + atf_check diff -rq A C + + atf_check \ + -s exit:1 \ + -o inline:"Only in D: another-test-file\n" \ + diff -rq A D + + atf_check \ + -s exit:1 \ + -o inline:"Files A/another-test-file and D/another-test-file differ\n" \ + diff -Nrq A D +} + +Bflag_body() +{ + atf_check -x 'printf "A\nB\n" > A' + atf_check -x 'printf "A\n\nB\n" > B' + atf_check -x 'printf "A\n \nB\n" > C' + atf_check -x 'printf "A\nC\nB\n" > D' + atf_check -x 'printf "A\nB\nC\nD\nE\nF\nG\nH" > E' + atf_check -x 'printf "A\n\nB\nC\nD\nE\nF\nX\nH" > F' + + atf_check -s exit:0 -o inline:"" diff -B A B + atf_check -s exit:1 -o file:"$(atf_get_srcdir)/Bflag_C.out" diff -B A C + atf_check -s exit:1 -o file:"$(atf_get_srcdir)/Bflag_D.out" diff -B A D + atf_check -s exit:1 -o file:"$(atf_get_srcdir)/Bflag_F.out" diff -B E F +} + +Nflag_body() +{ + atf_check -x 'printf "foo" > A' + + atf_check -s exit:1 -o ignore -e ignore diff -N A NOFILE + atf_check -s exit:1 -o ignore -e ignore diff -N NOFILE A + atf_check -s exit:2 -o ignore -e ignore diff -N NOFILE1 NOFILE2 +} + +tabsize_body() +{ + printf "\tA\n" > A + printf "\tB\n" > B + + atf_check -s exit:1 \ + -o inline:"1c1\n< A\n---\n> B\n" \ + diff -t --tabsize 1 A B +} + +conflicting_format_body() +{ + printf "\tA\n" > A + printf "\tB\n" > B + + atf_check -s exit:2 -e ignore diff -c -u A B + atf_check -s exit:2 -e ignore diff -e -f A B + atf_check -s exit:2 -e ignore diff -y -q A B + atf_check -s exit:2 -e ignore diff -q -u A B + atf_check -s exit:2 -e ignore diff -q -c A B + atf_check -s exit:2 -e ignore diff --normal -c A B + atf_check -s exit:2 -e ignore diff -c --normal A B + + atf_check -s exit:1 -o ignore -e ignore diff -u -u A B + atf_check -s exit:1 -o ignore -e ignore diff -e -e A B + atf_check -s exit:1 -o ignore -e ignore diff -y -y A B + atf_check -s exit:1 -o ignore -e ignore diff -q -q A B + atf_check -s exit:1 -o ignore -e ignore diff -c -c A B + atf_check -s exit:1 -o ignore -e ignore diff --normal --normal A B +} + +label_body() +{ + printf "\tA\n" > A + + atf_check -o inline:"Files hello and world are identical\n" \ + -s exit:0 diff --label hello --label world -s A A + + atf_check -o inline:"Binary files hello and world differ\n" \ + -s exit:1 diff --label hello --label world `which diff` `which ls` +} + +report_identical_head() +{ + atf_set "require.user" unprivileged +} +report_identical_body() +{ + printf "\tA\n" > A + printf "\tB\n" > B + chmod -r B + atf_check -s exit:2 -e inline:"diff: B: Permission denied\n" \ + -o empty diff -s A B +} + +non_regular_file_body() +{ + printf "\tA\n" > A + mkfifo B + printf "\tA\n" > B & + + atf_check diff A B + printf "\tB\n" > B & + atf_check -s exit:1 \ + -o inline:"--- A\n+++ B\n@@ -1 +1 @@\n-\tA\n+\tB\n" \ + diff --label A --label B -u A B +} + +binary_body() +{ + # the NUL byte has to be after at least BUFSIZ bytes to trick asciifile() + yes 012345678901234567890123456789012345678901234567890 | head -n 174 > A + cp A B + printf '\n\0\n' >> A + printf '\nx\n' >> B + + atf_check -o inline:"Binary files A and B differ\n" -s exit:1 diff A B + atf_check -o inline:"176c\nx\n.\n" -s exit:1 diff -ae A B +} + +functionname_body() +{ + atf_check -o file:$(atf_get_srcdir)/functionname_c.out -s exit:1 \ + diff -u -p -L functionname.in -L functionname_c.in \ + "$(atf_get_srcdir)/functionname.in" "$(atf_get_srcdir)/functionname_c.in" + + atf_check -o file:$(atf_get_srcdir)/functionname_objcm.out -s exit:1 \ + diff -u -p -L functionname.in -L functionname_objcm.in \ + "$(atf_get_srcdir)/functionname.in" "$(atf_get_srcdir)/functionname_objcm.in" + + atf_check -o file:$(atf_get_srcdir)/functionname_objcclassm.out -s exit:1 \ + diff -u -p -L functionname.in -L functionname_objcclassm.in \ + "$(atf_get_srcdir)/functionname.in" "$(atf_get_srcdir)/functionname_objcclassm.in" +} + +noderef_body() +{ + atf_check mkdir A B + + atf_check -x "echo 1 > A/test-file" + atf_check -x "echo 1 > test-file" + atf_check -x "echo 1 > test-file2" + + atf_check ln -s $(pwd)/test-file B/test-file + + atf_check -o empty -s exit:0 diff -r A B + atf_check -o inline:"File A/test-file is a file while file B/test-file is a symbolic link\n" \ + -s exit:1 diff -r --no-dereference A B + + # both test files are now the same symbolic link + atf_check rm A/test-file + + atf_check ln -s $(pwd)/test-file A/test-file + atf_check -o empty -s exit:0 diff -r A B + atf_check -o empty -s exit:0 diff -r --no-dereference A B + + # make test files different symbolic links, but same contents + atf_check unlink A/test-file + atf_check ln -s $(pwd)/test-file2 A/test-file + + atf_check -o empty -s exit:0 diff -r A B + atf_check -o inline:"Symbolic links A/test-file and B/test-file differ\n" -s exit:1 diff -r --no-dereference A B +} + +ignorecase_body() +{ + atf_check mkdir A + atf_check mkdir B + + atf_check -x "echo hello > A/foo" + atf_check -x "echo hello > B/FOO" + + atf_check -o empty -s exit:0 diff -u -r --ignore-file-name-case A B +} + +dirloop_head() +{ + atf_set "timeout" "10" +} +dirloop_body() +{ + atf_check mkdir -p a/foo/bar + atf_check ln -s .. a/foo/bar/up + atf_check cp -a a b + atf_check \ + -e match:"a/foo/bar/up: Directory loop detected" \ + -e match:"b/foo/bar/up: Directory loop detected" \ + diff -r a b +} + +bigc_head() +{ + atf_set "descr" "Context diff with very large context" +} +bigc_body() +{ + echo $'x\na\ny' >a + echo $'x\nb\ny' >b + atf_check -s exit:2 -e ignore diff -C$(((1<<31)-1)) a b + atf_check -s exit:1 -o match:'--- 1,3 ---' \ + diff -C$(((1<<31)-2)) a b +} + +bigu_head() +{ + atf_set "descr" "Unified diff with very large context" +} +bigu_body() +{ + echo $'x\na\ny' >a + echo $'x\nb\ny' >b + atf_check -s exit:2 -e ignore diff -U$(((1<<31)-1)) a b + atf_check -s exit:1 -o match:'^@@ -1,3 \+1,3 @@$' \ + diff -U$(((1<<31)-2)) a b +} + +prleak_head() +{ + atf_set "descr" "Verify that pagination does not leak resources" +} +prleak_body() +{ + local n=32 + mkdir a b + for hi in $(jot -w%02x $n 0) ; do + mkdir a/$hi b/$hi + for lo in $(jot -w%02x $n 0) ; do + echo "$hi$lo" >a/$hi/$lo + echo "$hi$lo" >b/$hi/$lo + done + done + ulimit -n 1000 + ulimit -u 1000 + atf_check diff -rul a b +} + +same_head() +{ + atf_set "descr" "Don't diff a file or directory with itself" +} +same_body() +{ + local n=256 + mkdir a + for hi in $(jot -w%02x $n 0) ; do + mkdir a/$hi + for lo in $(jot -w%02x $n 0) ; do + echo "$hi$lo" >a/$hi/$lo + done + done + ln -s a b + atf_check timeout 1s diff -rqs a b +} + +atf_init_test_cases() +{ + atf_add_test_case simple + atf_add_test_case unified + atf_add_test_case header + atf_add_test_case header_ns + atf_add_test_case ifdef + atf_add_test_case group_format + atf_add_test_case side_by_side + atf_add_test_case side_by_side_tabbed + atf_add_test_case brief_format + atf_add_test_case b230049 + atf_add_test_case stripcr_o + atf_add_test_case b252515 + atf_add_test_case b278988 + atf_add_test_case Bflag + atf_add_test_case Nflag + atf_add_test_case tabsize + atf_add_test_case conflicting_format + atf_add_test_case label + atf_add_test_case report_identical + atf_add_test_case non_regular_file + atf_add_test_case binary + atf_add_test_case functionname + atf_add_test_case noderef + atf_add_test_case ignorecase + atf_add_test_case dirloop + atf_add_test_case bigc + atf_add_test_case bigu + atf_add_test_case prleak + atf_add_test_case same +} diff --git a/src/diff/tests/functionname.in b/src/diff/tests/functionname.in new file mode 100644 index 0000000..7b4c50c --- /dev/null +++ b/src/diff/tests/functionname.in @@ -0,0 +1,29 @@ +static void +doSomethingThenPrintHello(int test) +{ + test = test << 4; + if (test % 8 == 6) { + return; + } + + print("goodbye\n"); +} + + +- (long) readOffset:(FILE*)file +{ + if( version >= 11){ + long offset; + fread(&offset, sizeof(long), 1, file); + return offset; + } else { + int offset; + fread(&offset, sizeof(int), 1, file); + return offset; + } +} + ++ (BOOL) isEdible:(NSString *)mushroom +{ + return TRUE; +} diff --git a/src/diff/tests/functionname_c.in b/src/diff/tests/functionname_c.in new file mode 100644 index 0000000..84f6846 --- /dev/null +++ b/src/diff/tests/functionname_c.in @@ -0,0 +1,29 @@ +static void +doSomethingThenPrintHello(int test) +{ + test = test << 4; + if (test % 8 == 6) { + return; + } + + print("hello\n"); +} + + +- (long) readOffset:(FILE*)file +{ + if( version >= 11){ + long offset; + fread(&offset, sizeof(long), 1, file); + return offset; + } else { + int offset; + fread(&offset, sizeof(int), 1, file); + return offset; + } +} + ++ (BOOL) isEdible:(NSString *)mushroom +{ + return TRUE; +} diff --git a/src/diff/tests/functionname_c.out b/src/diff/tests/functionname_c.out new file mode 100644 index 0000000..b17ce05 --- /dev/null +++ b/src/diff/tests/functionname_c.out @@ -0,0 +1,11 @@ +--- functionname.in ++++ functionname_c.in +@@ -6,7 +6,7 @@ doSomethingThenPrintHello(int test) + return; + } + +- print("goodbye\n"); ++ print("hello\n"); + } + + diff --git a/src/diff/tests/functionname_objcclassm.in b/src/diff/tests/functionname_objcclassm.in new file mode 100644 index 0000000..37a9a76 --- /dev/null +++ b/src/diff/tests/functionname_objcclassm.in @@ -0,0 +1,31 @@ +static void +doSomethingThenPrintHello(int test) +{ + test = test << 4; + if (test % 8 == 6) { + return; + } + + print("goodbye\n"); +} + + +- (long) readOffset:(FILE*)file +{ + if( version >= 11){ + long offset; + fread(&offset, sizeof(long), 1, file); + return offset; + } else { + int offset; + fread(&offset, sizeof(int), 1, file); + return offset; + } +} + ++ (BOOL) isEdible:(NSString *)mushroom +{ + /* With a solid guide book (such as Phillips 2006) assume we can't eat + * the fungus */ + return FALSE; +} diff --git a/src/diff/tests/functionname_objcclassm.out b/src/diff/tests/functionname_objcclassm.out new file mode 100644 index 0000000..b68b732 --- /dev/null +++ b/src/diff/tests/functionname_objcclassm.out @@ -0,0 +1,11 @@ +--- functionname.in ++++ functionname_objcclassm.in +@@ -25,5 +25,7 @@ + (BOOL) isEdible:(NSString *)mushroom + + + (BOOL) isEdible:(NSString *)mushroom + { +- return TRUE; ++ /* With a solid guide book (such as Phillips 2006) assume we can't eat ++ * the fungus */ ++ return FALSE; + } diff --git a/src/diff/tests/functionname_objcm.in b/src/diff/tests/functionname_objcm.in new file mode 100644 index 0000000..06c3e9b --- /dev/null +++ b/src/diff/tests/functionname_objcm.in @@ -0,0 +1,29 @@ +static void +doSomethingThenPrintHello(int test) +{ + test = test << 4; + if (test % 8 == 6) { + return; + } + + print("goodbye\n"); +} + + +- (long) readOffset:(FILE*)file +{ + if( version >= 11){ + long offset; + fread(&offset, sizeof(long), 1, file); + return offset; + } else { + int offset; + fread(&offset-1, sizeof(int), 1, file); + return offset; + } +} + ++ (BOOL) isEdible:(NSString *)mushroom +{ + return TRUE; +} diff --git a/src/diff/tests/functionname_objcm.out b/src/diff/tests/functionname_objcm.out new file mode 100644 index 0000000..cb29b08 --- /dev/null +++ b/src/diff/tests/functionname_objcm.out @@ -0,0 +1,11 @@ +--- functionname.in ++++ functionname_objcm.in +@@ -18,7 +18,7 @@ - (long) readOffset:(FILE*)file + return offset; + } else { + int offset; +- fread(&offset, sizeof(int), 1, file); ++ fread(&offset-1, sizeof(int), 1, file); + return offset; + } + } diff --git a/src/diff/tests/group-format.out b/src/diff/tests/group-format.out new file mode 100644 index 0000000..4e1bf85 --- /dev/null +++ b/src/diff/tests/group-format.out @@ -0,0 +1,27 @@ +/* + * A comment + * +<<<<<<< (local) + * And another bla +======= + * And another bla +>>>>>>> (stock) + * +<<<<<<< (local) + * And yet another +======= + * and yet another +>>>>>>> (stock) + */ + +int +main(void) +{ +<<<<<<< (local) +======= + +>>>>>>> (stock) + printf("something"); + + return (0); +} diff --git a/src/diff/tests/header.out b/src/diff/tests/header.out new file mode 100644 index 0000000..2e1665a --- /dev/null +++ b/src/diff/tests/header.out @@ -0,0 +1,4 @@ +--- empty 2015-04-03 01:02:03.000000000 +0000 ++++ hello 2016-12-22 11:22:33.000000000 +0000 +@@ -0,0 +1 @@ ++hello diff --git a/src/diff/tests/header_ns.out b/src/diff/tests/header_ns.out new file mode 100644 index 0000000..b1316df --- /dev/null +++ b/src/diff/tests/header_ns.out @@ -0,0 +1,4 @@ +--- empty 2015-04-03 01:02:03.123456789 +0000 ++++ hello 2016-12-22 11:22:33.987654321 +0000 +@@ -0,0 +1 @@ ++hello diff --git a/src/diff/tests/ifdef.out b/src/diff/tests/ifdef.out new file mode 100644 index 0000000..cc72cac --- /dev/null +++ b/src/diff/tests/ifdef.out @@ -0,0 +1,26 @@ +/* + * A comment + * +#ifndef PLOP + * And another bla +#else /* PLOP */ + * And another bla +#endif /* PLOP */ + * +#ifndef PLOP + * And yet another +#else /* PLOP */ + * and yet another +#endif /* PLOP */ + */ + +int +main(void) +{ +#ifdef PLOP + +#endif /* PLOP */ + printf("something"); + + return (0); +} diff --git a/src/diff/tests/input1.in b/src/diff/tests/input1.in new file mode 100644 index 0000000..3892e84 --- /dev/null +++ b/src/diff/tests/input1.in @@ -0,0 +1,2 @@ +Simple input file designed +to be able to test diff diff --git a/src/diff/tests/input2.in b/src/diff/tests/input2.in new file mode 100644 index 0000000..c38b487 --- /dev/null +++ b/src/diff/tests/input2.in @@ -0,0 +1,3 @@ +Simple input file designed +and written +to be able to test diff utility diff --git a/src/diff/tests/input_c1.in b/src/diff/tests/input_c1.in new file mode 100644 index 0000000..d39dfbd --- /dev/null +++ b/src/diff/tests/input_c1.in @@ -0,0 +1,15 @@ +/* + * A comment + * + * And another bla + * + * And yet another + */ + +int +main(void) +{ + printf("something"); + + return (0); +} diff --git a/src/diff/tests/input_c2.in b/src/diff/tests/input_c2.in new file mode 100644 index 0000000..933ec67 --- /dev/null +++ b/src/diff/tests/input_c2.in @@ -0,0 +1,16 @@ +/* + * A comment + * + * And another bla + * + * and yet another + */ + +int +main(void) +{ + + printf("something"); + + return (0); +} diff --git a/src/diff/tests/side_by_side_tabbed_a.in b/src/diff/tests/side_by_side_tabbed_a.in new file mode 100644 index 0000000..3198d29 --- /dev/null +++ b/src/diff/tests/side_by_side_tabbed_a.in @@ -0,0 +1,4 @@ +This + is my test file A + it has tabs +Thanks diff --git a/src/diff/tests/side_by_side_tabbed_b.in b/src/diff/tests/side_by_side_tabbed_b.in new file mode 100644 index 0000000..6698182 --- /dev/null +++ b/src/diff/tests/side_by_side_tabbed_b.in @@ -0,0 +1,4 @@ +This + is my test file B + it has tabs +Thanks diff --git a/src/diff/tests/simple.out b/src/diff/tests/simple.out new file mode 100644 index 0000000..fcbcaa0 --- /dev/null +++ b/src/diff/tests/simple.out @@ -0,0 +1,5 @@ +2c2,3 +< to be able to test diff +--- +> and written +> to be able to test diff utility diff --git a/src/diff/tests/simple_b.out b/src/diff/tests/simple_b.out new file mode 100644 index 0000000..704be9d --- /dev/null +++ b/src/diff/tests/simple_b.out @@ -0,0 +1,6 @@ +6c6 +< * And yet another +--- +> * and yet another +11a12 +> diff --git a/src/diff/tests/simple_e.out b/src/diff/tests/simple_e.out new file mode 100644 index 0000000..0c7e2b5 --- /dev/null +++ b/src/diff/tests/simple_e.out @@ -0,0 +1,4 @@ +2c +and written +to be able to test diff utility +. diff --git a/src/diff/tests/simple_i.out b/src/diff/tests/simple_i.out new file mode 100644 index 0000000..9edc1f9 --- /dev/null +++ b/src/diff/tests/simple_i.out @@ -0,0 +1,6 @@ +4c4 +< * And another bla +--- +> * And another bla +11a12 +> diff --git a/src/diff/tests/simple_n.out b/src/diff/tests/simple_n.out new file mode 100644 index 0000000..33ca709 --- /dev/null +++ b/src/diff/tests/simple_n.out @@ -0,0 +1,4 @@ +d2 1 +a2 2 +and written +to be able to test diff utility diff --git a/src/diff/tests/simple_p.out b/src/diff/tests/simple_p.out new file mode 100644 index 0000000..f5aebb0 --- /dev/null +++ b/src/diff/tests/simple_p.out @@ -0,0 +1,34 @@ +*** input_c1.in +--- input_c2.in +*************** +*** 1,14 **** + /* + * A comment + * +! * And another bla + * +! * And yet another + */ + + int + main(void) + { + printf("something"); + + return (0); +--- 1,15 ---- + /* + * A comment + * +! * And another bla + * +! * and yet another + */ + + int + main(void) + { ++ + printf("something"); + + return (0); diff --git a/src/diff/tests/simple_u.out b/src/diff/tests/simple_u.out new file mode 100644 index 0000000..f341987 --- /dev/null +++ b/src/diff/tests/simple_u.out @@ -0,0 +1,7 @@ +--- input1 ++++ input2 +@@ -1,2 +1,3 @@ + Simple input file designed +-to be able to test diff ++and written ++to be able to test diff utility diff --git a/src/diff/tests/simple_w.out b/src/diff/tests/simple_w.out new file mode 100644 index 0000000..704be9d --- /dev/null +++ b/src/diff/tests/simple_w.out @@ -0,0 +1,6 @@ +6c6 +< * And yet another +--- +> * and yet another +11a12 +> diff --git a/src/diff/tests/strip_o.out b/src/diff/tests/strip_o.out new file mode 100644 index 0000000..47bbfbd --- /dev/null +++ b/src/diff/tests/strip_o.out @@ -0,0 +1,7 @@ +--- 1 ++++ 2 +@@ -1,3 +1,3 @@ + a +-X ++Y + c diff --git a/src/diff/tests/unified_9999.out b/src/diff/tests/unified_9999.out new file mode 100644 index 0000000..0f9303f --- /dev/null +++ b/src/diff/tests/unified_9999.out @@ -0,0 +1,21 @@ +--- input_c1.in ++++ input_c2.in +@@ -1,15 +1,16 @@ + /* + * A comment + * +- * And another bla ++ * And another bla + * +- * And yet another ++ * and yet another + */ + + int + main(void) + { ++ + printf("something"); + + return (0); + } diff --git a/src/diff/tests/unified_c9999.out b/src/diff/tests/unified_c9999.out new file mode 100644 index 0000000..87f4394 --- /dev/null +++ b/src/diff/tests/unified_c9999.out @@ -0,0 +1,36 @@ +*** input_c1.in +--- input_c2.in +*************** +*** 1,15 **** + /* + * A comment + * +! * And another bla + * +! * And yet another + */ + + int + main(void) + { + printf("something"); + + return (0); + } +--- 1,16 ---- + /* + * A comment + * +! * And another bla + * +! * and yet another + */ + + int + main(void) + { ++ + printf("something"); + + return (0); + } diff --git a/src/diff/tests/unified_p.out b/src/diff/tests/unified_p.out new file mode 100644 index 0000000..938b078 --- /dev/null +++ b/src/diff/tests/unified_p.out @@ -0,0 +1,20 @@ +--- input_c1.in ++++ input_c2.in +@@ -1,14 +1,15 @@ + /* + * A comment + * +- * And another bla ++ * And another bla + * +- * And yet another ++ * and yet another + */ + + int + main(void) + { ++ + printf("something"); + + return (0); diff --git a/src/diff/xmalloc.c b/src/diff/xmalloc.c new file mode 100644 index 0000000..ce0f454 --- /dev/null +++ b/src/diff/xmalloc.c @@ -0,0 +1,85 @@ +/* $OpenBSD: xmalloc.c,v 1.10 2019/06/28 05:44:09 deraadt Exp $ */ +/* + * Author: Tatu Ylonen + * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland + * All rights reserved + * Versions of malloc and friends that check their results, and never return + * failure (they call fatal if they encounter an error). + * + * As far as I am concerned, the code I have written for this software + * can be used freely for any purpose. Any derived versions of this + * software must be clearly marked as such, and if the derived work is + * incompatible with the protocol description in the RFC file, it must be + * called by a name other than "ssh" or "Secure Shell". + */ + +#include +#include +#include +#include +#include +#include + +#include "xmalloc.h" + +void * +xmalloc(size_t size) +{ + void *ptr; + + if (size == 0) + errx(2, "xmalloc: zero size"); + ptr = malloc(size); + if (ptr == NULL) + err(2, "xmalloc: allocating %zu bytes", size); + return ptr; +} + +void * +xcalloc(size_t nmemb, size_t size) +{ + void *ptr; + + ptr = calloc(nmemb, size); + if (ptr == NULL) + err(2, "xcalloc: allocating %zu * %zu bytes", nmemb, size); + return ptr; +} + +void * +xreallocarray(void *ptr, size_t nmemb, size_t size) +{ + void *new_ptr; + + new_ptr = reallocarray(ptr, nmemb, size); + if (new_ptr == NULL) + err(2, "xreallocarray: allocating %zu * %zu bytes", + nmemb, size); + return new_ptr; +} + +char * +xstrdup(const char *str) +{ + char *cp; + + if ((cp = strdup(str)) == NULL) + err(2, "xstrdup"); + return cp; +} + +int +xasprintf(char **ret, const char *fmt, ...) +{ + va_list ap; + int i; + + va_start(ap, fmt); + i = vasprintf(ret, fmt, ap); + va_end(ap); + + if (i == -1) + err(2, "xasprintf"); + + return i; +} diff --git a/src/diff/xmalloc.h b/src/diff/xmalloc.h new file mode 100644 index 0000000..2139671 --- /dev/null +++ b/src/diff/xmalloc.h @@ -0,0 +1,30 @@ +/* $OpenBSD: xmalloc.h,v 1.4 2015/11/12 16:30:30 mmcc Exp $ */ + +/* + * Author: Tatu Ylonen + * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland + * All rights reserved + * Created: Mon Mar 20 22:09:17 1995 ylo + * + * Versions of malloc and friends that check their results, and never return + * failure (they call fatal if they encounter an error). + * + * As far as I am concerned, the code I have written for this software + * can be used freely for any purpose. Any derived versions of this + * software must be clearly marked as such, and if the derived work is + * incompatible with the protocol description in the RFC file, it must be + * called by a name other than "ssh" or "Secure Shell". + */ + +#ifndef XMALLOC_H +#define XMALLOC_H + +void *xmalloc(size_t); +void *xcalloc(size_t, size_t); +void *xreallocarray(void *, size_t, size_t); +char *xstrdup(const char *); +int xasprintf(char **, const char *, ...) + __attribute__((__format__ (printf, 2, 3))) + __attribute__((__nonnull__ (2))); + +#endif /* XMALLOC_H */ diff --git a/src/diff3/diff3.1 b/src/diff3/diff3.1 new file mode 100644 index 0000000..9f66e05 --- /dev/null +++ b/src/diff3/diff3.1 @@ -0,0 +1,223 @@ +.\" $OpenBSD: diff3.1,v 1.7 2007/05/31 19:20:09 jmc Exp $ +.\" +.\" Copyright (c) 1990, 1993, 1994 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. +.\" +.\" @(#)diff3.1 8.2 (Berkeley) 4/18/94 +.\" +.Dd June 23, 2022 +.Dt DIFF3 1 +.Os +.Sh NAME +.Nm diff3 +.Nd 3-way differential file comparison +.Sh SYNOPSIS +.Nm +.Op Fl 3AaEeimTXx +.Op Fl Fl diff-program Ar program +.Op Fl Fl strip-trailing-cr +.Op Fl L | Fl Fl label Ar label1 +.Op Fl L | Fl Fl label Ar label2 +.Op Fl L | Fl Fl label Ar label3 +.Ar file1 file2 file3 +.Nm +.Op Fl Fl help +.Op Fl Fl version +.Sh DESCRIPTION +The +.Nm +utility compares the contents of three different versions of a file, +.Ar file1 , +.Ar file2 +and +.Ar file3 , +writing the result to the standard output. +The options describe different methods of merging and +purging +the separate versions into a new file. +.Nm +is used by +.Xr rcs 1 +to merge specific versions or create +new versions. +.Pp +The options are as follows: +.Bl -tag -width "-E, -X" +.It Fl 3 , Fl Fl easy-only +Produces an output script suitable for +.Xr ed 1 +with changes +specific only to +.Ar file3 . +.It Fl A Fl Fl show-all +Output all changes, bracketing conflicts. +.It Fl a , Fl Fl text +Treat all files as ASCII. +.It Fl E , Fl Fl show-overlap +.It Fl X +Similar to +.Fl e +and +.Fl x , +respectively, but treat overlapping changes (i.e., changes that would +be noted with ==== in the normal listing) differently. +The overlapping lines from both files will be inserted by the edit script, +bracketed by "<<<<<<" and ">>>>>>" lines. +.It Fl e , Fl Fl ed +Produces output in a form suitable as an input script for the +.Xr ed 1 +utility. +The script may then be used to merge differences common between all +three files and differences specific to +.Ar file1 +and +.Ar file3 . +In other words, the +.Fl e +option ignores differences specific to +.Ar file1 +and +.Ar file2 , +and those specific to +.Ar file2 +and +.Ar file3 . +It is useful for backing out changes specific to +.Ar file2 +only. +.It Fl Fl help +Prints usage information and exits. +.It Fl i +Appends 'w' and 'q' +.Xr ed 1 +commands. +.It Fl L , Fl Fl label +Defines labels to print instead of file names +.Ar file1 , +.Ar file2 +and +.Ar file3 . +.It Fl m, Fl Fl merge +Merge output instead of generating ed script. +.It Fl T, Fl Fl initial-tab +In the normal listing, +use a tab instead of two spaces +at the beginning of each line. +In modes that produce an +.Xr ed 1 +script, this option changes nothing. +.It Fl x, Fl Fl overlap-only +Produces an output script suitable for +.Xr ed 1 +with changes +specific only to all three versions. +.It Fl Fl diff-program Ar program +Use +.Ar program +instead of the default +.Xr diff 1 +to compare files. +.It Fl Fl strip-trailing-cr +Strip trailing carriage return on input files. +.It Fl Fl version +Prints version information and exits. +.El +.Pp +The +.Fl E +option is used by +.Tn RCS +.Xr merge 1 +to ensure that overlapping changes in the merged files are preserved +and brought to someone's attention. +.Pp +For example, suppose lines 7-8 are changed in both +.Ar file1 +and +.Ar file2 . +Applying the edit script generated by the command +.Pp +.Dl $ diff3 -E file1 file2 file3 +.Pp +to +.Ar file1 +results in the file: +.Bd -literal -offset indent +lines 1-6 +of file1 +<<<<<<< file1 +lines 7-8 +of file1 +======= +lines 7-8 +of file3 +>>>>>>> file3 +rest of file1 +.Ed +.Pp +The default output of +.Nm +makes notation of the differences between all files, and those +differences specific to each pair of files. +The changes are described by the commands necessary for +.Xr ed 1 +to create the desired target from the different versions. +See +.Xr diff 1 +for a description of the commands. +.Bl -tag -width "=====" +.It Li \&==== +The lines beneath this notation are ranges of lines which are different +between all files. +.It \&==== Ns Va n +The lines beneath this notation are ranges of lines which are exclusively +different in file +.Va n . +.El +.Sh SEE ALSO +.Xr diff 1 , +.Xr ed 1 , +.Xr merge 1 , +.Xr rcs 1 , +.Xr sdiff 1 +.Sh HISTORY +A +.Nm +command appeared in +.At v7 . +.Sh BUGS +The +.Fl e +option cannot catch and change lines which have +.Ql \&. +as the first and only character on the line. +The resulting script will fail on that line +as +.Ql \&. +is an +.Xr ed 1 +editing command. diff --git a/src/diff3/diff3.c b/src/diff3/diff3.c new file mode 100644 index 0000000..70b3f87 --- /dev/null +++ b/src/diff3/diff3.c @@ -0,0 +1,977 @@ +/* $OpenBSD: diff3prog.c,v 1.11 2009/10/27 23:59:37 deraadt Exp $ */ + +/* + * Copyright (C) Caldera International Inc. 2001-2002. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code and documentation must retain the above + * copyright notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed or owned by Caldera + * International, Inc. + * 4. Neither the name of Caldera International, Inc. nor the names of other + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * USE OF THE SOFTWARE PROVIDED FOR UNDER THIS LICENSE BY CALDERA + * INTERNATIONAL, INC. 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 CALDERA INTERNATIONAL, INC. 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. + */ +/*- + * Copyright (c) 1991, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + * + * @(#)diff3.c 8.1 (Berkeley) 6/6/93 + */ + +#if 0 +#ifndef lint +static char sccsid[] = "@(#)diff3.c 8.1 (Berkeley) 6/6/93"; +#endif +#endif /* not lint */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/* + * "from" is first in range of changed lines; "to" is last+1 + * from=to=line after point of insertion for added lines. + */ +struct range { + int from; + int to; +}; + +struct diff { +#define DIFF_TYPE2 2 +#define DIFF_TYPE3 3 + int type; +#if DEBUG + char *line; +#endif /* DEBUG */ + + /* Ranges as lines */ + struct range old; + struct range new; +}; + +#define EFLAG_NONE 0 +#define EFLAG_OVERLAP 1 +#define EFLAG_NOOVERLAP 2 +#define EFLAG_UNMERGED 3 + +static size_t szchanges; + +static struct diff *d13; +static struct diff *d23; +/* + * "de" is used to gather editing scripts. These are later spewed out in + * reverse order. Its first element must be all zero, the "old" and "new" + * components of "de" contain line positions. Array overlap indicates which + * sections in "de" correspond to lines that are different in all three files. + */ +static struct diff *de; +static char *overlap; +static int overlapcnt; +static FILE *fp[3]; +static int cline[3]; /* # of the last-read line in each file (0-2) */ +/* + * The latest known correspondence between line numbers of the 3 files + * is stored in last[1-3]; + */ +static int last[4]; +static int Aflag, eflag, iflag, mflag, Tflag; +static int oflag; /* indicates whether to mark overlaps (-E or -X) */ +static int strip_cr; +static char *f1mark, *f2mark, *f3mark; +static const char *oldmark = "<<<<<<<"; +static const char *orgmark = "|||||||"; +static const char *newmark = ">>>>>>>"; +static const char *divider = "======="; + +static bool duplicate(struct range *, struct range *); +static int edit(struct diff *, bool, int, int); +static char *getchange(FILE *); +static char *get_line(FILE *, size_t *); +static int readin(int fd, struct diff **); +static int skip(int, int, const char *); +static void change(int, struct range *, bool); +static void keep(int, struct range *); +static void merge(int, int); +static void prange(struct range *, bool); +static void repos(int); +static void edscript(int) __dead2; +static void Ascript(int) __dead2; +static void mergescript(int) __dead2; +static void increase(void); +static void usage(void); +static void printrange(FILE *, struct range *); + +static const char diff3_version[] = "FreeBSD diff3 20220517"; + +enum { + DIFFPROG_OPT, + STRIPCR_OPT, + HELP_OPT, + VERSION_OPT +}; + +#define DIFF_PATH "/usr/bin/diff" + +#define OPTIONS "3aAeEiL:mTxX" +static struct option longopts[] = { + { "ed", no_argument, NULL, 'e' }, + { "show-overlap", no_argument, NULL, 'E' }, + { "overlap-only", no_argument, NULL, 'x' }, + { "initial-tab", no_argument, NULL, 'T' }, + { "text", no_argument, NULL, 'a' }, + { "strip-trailing-cr", no_argument, NULL, STRIPCR_OPT }, + { "show-all", no_argument, NULL, 'A' }, + { "easy-only", no_argument, NULL, '3' }, + { "merge", no_argument, NULL, 'm' }, + { "label", required_argument, NULL, 'L' }, + { "diff-program", required_argument, NULL, DIFFPROG_OPT }, + { "help", no_argument, NULL, HELP_OPT}, + { "version", no_argument, NULL, VERSION_OPT} +}; + +static void +usage(void) +{ + fprintf(stderr, "usage: diff3 [-3aAeEimTxX] [-L label1] [-L label2] " + "[-L label3] file1 file2 file3\n"); +} + +static int +readin(int fd, struct diff **dd) +{ + int a, b, c, d; + size_t i; + char kind, *p; + FILE *f; + + f = fdopen(fd, "r"); + if (f == NULL) + err(2, "fdopen"); + for (i = 0; (p = getchange(f)); i++) { +#if DEBUG + (*dd)[i].line = strdup(p); +#endif /* DEBUG */ + + if (i >= szchanges - 1) + increase(); + a = b = (int)strtoimax(p, &p, 10); + if (*p == ',') { + p++; + b = (int)strtoimax(p, &p, 10); + } + kind = *p++; + c = d = (int)strtoimax(p, &p, 10); + if (*p == ',') { + p++; + d = (int)strtoimax(p, &p, 10); + } + if (kind == 'a') + a++; + if (kind == 'd') + c++; + b++; + d++; + (*dd)[i].old.from = a; + (*dd)[i].old.to = b; + (*dd)[i].new.from = c; + (*dd)[i].new.to = d; + } + if (i) { + (*dd)[i].old.from = (*dd)[i - 1].old.to; + (*dd)[i].new.from = (*dd)[i - 1].new.to; + } + fclose(f); + return (i); +} + +static pid_t +diffexec(const char *diffprog, char **diffargv, int fd[]) +{ + pid_t pid; + + switch ((pid = fork())) { + case 0: + close(fd[0]); + if (dup2(fd[1], STDOUT_FILENO) == -1) + err(2, "child could not duplicate descriptor"); + close(fd[1]); + execvp(diffprog, diffargv); + err(2, "could not execute diff: %s", diffprog); + break; + case -1: + err(2, "could not fork"); + break; + } + close(fd[1]); + return (pid); +} + +static char * +getchange(FILE *b) +{ + char *line; + + while ((line = get_line(b, NULL))) { + if (isdigit((unsigned char)line[0])) + return (line); + } + return (NULL); +} + + +static char * +get_line(FILE *b, size_t *n) +{ + ssize_t len; + static char *buf = NULL; + static size_t bufsize = 0; + + if ((len = getline(&buf, &bufsize, b)) < 0) + return (NULL); + + if (strip_cr && len >= 2 && strcmp("\r\n", &(buf[len - 2])) == 0) { + buf[len - 2] = '\n'; + buf[len - 1] = '\0'; + len--; + } + + if (n != NULL) + *n = len; + + return (buf); +} + +static void +merge(int m1, int m2) +{ + struct diff *d1, *d2, *d3; + int j, t1, t2; + bool dup = false; + + d1 = d13; + d2 = d23; + j = 0; + + while (t1 = d1 < d13 + m1, t2 = d2 < d23 + m2, t1 || t2) { + /* first file is different from the others */ + if (!t2 || (t1 && d1->new.to < d2->new.from)) { + /* stuff peculiar to 1st file */ + if (eflag == EFLAG_NONE) { + printf("====1\n"); + change(1, &d1->old, false); + keep(2, &d1->new); + change(3, &d1->new, false); + } + d1++; + continue; + } + /* second file is different from others */ + if (!t1 || (t2 && d2->new.to < d1->new.from)) { + if (eflag == EFLAG_NONE) { + printf("====2\n"); + keep(1, &d2->new); + change(3, &d2->new, false); + change(2, &d2->old, false); + } else if (Aflag || mflag) { + // XXX-THJ: What does it mean for the second file to differ? + j = edit(d2, dup, j, DIFF_TYPE2); + } + d2++; + continue; + } + /* + * Merge overlapping changes in first file + * this happens after extension (see below). + */ + if (d1 + 1 < d13 + m1 && d1->new.to >= d1[1].new.from) { + d1[1].old.from = d1->old.from; + d1[1].new.from = d1->new.from; + d1++; + continue; + } + + /* merge overlapping changes in second */ + if (d2 + 1 < d23 + m2 && d2->new.to >= d2[1].new.from) { + d2[1].old.from = d2->old.from; + d2[1].new.from = d2->new.from; + d2++; + continue; + } + /* stuff peculiar to third file or different in all */ + if (d1->new.from == d2->new.from && d1->new.to == d2->new.to) { + dup = duplicate(&d1->old, &d2->old); + /* + * dup = 0 means all files differ + * dup = 1 means files 1 and 2 identical + */ + if (eflag == EFLAG_NONE) { + printf("====%s\n", dup ? "3" : ""); + change(1, &d1->old, dup); + change(2, &d2->old, false); + d3 = d1->old.to > d1->old.from ? d1 : d2; + change(3, &d3->new, false); + } else { + j = edit(d1, dup, j, DIFF_TYPE3); + } + dup = false; + d1++; + d2++; + continue; + } + /* + * Overlapping changes from file 1 and 2; extend changes + * appropriately to make them coincide. + */ + if (d1->new.from < d2->new.from) { + d2->old.from -= d2->new.from - d1->new.from; + d2->new.from = d1->new.from; + } else if (d2->new.from < d1->new.from) { + d1->old.from -= d1->new.from - d2->new.from; + d1->new.from = d2->new.from; + } + if (d1->new.to > d2->new.to) { + d2->old.to += d1->new.to - d2->new.to; + d2->new.to = d1->new.to; + } else if (d2->new.to > d1->new.to) { + d1->old.to += d2->new.to - d1->new.to; + d1->new.to = d2->new.to; + } + } + + if (mflag) + mergescript(j); + else if (Aflag) + Ascript(j); + else if (eflag) + edscript(j); +} + +/* + * The range of lines rold.from thru rold.to in file i is to be changed. + * It is to be printed only if it does not duplicate something to be + * printed later. + */ +static void +change(int i, struct range *rold, bool dup) +{ + + printf("%d:", i); + last[i] = rold->to; + prange(rold, false); + if (dup) + return; + i--; + skip(i, rold->from, NULL); + skip(i, rold->to, " "); +} + +/* + * Print the range of line numbers, rold.from thru rold.to, as n1,n2 or + * n1. + */ +static void +prange(struct range *rold, bool delete) +{ + + if (rold->to <= rold->from) + printf("%da\n", rold->from - 1); + else { + printf("%d", rold->from); + if (rold->to > rold->from + 1) + printf(",%d", rold->to - 1); + if (delete) + printf("d\n"); + else + printf("c\n"); + } +} + +/* + * No difference was reported by diff between file 1 (or 2) and file 3, + * and an artificial dummy difference (trange) must be ginned up to + * correspond to the change reported in the other file. + */ +static void +keep(int i, struct range *rnew) +{ + int delta; + struct range trange; + + delta = last[3] - last[i]; + trange.from = rnew->from - delta; + trange.to = rnew->to - delta; + change(i, &trange, true); +} + +/* + * skip to just before line number from in file "i". If "pr" is non-NULL, + * print all skipped stuff with string pr as a prefix. + */ +static int +skip(int i, int from, const char *pr) +{ + size_t j, n; + char *line; + + for (n = 0; cline[i] < from - 1; n += j) { + if ((line = get_line(fp[i], &j)) == NULL) + errx(EXIT_FAILURE, "logic error"); + if (pr != NULL) + printf("%s%s", Tflag == 1 ? "\t" : pr, line); + cline[i]++; + } + return ((int) n); +} + +/* + * Return 1 or 0 according as the old range (in file 1) contains exactly + * the same data as the new range (in file 2). + */ +static bool +duplicate(struct range *r1, struct range *r2) +{ + int c, d; + int nchar; + int nline; + + if (r1->to-r1->from != r2->to-r2->from) + return (0); + skip(0, r1->from, NULL); + skip(1, r2->from, NULL); + nchar = 0; + for (nline = 0; nline < r1->to - r1->from; nline++) { + do { + c = getc(fp[0]); + d = getc(fp[1]); + if (c == -1 && d == -1) + break; + if (c == -1 || d == -1) + errx(EXIT_FAILURE, "logic error"); + nchar++; + if (c != d) { + repos(nchar); + return (0); + } + } while (c != '\n'); + } + repos(nchar); + return (1); +} + +static void +repos(int nchar) +{ + int i; + + for (i = 0; i < 2; i++) + (void)fseek(fp[i], (long)-nchar, SEEK_CUR); +} + +/* + * collect an editing script for later regurgitation + */ +static int +edit(struct diff *diff, bool dup, int j, int difftype) +{ + if (!(eflag == EFLAG_UNMERGED || + (!dup && eflag == EFLAG_OVERLAP ) || + (dup && eflag == EFLAG_NOOVERLAP))) { + return (j); + } + j++; + overlap[j] = !dup; + if (!dup) + overlapcnt++; + + de[j].type = difftype; +#if DEBUG + de[j].line = strdup(diff->line); +#endif /* DEBUG */ + + de[j].old.from = diff->old.from; + de[j].old.to = diff->old.to; + de[j].new.from = diff->new.from; + de[j].new.to = diff->new.to; + return (j); +} + +static void +printrange(FILE *p, struct range *r) +{ + char *line = NULL; + size_t len = 0; + int i = 1; + ssize_t rlen = 0; + + /* We haven't been asked to print anything */ + if (r->from == r->to) + return; + + if (r->from > r->to) + errx(EXIT_FAILURE, "invalid print range"); + + /* + * XXX-THJ: We read through all of the file for each range printed. + * This duplicates work and will probably impact performance on large + * files with lots of ranges. + */ + fseek(p, 0L, SEEK_SET); + while ((rlen = getline(&line, &len, p)) > 0) { + if (i >= r->from) + printf("%s", line); + if (++i > r->to - 1) + break; + } + free(line); +} + +/* regurgitate */ +static void +edscript(int n) +{ + bool delete; + struct range *new, *old; + + for (; n > 0; n--) { + new = &de[n].new; + old = &de[n].old; + + delete = (new->from == new->to); + if (!oflag || !overlap[n]) { + prange(old, delete); + } else { + printf("%da\n", old->to - 1); + printf("%s\n", divider); + } + printrange(fp[2], new); + if (!oflag || !overlap[n]) { + if (!delete) + printf(".\n"); + } else { + printf("%s %s\n.\n", newmark, f3mark); + printf("%da\n%s %s\n.\n", old->from - 1, + oldmark, f1mark); + } + } + if (iflag) + printf("w\nq\n"); + + exit(eflag == EFLAG_NONE ? overlapcnt : 0); +} + +/* + * Output an edit script to turn mine into yours, when there is a conflict + * between the 3 files bracket the changes. Regurgitate the diffs in reverse + * order to allow the ed script to track down where the lines are as changes + * are made. + */ +static void +Ascript(int n) +{ + int startmark; + bool deletenew; + bool deleteold; + + struct range *new, *old; + + for (; n > 0; n--) { + new = &de[n].new; + old = &de[n].old; + deletenew = (new->from == new->to); + deleteold = (old->from == old->to); + + if (de[n].type == DIFF_TYPE2) { + if (!oflag || !overlap[n]) { + prange(old, deletenew); + printrange(fp[2], new); + } else { + startmark = new->to; + + if (!deletenew) + startmark--; + + printf("%da\n", startmark); + printf("%s %s\n", newmark, f3mark); + + printf(".\n"); + + printf("%da\n", startmark - + (new->to - new->from)); + printf("%s %s\n", oldmark, f2mark); + if (!deleteold) + printrange(fp[1], old); + printf("%s\n.\n", divider); + } + + } else if (de[n].type == DIFF_TYPE3) { + startmark = old->to - 1; + + if (!oflag || !overlap[n]) { + prange(old, deletenew); + printrange(fp[2], new); + } else { + printf("%da\n", startmark); + printf("%s %s\n", orgmark, f2mark); + + if (deleteold) { + struct range r; + r.from = old->from-1; + r.to = new->to; + printrange(fp[1], &r); + } else + printrange(fp[1], old); + + printf("%s\n", divider); + printrange(fp[2], new); + } + + if (!oflag || !overlap[n]) { + if (!deletenew) + printf(".\n"); + } else { + printf("%s %s\n.\n", newmark, f3mark); + + /* + * Go to the start of the conflict in original + * file and append lines + */ + printf("%da\n%s %s\n.\n", + startmark - (old->to - old->from), + oldmark, f1mark); + } + } + } + if (iflag) + printf("w\nq\n"); + + exit(overlapcnt > 0); +} + +/* + * Output the merged file directly (don't generate an ed script). When + * regurgitating diffs we need to walk forward through the file and print any + * inbetween lines. + */ +static void +mergescript(int i) +{ + struct range r, *new, *old; + int n; + + r.from = 1; + r.to = 1; + + for (n = 1; n < i+1; n++) { + new = &de[n].new; + old = &de[n].old; + + /* print any lines leading up to here */ + r.to = old->from; + printrange(fp[0], &r); + + if (de[n].type == DIFF_TYPE2) { + printf("%s %s\n", oldmark, f2mark); + printrange(fp[1], old); + printf("%s\n", divider); + printrange(fp[2], new); + printf("%s %s\n", newmark, f3mark); + } else if (de[n].type == DIFF_TYPE3) { + if (!oflag || !overlap[n]) { + printrange(fp[2], new); + } else { + + printf("%s %s\n", oldmark, f1mark); + printrange(fp[0], old); + + printf("%s %s\n", orgmark, f2mark); + if (old->from == old->to) { + struct range or; + or.from = old->from - 1; + or.to = new->to; + printrange(fp[1], &or); + } else + printrange(fp[1], old); + + printf("%s\n", divider); + + printrange(fp[2], new); + printf("%s %s\n", newmark, f3mark); + } + } + + if (old->from == old->to) + r.from = new->to; + else + r.from = old->to; + } + /* + * Print from the final range to the end of 'myfile'. Any deletions or + * additions to this file should have been handled by now. + * + * If the ranges are the same we need to rewind a line. + * If the new range is 0 length (from == to), we need to use the old + * range. + */ + new = &de[n-1].new; + old = &de[n-1].old; + if ((old->from == new->from) && + (old->to == new->to)) + r.from--; + else if (new->from == new->to) + r.from = old->from; + + /* + * If the range is a 3 way merge then we need to skip a line in the + * trailing output. + */ + if (de[n-1].type == DIFF_TYPE3) + r.from++; + + r.to = INT_MAX; + printrange(fp[0], &r); + exit(overlapcnt > 0); +} + +static void +increase(void) +{ + struct diff *p; + char *q; + size_t newsz, incr; + + /* are the memset(3) calls needed? */ + newsz = szchanges == 0 ? 64 : 2 * szchanges; + incr = newsz - szchanges; + + p = reallocarray(d13, newsz, sizeof(struct diff)); + if (p == NULL) + err(1, NULL); + memset(p + szchanges, 0, incr * sizeof(struct diff)); + d13 = p; + p = reallocarray(d23, newsz, sizeof(struct diff)); + if (p == NULL) + err(1, NULL); + memset(p + szchanges, 0, incr * sizeof(struct diff)); + d23 = p; + p = reallocarray(de, newsz, sizeof(struct diff)); + if (p == NULL) + err(1, NULL); + memset(p + szchanges, 0, incr * sizeof(struct diff)); + de = p; + q = reallocarray(overlap, newsz, sizeof(char)); + if (q == NULL) + err(1, NULL); + memset(q + szchanges, 0, incr * sizeof(char)); + overlap = q; + szchanges = newsz; +} + + +int +diff3_main(int argc, char **argv) +{ + int ch, nblabels, status, m, n, i; + char *labels[] = { NULL, NULL, NULL }; + const char *diffprog = DIFF_PATH; + char *file1, *file2, *file3; + char *diffargv[7]; + int diffargc = 0; + int fd13[2], fd23[2]; + pid_t pd13, pd23; + + nblabels = 0; + eflag = EFLAG_NONE; + oflag = 0; + diffargv[diffargc++] = __DECONST(char *, diffprog); + while ((ch = getopt_long(argc, argv, OPTIONS, longopts, NULL)) != -1) { + switch (ch) { + case '3': + eflag = EFLAG_NOOVERLAP; + break; + case 'a': + diffargv[diffargc++] = __DECONST(char *, "-a"); + break; + case 'A': + Aflag = 1; + break; + case 'e': + eflag = EFLAG_UNMERGED; + break; + case 'E': + eflag = EFLAG_UNMERGED; + oflag = 1; + break; + case 'i': + iflag = 1; + break; + case 'L': + oflag = 1; + if (nblabels >= 3) + errx(2, "too many file label options"); + labels[nblabels++] = optarg; + break; + case 'm': + Aflag = 1; + oflag = 1; + mflag = 1; + break; + case 'T': + Tflag = 1; + break; + case 'x': + eflag = EFLAG_OVERLAP; + break; + case 'X': + oflag = 1; + eflag = EFLAG_OVERLAP; + break; + case DIFFPROG_OPT: + diffprog = optarg; + break; + case STRIPCR_OPT: + strip_cr = 1; + diffargv[diffargc++] = __DECONST(char *, "--strip-trailing-cr"); + break; + case HELP_OPT: + usage(); + exit(0); + case VERSION_OPT: + printf("%s\n", diff3_version); + exit(0); + } + } + argc -= optind; + argv += optind; + + if (Aflag) { + eflag = EFLAG_UNMERGED; + oflag = 1; + } + + if (argc != 3) { + usage(); + exit(2); + } + + /* TODO stdio */ + file1 = argv[0]; + file2 = argv[1]; + file3 = argv[2]; + + if (oflag) { + asprintf(&f1mark, "%s", + labels[0] != NULL ? labels[0] : file1); + if (f1mark == NULL) + err(2, "asprintf"); + asprintf(&f2mark, "%s", + labels[1] != NULL ? labels[1] : file2); + if (f2mark == NULL) + err(2, "asprintf"); + asprintf(&f3mark, "%s", + labels[2] != NULL ? labels[2] : file3); + if (f3mark == NULL) + err(2, "asprintf"); + } + fp[0] = fopen(file1, "r"); + if (fp[0] == NULL) + err(2, "Can't open %s", file1); + + fp[1] = fopen(file2, "r"); + if (fp[1] == NULL) + err(2, "Can't open %s", file2); + + fp[2] = fopen(file3, "r"); + if (fp[2] == NULL) + err(2, "Can't open %s", file3); + + if (pipe(fd13)) + err(2, "pipe"); + if (pipe(fd23)) + err(2, "pipe"); + + diffargv[diffargc] = file1; + diffargv[diffargc + 1] = file3; + diffargv[diffargc + 2] = NULL; + + pd13 = diffexec(diffprog, diffargv, fd13); + + diffargv[diffargc] = file2; + pd23 = diffexec(diffprog, diffargv, fd23); + + /* parse diffs */ + increase(); + m = readin(fd13[0], &d13); + n = readin(fd23[0], &d23); + + /* wait for diff children */ + for (i = 0; i < 2; i++) { + pid_t pid = (i == 0) ? pd13 : pd23; + if (waitpid(pid, &status, 0) == -1) + err(2, "waitpid"); + if (WIFEXITED(status) && WEXITSTATUS(status) >= 2) + errx(2, "diff exited abnormally"); + else if (WIFSIGNALED(status)) + errx(2, "diff killed by signal %d", + WTERMSIG(status)); + } + merge(m, n); + + return (EXIT_SUCCESS); +} diff --git a/src/diff3/tests/1.out b/src/diff3/tests/1.out new file mode 100644 index 0000000..9b872bf --- /dev/null +++ b/src/diff3/tests/1.out @@ -0,0 +1,14 @@ +==== +1:2,3c + which multiple lines + and a few typ0s to fix +2:2,3c + which multiple lines + and a few typos to fix +3:2c + and a few typ0s to fix +====2 +1:4a +3:3a +2:5c + New line diff --git a/src/diff3/tests/1.txt b/src/diff3/tests/1.txt new file mode 100644 index 0000000..f0cd806 --- /dev/null +++ b/src/diff3/tests/1.txt @@ -0,0 +1,6 @@ +This is a more complete test +which multiple lines +and a few typ0s to fix +also I plan to add few lines + and to remove +other lines diff --git a/src/diff3/tests/1cr.txt b/src/diff3/tests/1cr.txt new file mode 100644 index 0000000..c24bf24 --- /dev/null +++ b/src/diff3/tests/1cr.txt @@ -0,0 +1,6 @@ +This is a more complete test +which multiple lines +and a few typ0s to fix +also I plan to add few lines + and to remove +other lines diff --git a/src/diff3/tests/1t.out b/src/diff3/tests/1t.out new file mode 100644 index 0000000..fc54ce5 --- /dev/null +++ b/src/diff3/tests/1t.out @@ -0,0 +1,14 @@ +==== +1:2,3c + which multiple lines + and a few typ0s to fix +2:2,3c + which multiple lines + and a few typos to fix +3:2c + and a few typ0s to fix +====2 +1:4a +3:3a +2:5c + New line diff --git a/src/diff3/tests/2.out b/src/diff3/tests/2.out new file mode 100644 index 0000000..420dc47 --- /dev/null +++ b/src/diff3/tests/2.out @@ -0,0 +1,3 @@ +2,3c +and a few typ0s to fix +. diff --git a/src/diff3/tests/2.txt b/src/diff3/tests/2.txt new file mode 100644 index 0000000..990aa6f --- /dev/null +++ b/src/diff3/tests/2.txt @@ -0,0 +1,7 @@ +This is a more complete test +which multiple lines +and a few typos to fix +also I plan to add few lines +New line + and to remove +other lines diff --git a/src/diff3/tests/3.out b/src/diff3/tests/3.out new file mode 100644 index 0000000..81509d4 --- /dev/null +++ b/src/diff3/tests/3.out @@ -0,0 +1,8 @@ +3a +======= +and a few typ0s to fix +>>>>>>> 3 +. +1a +<<<<<<< 1 +. diff --git a/src/diff3/tests/3.txt b/src/diff3/tests/3.txt new file mode 100644 index 0000000..104466e --- /dev/null +++ b/src/diff3/tests/3.txt @@ -0,0 +1,5 @@ +This is a more complete test +and a few typ0s to fix +also I plan to add few lines + and to remove +other lines diff --git a/src/diff3/tests/4.out b/src/diff3/tests/4.out new file mode 100644 index 0000000..81509d4 --- /dev/null +++ b/src/diff3/tests/4.out @@ -0,0 +1,8 @@ +3a +======= +and a few typ0s to fix +>>>>>>> 3 +. +1a +<<<<<<< 1 +. diff --git a/src/diff3/tests/5.out b/src/diff3/tests/5.out new file mode 100644 index 0000000..420dc47 --- /dev/null +++ b/src/diff3/tests/5.out @@ -0,0 +1,3 @@ +2,3c +and a few typ0s to fix +. diff --git a/src/diff3/tests/6.out b/src/diff3/tests/6.out new file mode 100644 index 0000000..e69de29 diff --git a/src/diff3/tests/7.out b/src/diff3/tests/7.out new file mode 100644 index 0000000..9b872bf --- /dev/null +++ b/src/diff3/tests/7.out @@ -0,0 +1,14 @@ +==== +1:2,3c + which multiple lines + and a few typ0s to fix +2:2,3c + which multiple lines + and a few typos to fix +3:2c + and a few typ0s to fix +====2 +1:4a +3:3a +2:5c + New line diff --git a/src/diff3/tests/8.out b/src/diff3/tests/8.out new file mode 100644 index 0000000..5d02326 --- /dev/null +++ b/src/diff3/tests/8.out @@ -0,0 +1,19 @@ +4a +>>>>>>> 3 +. +4a +<<<<<<< 2 +New line +======= +. +3a +||||||| 2 +which multiple lines +and a few typos to fix +======= +and a few typ0s to fix +>>>>>>> 3 +. +1a +<<<<<<< 1 +. diff --git a/src/diff3/tests/9.out b/src/diff3/tests/9.out new file mode 100644 index 0000000..2071bab --- /dev/null +++ b/src/diff3/tests/9.out @@ -0,0 +1,17 @@ +This is a more complete test +<<<<<<< 1 +which multiple lines +and a few typ0s to fix +||||||| 2 +which multiple lines +and a few typos to fix +======= +and a few typ0s to fix +>>>>>>> 3 +also I plan to add few lines +<<<<<<< 2 +New line +======= +>>>>>>> 3 + and to remove +other lines diff --git a/src/diff3/tests/Makefile b/src/diff3/tests/Makefile new file mode 100644 index 0000000..df000d6 --- /dev/null +++ b/src/diff3/tests/Makefile @@ -0,0 +1,27 @@ + +PACKAGE= tests + +ATF_TESTS_SH= diff3_test +${PACKAGE}FILES+= \ + 1.txt \ + 1cr.txt \ + 2.txt \ + 3.txt \ + long-m.txt \ + long-o.txt \ + long-y.txt \ + 1.out \ + 1t.out \ + 2.out \ + 3.out \ + 4.out \ + 5.out \ + 6.out \ + 7.out \ + 8.out \ + 9.out \ + long-ed.out \ + long-A.out \ + long-merge.out \ + +.include diff --git a/src/diff3/tests/Makefile.depend b/src/diff3/tests/Makefile.depend new file mode 100644 index 0000000..11aba52 --- /dev/null +++ b/src/diff3/tests/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/src/diff3/tests/diff3_test.sh b/src/diff3/tests/diff3_test.sh new file mode 100755 index 0000000..3a4fc12 --- /dev/null +++ b/src/diff3/tests/diff3_test.sh @@ -0,0 +1,76 @@ + +atf_test_case diff3 +atf_test_case diff3_lesssimple +atf_test_case diff3_ed +atf_test_case diff3_A +atf_test_case diff3_merge + +diff3_body() +{ + atf_check -o file:$(atf_get_srcdir)/1.out \ + diff3 $(atf_get_srcdir)/1.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt + + atf_check -o file:$(atf_get_srcdir)/1.out \ + diff3 --strip-trailing-cr $(atf_get_srcdir)/1cr.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt + + atf_check -o file:$(atf_get_srcdir)/1t.out \ + diff3 -T $(atf_get_srcdir)/1.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt + + atf_check -o file:$(atf_get_srcdir)/2.out \ + diff3 -e $(atf_get_srcdir)/1.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt + + atf_check -o file:$(atf_get_srcdir)/3.out \ + diff3 -E -L 1 -L 2 -L 3 $(atf_get_srcdir)/1.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt + + atf_check -o file:$(atf_get_srcdir)/4.out \ + diff3 -X -L 1 -L 2 -L 3 $(atf_get_srcdir)/1.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt + + atf_check -o file:$(atf_get_srcdir)/5.out \ + diff3 -x $(atf_get_srcdir)/1.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt + + atf_check -o file:$(atf_get_srcdir)/6.out \ + diff3 -3 $(atf_get_srcdir)/1.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt + + atf_check -o file:$(atf_get_srcdir)/7.out \ + diff3 -i $(atf_get_srcdir)/1.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt +} + +diff3_lesssimple_body() +{ + atf_check -s exit:1 -o file:$(atf_get_srcdir)/10.out \ + diff3 -m -L 1 -L 2 -L 3 $(atf_get_srcdir)/4.txt $(atf_get_srcdir)/5.txt $(atf_get_srcdir)/6.txt +} + +diff3_ed_body() +{ + atf_check -s exit:0 -o file:$(atf_get_srcdir)/long-ed.out \ + diff3 -e $(atf_get_srcdir)/long-m.txt $(atf_get_srcdir)/long-o.txt $(atf_get_srcdir)/long-y.txt +} + +diff3_A_body() +{ + atf_check -s exit:1 -o file:$(atf_get_srcdir)/8.out \ + diff3 -A -L 1 -L 2 -L 3 $(atf_get_srcdir)/1.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt + + atf_check -s exit:1 -o file:$(atf_get_srcdir)/long-A.out \ + diff3 -A -L long-m.txt -L long-o.txt -L long-y.txt $(atf_get_srcdir)/long-m.txt $(atf_get_srcdir)/long-o.txt $(atf_get_srcdir)/long-y.txt +} + + +diff3_merge_body() +{ + atf_check -s exit:1 -o file:$(atf_get_srcdir)/9.out \ + diff3 -m -L 1 -L 2 -L 3 $(atf_get_srcdir)/1.txt $(atf_get_srcdir)/2.txt $(atf_get_srcdir)/3.txt + + atf_check -s exit:1 -o file:$(atf_get_srcdir)/long-merge.out \ + diff3 -m -L long-m.txt -L long-o.txt -L long-y.txt $(atf_get_srcdir)/long-m.txt $(atf_get_srcdir)/long-o.txt $(atf_get_srcdir)/long-y.txt +} + +atf_init_test_cases() +{ + atf_add_test_case diff3 +# atf_add_test_case diff3_lesssimple + atf_add_test_case diff3_ed + atf_add_test_case diff3_A + atf_add_test_case diff3_merge +} diff --git a/src/diff3/tests/long-A.out b/src/diff3/tests/long-A.out new file mode 100644 index 0000000..2a2e4cb --- /dev/null +++ b/src/diff3/tests/long-A.out @@ -0,0 +1,22 @@ +24d +16a +||||||| long-o.txt +This line is different in yours and mine, but the different in each +======= +This line is different in yours and mine, but the best in yours +>>>>>>> long-y.txt +. +15a +<<<<<<< long-m.txt +. +11a +>>>>>>> long-y.txt +. +10a +<<<<<<< long-o.txt +This line is different in yours and mine, but the same +======= +. +8c +This line is different in yours, much butter +. diff --git a/src/diff3/tests/long-ed.out b/src/diff3/tests/long-ed.out new file mode 100644 index 0000000..8814a35 --- /dev/null +++ b/src/diff3/tests/long-ed.out @@ -0,0 +1,7 @@ +24d +16c +This line is different in yours and mine, but the best in yours +. +8c +This line is different in yours, much butter +. diff --git a/src/diff3/tests/long-m.txt b/src/diff3/tests/long-m.txt new file mode 100644 index 0000000..327bf8c --- /dev/null +++ b/src/diff3/tests/long-m.txt @@ -0,0 +1,26 @@ +This is a long file +These lines are the same in all three files +These lines are the same in all three files +This line is different in mine, not better +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is different in yours +These lines are the same in all three files +These lines are the same in all three files +This line is different in yours and mine, the same is in both +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is different in yours and mine, best change in mine +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is deleted in yours +These lines are the same in all three files +These lines are the same in all three files diff --git a/src/diff3/tests/long-merge.out b/src/diff3/tests/long-merge.out new file mode 100644 index 0000000..5139a48 --- /dev/null +++ b/src/diff3/tests/long-merge.out @@ -0,0 +1,35 @@ +This is a long file +These lines are the same in all three files +These lines are the same in all three files +This line is different in mine, not better +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is different in yours, much butter +These lines are the same in all three files +These lines are the same in all three files +<<<<<<< long-o.txt +This line is different in yours and mine, but the same +======= +This line is different in yours and mine, the same is in both +>>>>>>> long-y.txt +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +<<<<<<< long-m.txt +This line is different in yours and mine, best change in mine +||||||| long-o.txt +This line is different in yours and mine, but the different in each +======= +This line is different in yours and mine, but the best in yours +>>>>>>> long-y.txt +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files diff --git a/src/diff3/tests/long-o.txt b/src/diff3/tests/long-o.txt new file mode 100644 index 0000000..b4085d1 --- /dev/null +++ b/src/diff3/tests/long-o.txt @@ -0,0 +1,27 @@ +This is a long file +These lines are the same in all three files +These lines are the same in all three files +This line is different in mine +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is different in yours +These lines are the same in all three files +These lines are the same in all three files +This line is different in yours and mine, but the same +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is different in yours and mine, but the different in each +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is deleted in mine +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is deleted in yours +These lines are the same in all three files +These lines are the same in all three files diff --git a/src/diff3/tests/long-y.txt b/src/diff3/tests/long-y.txt new file mode 100644 index 0000000..1bb2500 --- /dev/null +++ b/src/diff3/tests/long-y.txt @@ -0,0 +1,26 @@ +This is a long file +These lines are the same in all three files +These lines are the same in all three files +This line is different in mine +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is different in yours, much butter +These lines are the same in all three files +These lines are the same in all three files +This line is different in yours and mine, the same is in both +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is different in yours and mine, but the best in yours +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +This line is deleted in mine +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files +These lines are the same in all three files diff --git a/src/find/extern.h b/src/find/extern.h new file mode 100644 index 0000000..3b84249 --- /dev/null +++ b/src/find/extern.h @@ -0,0 +1,133 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1991, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + * + * @(#)extern.h 8.3 (Berkeley) 4/16/94 + */ + +#include + +void brace_subst(char *, char **, char *, size_t); +PLAN *find_create(char ***); +int find_execute(PLAN *, char **); +PLAN *find_formplan(char **); +PLAN *not_squish(PLAN *); +PLAN *or_squish(PLAN *); +PLAN *paren_squish(PLAN *); +time_t get_date(char *); +struct stat; +void printlong(char *, char *, struct stat *); +int queryuser(char **); +OPTION *lookup_option(const char *); +void finish_execplus(void); + +creat_f c_Xmin; +creat_f c_Xtime; +creat_f c_acl; +creat_f c_and; +creat_f c_delete; +creat_f c_depth; +creat_f c_daystart; +creat_f c_empty; +creat_f c_exec; +creat_f c_flags; +creat_f c_follow; +creat_f c_fstype; +creat_f c_group; +creat_f c_ignore_readdir_race; +creat_f c_inum; +creat_f c_links; +creat_f c_ls; +creat_f c_mXXdepth; +creat_f c_name; +creat_f c_newer; +creat_f c_nogroup; +creat_f c_nouser; +creat_f c_perm; +creat_f c_print; +creat_f c_printf; +creat_f c_regex; +creat_f c_samefile; +creat_f c_simple; +creat_f c_size; +creat_f c_sparse; +creat_f c_type; +creat_f c_user; +creat_f c_xdev; + +exec_f f_Xmin; +exec_f f_Xtime; +exec_f f_acl; +exec_f f_always_true; +exec_f f_closeparen; +exec_f f_delete; +exec_f f_depth; +exec_f f_empty; +exec_f f_exec; +exec_f f_executable; +exec_f f_expr; +exec_f f_false; +exec_f f_flags; +exec_f f_fstype; +exec_f f_group; +exec_f f_inum; +exec_f f_links; +exec_f f_ls; +exec_f f_name; +exec_f f_newer; +exec_f f_nogroup; +exec_f f_not; +exec_f f_nouser; +exec_f f_openparen; +exec_f f_or; +exec_f f_path; +exec_f f_perm; +exec_f f_print; +exec_f f_print0; +exec_f f_printf; +exec_f f_prune; +exec_f f_quit; +exec_f f_readable; +exec_f f_regex; +exec_f f_size; +exec_f f_sparse; +exec_f f_type; +exec_f f_user; +exec_f f_writable; + +extern int ftsoptions, ignore_readdir_race, isdepth, isoutput; +extern int issort, isxargs; +extern int daystart; +extern int mindepth, maxdepth; +extern int regexp_flags; +extern int exitstatus; +extern time_t now; +extern int dotfd; +extern FTS *tree; +extern volatile sig_atomic_t showinfo; diff --git a/src/find/find.1 b/src/find/find.1 new file mode 100644 index 0000000..6da6e5a --- /dev/null +++ b/src/find/find.1 @@ -0,0 +1,1206 @@ +.\" Copyright (c) 1990, 1993 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" This code is derived from software contributed to Berkeley by +.\" the Institute of Electrical and Electronics Engineers, Inc. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. +.\" +.\" @(#)find.1 8.7 (Berkeley) 5/9/95 +.\" +.Dd November 23, 2024 +.Dt FIND 1 +.Os +.Sh NAME +.Nm find +.Nd walk a file hierarchy +.Sh SYNOPSIS +.Nm +.Op Fl H | Fl L | Fl P +.Op Fl EXdsx +.Op Fl f Ar path +.Ar path ... +.Op Ar expression +.Nm +.Op Fl H | Fl L | Fl P +.Op Fl EXdsx +.Fl f Ar path +.Op Ar path ... +.Op Ar expression +.Sh DESCRIPTION +The +.Nm +utility recursively descends the directory tree for each +.Ar path +listed, evaluating an +.Ar expression +(composed of the +.Dq primaries +and +.Dq operands +listed below) in terms +of each file in the tree. +.Pp +The options are as follows: +.Bl -tag -width indent +.It Fl E +Interpret regular expressions following the +.Ic -regex +and +.Ic -iregex +primaries as extended (modern) regular expressions rather than basic +regular expressions (BRE's). +The +.Xr re_format 7 +manual page fully describes both formats. +.It Fl H +Cause the file information and file type (see +.Xr stat 2 ) +returned for each symbolic link specified on the command line to be +those of the file referenced by the link, not the link itself. +If the referenced file does not exist, the file information and type will +be for the link itself. +File information of all symbolic links not on +the command line is that of the link itself. +.It Fl L +Cause the file information and file type (see +.Xr stat 2 ) +returned for each symbolic link to be those of the file referenced by the +link, not the link itself. +If the referenced file does not exist, the file information and type will +be for the link itself. +.Pp +This option is equivalent to the deprecated +.Ic -follow +primary. +.It Fl P +Cause the file information and file type (see +.Xr stat 2 ) +returned for each symbolic link to be those of the link itself. +This is the default. +.It Fl X +Permit +.Nm +to be safely used in conjunction with +.Xr xargs 1 . +If a file name contains any of the delimiting characters used by +.Xr xargs 1 , +a diagnostic message is displayed on standard error, and the file +is skipped. +The delimiting characters include single +.Pq Dq Li " ' " +and double +.Pq Dq Li " \*q " +quotes, backslash +.Pq Dq Li \e , +space, tab and newline characters. +.Pp +However, you may wish to consider the +.Fl print0 +primary in conjunction with +.Dq Nm xargs Fl 0 +as an effective alternative. +.It Fl d +Cause +.Nm +to perform a depth-first traversal. +.Pp +This option is a BSD-specific equivalent of the +.Ic -depth +primary specified by +.St -p1003.1-2001 . +Refer to its description under +.Sx PRIMARIES +for more information. +.It Fl f Ar path +Add +.Ar path +to the list of paths that will be recursed into. +This is useful when +.Ar path +begins with a character that would otherwise be interpreted as an +.Ar expression , +namely +.Dq Li "!" , +.Dq Li "(" +and +.Dq Li - . +.It Fl s +Cause +.Nm +to traverse the file hierarchies in lexicographical order, +i.e., alphabetical order within each directory. +Note: +.Ql find -s +and +.Ql "find | sort" +may give different results. +.Pp +For example, +.Ql find -s +puts a directory +.Ql Ar foo +with all its contents before a directory +.Ql Ar foo . +but +.Ql "find | sort" +puts the directory name +.Ql Ar foo . +before any string like +.Ql Ar foo/bar +because +.Ql .\& +goes before +.Ql / +in ASCII. +In locales other than +.Ar C +results may vary more due to collation differences. +.It Fl x +Prevent +.Nm +from descending into directories that have a device number different +than that of the file from which the descent began. +.Pp +This option is equivalent to the deprecated +.Ic -xdev +primary. +.El +.Sh PRIMARIES +All primaries which take a numeric argument allow the number to be +preceded by a plus sign +.Pq Dq Li + +or a minus sign +.Pq Dq Li - . +A preceding plus sign means +.Dq more than n , +a preceding minus sign means +.Dq less than n +and neither means +.Dq exactly n . +.Bl -tag -width indent +.It Ic -Bmin Ar n +True if the difference between the time of a file's inode creation +and the time +.Nm +was started, rounded up to the next full minute, is +.Ar n +minutes. +.It Ic -Bnewer Ar file +Same as +.Ic -newerBm . +.It Ic -Btime Ar n Ns Op Cm smhdw +If no units are specified, this primary evaluates to +true if the difference between the time of a file's inode creation +and the time +.Nm +was started, rounded up to the next full 24-hour period, is +.Ar n +24-hour periods. +.Pp +If units are specified, this primary evaluates to +true if the difference between the time of a file's inode creation +and the time +.Nm +was started is exactly +.Ar n +units. +Please refer to the +.Ic -atime +primary description for information on supported time units. +.It Ic -acl +May be used in conjunction with other primaries to locate +files with extended ACLs. +See +.Xr acl 3 +for more information. +.It Ic -amin Oo Cm - Ns | Ns Cm + Oc Ns Ar n +True if the difference between the file last access time and the time +.Nm +was started, rounded up to the next full minute, is +more than +.Ar n +.Pq + Ns Ar n , +less than +.Ar n +.Pq - Ns Ar n , +or exactly +.Ar n +minutes ago. +.It Ic -anewer Ar file +Same as +.Ic -neweram . +.It Ic -atime Ar n Ns Op Cm smhdw +If no units are specified, this primary evaluates to +true if the difference between the file last access time and the time +.Nm +was started, rounded up to the next full 24-hour period, is +.Ar n +24-hour periods. +.Pp +If units are specified, this primary evaluates to +true if the difference between the file last access time and the time +.Nm +was started is exactly +.Ar n +units. +Possible time units are as follows: +.Pp +.Bl -tag -width indent -compact +.It Cm s +second +.It Cm m +minute (60 seconds) +.It Cm h +hour (60 minutes) +.It Cm d +day (24 hours) +.It Cm w +week (7 days) +.El +.Pp +Any number of units may be combined in one +.Ic -atime +argument, for example, +.Dq Li "-atime -1h30m" . +Units are probably only useful when used in conjunction with the +.Cm + +or +.Cm - +modifier. +.It Ic -cmin Oo Cm - Ns | Ns Cm + Oc Ns Ar n +True if the difference between the time of last change of file status +information and the time +.Nm +was started, rounded up to the next full minute, is +more than +.Ar n +.Pq + Ns Ar n , +less than +.Ar n +.Pq - Ns Ar n , +or exactly +.Ar n +minutes ago. +.It Ic -cnewer Ar file +Same as +.Ic -newercm . +.It Ic -ctime Ar n Ns Op Cm smhdw +If no units are specified, this primary evaluates to +true if the difference between the time of last change of file status +information and the time +.Nm +was started, rounded up to the next full 24-hour period, is +.Ar n +24-hour periods. +.Pp +If units are specified, this primary evaluates to +true if the difference between the time of last change of file status +information and the time +.Nm +was started is exactly +.Ar n +units. +Please refer to the +.Ic -atime +primary description for information on supported time units. +.It Ic -d +Non-portable, BSD-specific version of +.Ic depth . +GNU find implements this as a primary in mistaken emulation of +.Fx +.Nm . +.It Ic -delete +Delete found files and/or directories. +Always returns true. +This executes +from the current working directory as +.Nm +recurses down the tree. +It will not attempt to delete a filename with a +.Dq Pa / +character in its pathname relative to +.Dq Pa \&. +for security reasons. +Depth-first traversal processing is implied by this option. +The +.Ic -delete +primary will fail to delete a directory if it is not empty. +Following symlinks is incompatible with this option. +.Pp +.Em WARNING : +The +.Ic -delete +primary will immediately attempt to delete the current path when +evaluated. +Be mindful of its place in the expression; as a general rule, it +should almost always come last. +If in doubt, try running with +.Ic -print +in place of +.Ic -delete +first. +See +.Sx OPERATORS +below for additional information on the order of evaluation. +.It Ic -depth +Always true; +same as the non-portable +.Fl d +option. +Cause +.Nm +to perform a depth-first traversal, i.e., directories +are visited in post-order and all entries in a directory will be acted +on before the directory itself. +By default, +.Nm +visits directories in pre-order, i.e., before their contents. +Note, the default is +.Em not +a breadth-first traversal. +.Pp +The +.Ic -depth +primary +can be useful when +.Nm +is used with +.Xr cpio 1 +to process files that are contained in directories with unusual permissions. +It ensures that you have write permission while you are placing files in a +directory, then sets the directory's permissions as the last thing. +.It Ic -depth Ar n +True if the depth of the file relative to the starting point of the traversal +is +.Ar n . +.It Ic -empty +True if the current file or directory is empty. +.It Ic -exec Ar utility Oo Ar argument ... Oc Li \&; +True if the program named +.Ar utility +returns a zero value as its exit status. +Optional +.Ar arguments +may be passed to the utility. +The expression must be terminated by a semicolon +.Pq Dq Li \&; . +If you invoke +.Nm +from a shell you may need to quote the semicolon if the shell would +otherwise treat it as a control operator. +If the string +.Dq Li {} +appears anywhere in the utility name or the +arguments it is replaced by the pathname of the current file. +.Ar Utility +will be executed from the directory from which +.Nm +was executed. +.Ar Utility +and +.Ar arguments +are not subject to the further expansion of shell patterns +and constructs. +.It Ic -exec Ar utility Oo Ar argument ... Oc Li {} + +Same as +.Ic -exec , +except that +.Dq Li {} +is replaced with as many pathnames as possible for each invocation of +.Ar utility . +This behaviour is similar to that of +.Xr xargs 1 . +The primary always returns true; +if at least one invocation of +.Ar utility +returns a non-zero exit status, +.Nm +will return a non-zero exit status. +.It Ic -execdir Ar utility Oo Ar argument ... Oc Li \&; +The +.Ic -execdir +primary is identical to the +.Ic -exec +primary with the exception that +.Ar utility +will be executed from the directory that holds +the current file. +The filename substituted for +the string +.Dq Li {} +is not qualified. +.It Ic -execdir Ar utility Oo Ar argument ... Oc Li {} + +Same as +.Ic -execdir , +except that +.Dq Li {} +is replaced with as many pathnames as possible for each invocation of +.Ar utility . +This behaviour is similar to that of +.Xr xargs 1 . +The primary always returns true; +if at least one invocation of +.Ar utility +returns a non-zero exit status, +.Nm +will return a non-zero exit status. +.It Ic -executable +Matches files which are executable by the current user. +This test makes use of the +.Xr access 2 +system call, and so can be fooled by NFS servers which do UID mapping (or root-squashing). +This is a GNU find extension. +.It Ic -flags Oo Cm - Ns | Ns Cm + Oc Ns Ar flags , Ns Ar notflags +The flags are specified using symbolic names (see +.Xr chflags 1 ) . +Those with the +.Qq Li no +prefix (except +.Qq Li nodump ) +are said to be +.Ar notflags . +Flags in +.Ar flags +are checked to be set, and flags in +.Ar notflags +are checked to be not set. +Note that this is different from +.Ic -perm , +which only allows the user to specify mode bits that are set. +.Pp +If flags are preceded by a dash +.Pq Dq Li - , +this primary evaluates to true +if at least all of the bits in +.Ar flags +and none of the bits in +.Ar notflags +are set in the file's flags bits. +If flags are preceded by a plus +.Pq Dq Li + , +this primary evaluates to true +if any of the bits in +.Ar flags +is set in the file's flags bits, +or any of the bits in +.Ar notflags +is not set in the file's flags bits. +Otherwise, +this primary evaluates to true +if the bits in +.Ar flags +exactly match the file's flags bits, +and none of the +.Ar flags +bits match those of +.Ar notflags . +.It Ic -fstype Ar type +True if the file is contained in a file system of type +.Ar type . +The +.Xr lsvfs 1 +command can be used to find out the types of file systems +that are available on the system. +In addition, there are two pseudo-types, +.Dq Li local +and +.Dq Li rdonly . +The former matches any file system physically mounted on the system where +the +.Nm +is being executed and the latter matches any file system which is +mounted read-only. +.It Ic -gid Ar gname +The same thing as +.Ic -group Ar gname +for compatibility with GNU find. +GNU find imposes a restriction that +.Ar gname +is numeric, while +.Nm +does not. +.It Ic -group Ar gname +True if the file belongs to the group +.Ar gname . +If +.Ar gname +is numeric and there is no such group name, then +.Ar gname +is treated as a group ID. +.It Ic -ignore_readdir_race +Ignore errors because a file or a directory is deleted +after reading the name from a directory. +This option does not affect errors occurring on starting points. +.It Ic -ilname Ar pattern +Like +.Ic -lname , +but the match is case insensitive. +This is a GNU find extension. +.It Ic -iname Ar pattern +Like +.Ic -name , +but the match is case insensitive. +.It Ic -inum Ar n +True if the file has inode number +.Ar n . +.It Ic -ipath Ar pattern +Like +.Ic -path , +but the match is case insensitive. +.It Ic -iregex Ar pattern +Like +.Ic -regex , +but the match is case insensitive. +.It Ic -iwholename Ar pattern +The same thing as +.Ic -ipath , +for GNU find compatibility. +.It Ic -links Ar n +True if the file has +.Ar n +links. +.It Ic -lname Ar pattern +Like +.Ic -name , +but the contents of the symbolic link are matched instead of the file +name. +Note that this only matches broken symbolic links +if symbolic links are being followed. +This is a GNU find extension. +.It Ic -ls +This primary always evaluates to true. +The following information for the current file is written to standard output: +its inode number, size in 512-byte blocks, file permissions, number of hard +links, owner, group, size in bytes, last modification time, and pathname. +If the file is a block or character special file, the device number +will be displayed instead of the size in bytes. +If the file is a symbolic link, the pathname of the linked-to file will be +displayed preceded by +.Dq Li -> . +The format is identical to that produced by +.Bk -words +.Dq Nm ls Fl dils . +.Ek +.It Ic -maxdepth Ar n +Always true; descend at most +.Ar n +directory levels below the command line arguments. +If any +.Ic -maxdepth +primary is specified, it applies to the entire expression even if it would +not normally be evaluated. +.Dq Ic -maxdepth Li 0 +limits the whole search to the command line arguments. +.It Ic -mindepth Ar n +Always true; do not apply any tests or actions at levels less than +.Ar n . +If any +.Ic -mindepth +primary is specified, it applies to the entire expression even if it would +not normally be evaluated. +.Dq Ic -mindepth Li 1 +processes all but the command line arguments. +.It Ic -mmin Oo Cm - Ns | Ns Cm + Oc Ns Ar n +True if the difference between the file last modification time and the time +.Nm +was started, rounded up to the next full minute, is +more than +.Ar n +.Pq + Ns Ar n , +less than +.Ar n +.Pq - Ns Ar n , +or exactly +.Ar n +minutes ago. +.It Ic -mnewer Ar file +Same as +.Ic -newer . +.It Ic -mount +The same thing as +.Ic -xdev , +for GNU find compatibility. +.It Ic -mtime Ar n Ns Op Cm smhdw +If no units are specified, this primary evaluates to +true if the difference between the file last modification time and the time +.Nm +was started, rounded up to the next full 24-hour period, is +.Ar n +24-hour periods. +.Pp +If units are specified, this primary evaluates to +true if the difference between the file last modification time and the time +.Nm +was started is exactly +.Ar n +units. +Please refer to the +.Ic -atime +primary description for information on supported time units. +.It Ic -name Ar pattern +True if the last component of the pathname being examined matches +.Ar pattern . +Special shell pattern matching characters +.Dq ( Li \&[ , +.Dq Li \&] , +.Dq Li * , +and +.Dq Li \&? ) +may be used as part of +.Ar pattern . +These characters may be matched explicitly by escaping them with a +backslash +.Pq Dq Li \e . +.It Ic -newer Ar file +True if the current file has a more recent last modification time than +.Ar file . +.It Ic -newer Ns Ar X Ns Ar Y Ar file +True if the current file has a more recent last access time +.Pq Ar X Ns = Ns Cm a , +inode creation time +.Pq Ar X Ns = Ns Cm B , +change time +.Pq Ar X Ns = Ns Cm c , +or modification time +.Pq Ar X Ns = Ns Cm m +than the last access time +.Pq Ar Y Ns = Ns Cm a , +inode creation time +.Pq Ar Y Ns = Ns Cm B , +change time +.Pq Ar Y Ns = Ns Cm c , +or modification time +.Pq Ar Y Ns = Ns Cm m +of +.Ar file . +In addition, if +.Ar Y Ns = Ns Cm t , +then +.Ar file +is instead interpreted as a direct date specification of the form +understood by ISO8601 or RFC822. +Note that +.Ic -newermm +is equivalent to +.Ic -newer . +.It Ic -nogroup +True if the file belongs to an unknown group. +.It Ic -noignore_readdir_race +Turn off the effect of +.Ic -ignore_readdir_race . +This is default behaviour. +.It Ic -noleaf +This option is for GNU find compatibility. +In GNU find it disables an optimization not relevant to +.Nm , +so it is ignored. +.It Ic -nouser +True if the file belongs to an unknown user. +.It Ic -ok Ar utility Oo Ar argument ... Oc Li \&; +The +.Ic -ok +primary is identical to the +.Ic -exec +primary with the exception that +.Nm +requests user affirmation for the execution of the +.Ar utility +by printing +a message to the terminal and reading a response. +If the response is not affirmative +.Ql ( y +in the +.Dq Li POSIX +locale), +the command is not executed and the +value of the +.Ic -ok +expression is false. +.It Ic -okdir Ar utility Oo Ar argument ... Oc Li \&; +The +.Ic -okdir +primary is identical to the +.Ic -execdir +primary with the same exception as described for the +.Ic -ok +primary. +.It Ic -path Ar pattern +True if the pathname being examined matches +.Ar pattern . +Special shell pattern matching characters +.Dq ( Li \&[ , +.Dq Li \&] , +.Dq Li * , +and +.Dq Li \&? ) +may be used as part of +.Ar pattern . +These characters may be matched explicitly by escaping them with a +backslash +.Pq Dq Li \e . +Slashes +.Pq Dq Li / +are treated as normal characters and do not have to be +matched explicitly. +.It Ic -perm Oo Cm - Ns | Ns Cm + Ns | Ns Cm / Oc Ns Ar mode +The +.Ar mode +may be either symbolic (see +.Xr chmod 1 ) +or an octal number. +If the +.Ar mode +is symbolic, a starting value of zero is assumed and the +.Ar mode +sets or clears permissions without regard to the process' file mode +creation mask. +If the +.Ar mode +is octal, only bits 07777 +.Pq Dv S_ISUID | S_ISGID | S_ISTXT | S_IRWXU | S_IRWXG | S_IRWXO +of the file's mode bits participate +in the comparison. +If the +.Ar mode +is preceded by a dash +.Pq Dq Li - , +this primary evaluates to true +if at least all of the bits in the +.Ar mode +are set in the file's mode bits. +If the +.Ar mode +is preceded by a plus +.Pq Dq Li + +this primary evaluates to true +if any of the bits in the +.Ar mode +are set in the file's mode bits. +A slash +.Pq Dq Li / +is also accepted with the same meaning as plus for compatibility with GNU find. +Otherwise, this primary evaluates to true if +the bits in the +.Ar mode +exactly match the file's mode bits. +Note, the first character of a symbolic mode may not be a dash +.Pq Dq Li - . +.It Ic -print +This primary always evaluates to true. +It prints the pathname of the current file to standard output. +If none of +.Ic -exec , -ls , -print0 , +or +.Ic -ok +is specified, the given expression shall be effectively replaced by +.Cm \&( Ar "given expression" Cm \&) Ic -print . +.It Ic -print0 +This primary always evaluates to true. +It prints the pathname of the current file to standard output, followed by an +ASCII +.Dv NUL +character (character code 0). +.It Ic -prune +This primary always evaluates to true. +It causes +.Nm +to not descend into the current file. +Note, the +.Ic -prune +primary has no effect if the +.Fl d +option was specified. +.It Ic -quit +Causes +.Nm +to terminate immediately. +.It Ic -readable +Matches files which are readable by the current user. +This test makes use of the +.Xr access 2 +system call, and so can be fooled by NFS servers which do UID mapping (or root-squashing). +This is a GNU find extension. +.It Ic -regex Ar pattern +True if the whole path of the file matches +.Ar pattern +using regular expression. +To match a file named +.Dq Pa ./foo/xyzzy , +you can use the regular expression +.Dq Li ".*/[xyz]*" +or +.Dq Li ".*/foo/.*" , +but not +.Dq Li xyzzy +or +.Dq Li /foo/ . +.It Ic -samefile Ar name +True if the file is a hard link to +.Ar name . +If the command option +.Ic -L +is specified, it is also true if the file is a symbolic link and +points to +.Ar name . +.It Ic -size Ar n Ns Op Cm ckMGTP +True if the file's size, rounded up, in 512-byte blocks is +.Ar n . +If +.Ar n +is followed by a +.Cm c , +then the primary is true if the +file's size is +.Ar n +bytes (characters). +Similarly if +.Ar n +is followed by a scale indicator then the file's size is compared to +.Ar n +scaled as: +.Pp +.Bl -tag -width indent -compact +.It Cm k +kilobytes (1024 bytes) +.It Cm M +megabytes (1024 kilobytes) +.It Cm G +gigabytes (1024 megabytes) +.It Cm T +terabytes (1024 gigabytes) +.It Cm P +petabytes (1024 terabytes) +.El +.It Ic -sparse +True if the current file is sparse, +i.e. has fewer blocks allocated than expected based on its size in bytes. +This might also match files that have been compressed by the filesystem. +.It Ic -type Ar t +True if the file is of the specified type. +Possible file types are as follows: +.Pp +.Bl -tag -width indent -compact +.It Cm b +block special +.It Cm c +character special +.It Cm d +directory +.It Cm f +regular file +.It Cm l +symbolic link +.It Cm p +FIFO +.It Cm s +socket +.El +.It Ic -uid Ar uname +The same thing as +.Ar -user Ar uname +for compatibility with GNU find. +GNU find imposes a restriction that +.Ar uname +is numeric, while +.Nm +does not. +.It Ic -user Ar uname +True if the file belongs to the user +.Ar uname . +If +.Ar uname +is numeric and there is no such user name, then +.Ar uname +is treated as a user ID. +.It Ic -wholename Ar pattern +The same thing as +.Ic -path , +for GNU find compatibility. +.It Ic -writable +Matches files which are writable by the current user. +This test makes use of the +.Xr access 2 +system call, and so can be fooled by NFS servers which do UID mapping (or root-squashing). +This is a GNU find extension. +.El +.Sh OPERATORS +The primaries may be combined using the following operators. +The operators are listed in order of decreasing precedence. +.Pp +.Bl -tag -width indent -compact +.It Cm \&( Ar expression Cm \&) +This evaluates to true if the parenthesized expression evaluates to +true. +.Pp +.It Cm \&! Ar expression +.It Cm -not Ar expression +This is the unary +.Tn NOT +operator. +It evaluates to true if the expression is false. +.Pp +.It Cm -false +Always false. +.It Cm -true +Always true. +.Pp +.It Ar expression Cm -and Ar expression +.It Ar expression expression +The +.Cm -and +operator is the logical +.Tn AND +operator. +As it is implied by the juxtaposition of two expressions it does not +have to be specified. +The expression evaluates to true if both expressions are true. +The second expression is not evaluated if the first expression is false. +.Pp +.It Ar expression Cm -or Ar expression +The +.Cm -or +operator is the logical +.Tn OR +operator. +The expression evaluates to true if either the first or the second expression +is true. +The second expression is not evaluated if the first expression is true. +.El +.Pp +All operands and primaries must be separate arguments to +.Nm . +Primaries which themselves take arguments expect each argument +to be a separate argument to +.Nm . +.Sh ENVIRONMENT +The +.Ev LANG , LC_ALL , LC_COLLATE , LC_CTYPE , LC_MESSAGES +and +.Ev LC_TIME +environment variables affect the execution of the +.Nm +utility as described in +.Xr environ 7 . +.Sh EXAMPLES +The following examples are shown as given to the shell: +.Bl -tag -width indent +.It Li "find / \e! -name \*q*.c\*q -print" +Print out a list of all the files whose names do not end in +.Pa .c . +.It Li "find / -newer ttt -user wnj -print" +Print out a list of all the files owned by user +.Dq wnj +that are newer +than the file +.Pa ttt . +.It Li "find / \e! \e( -newer ttt -user wnj \e) -print" +Print out a list of all the files which are not both newer than +.Pa ttt +and owned by +.Dq wnj . +.It Li "find / \e( -newer ttt -or -user wnj \e) -print" +Print out a list of all the files that are either owned by +.Dq wnj +or that are newer than +.Pa ttt . +.It Li "find / -newerct '1 minute ago' -print" +Print out a list of all the files whose inode change time is more +recent than the current time minus one minute. +.It Li "find / -type f -exec echo {} \e;" +Use the +.Xr echo 1 +command to print out a list of all the files. +.It Li "find -L /usr/ports/packages -type l -exec rm -- {} +" +Delete all broken symbolic links in +.Pa /usr/ports/packages . +.It Li "find /usr/src -name CVS -prune -o -depth +6 -print" +Find files and directories that are at least seven levels deep +in the working directory +.Pa /usr/src . +.It Li "find /usr/src -name CVS -prune -o -mindepth 7 -print" +Is not equivalent to the previous example, since +.Ic -prune +is not evaluated below level seven. +.El +.Sh COMPATIBILITY +The +.Ic -follow +primary is deprecated; the +.Fl L +option should be used instead. +See the +.Sx STANDARDS +section below for details. +.Sh SEE ALSO +.Xr chflags 1 , +.Xr chmod 1 , +.Xr locate 1 , +.Xr lsvfs 1 , +.Xr whereis 1 , +.Xr which 1 , +.Xr xargs 1 , +.Xr stat 2 , +.Xr acl 3 , +.Xr fts 3 , +.Xr getgrent 3 , +.Xr getpwent 3 , +.Xr strmode 3 , +.Xr ascii 7 , +.Xr re_format 7 , +.Xr symlink 7 +.Sh STANDARDS +The +.Nm +utility syntax is a superset of the syntax specified by the +.St -p1003.1-2001 +standard. +.Pp +All the single character options except +.Fl H +and +.Fl L +as well as +.Ic -amin , -anewer , -cmin , -cnewer , -delete , -empty , -fstype , +.Ic -iname , -inum , -iregex , -ls , -maxdepth , -mindepth , -mmin , +.Ic -not , -path , -print0 , -regex , -sparse +and all of the +.Fl B* +birthtime related primaries are extensions to +.St -p1003.1-2001 . +.Pp +Historically, the +.Fl d , L +and +.Fl x +options were implemented using the primaries +.Ic -depth , -follow , +and +.Ic -xdev . +These primaries always evaluated to true. +As they were really global variables that took effect before the traversal +began, some legal expressions could have unexpected results. +An example is the expression +.Ic -print Cm -o Ic -depth . +As +.Ic -print +always evaluates to true, the standard order of evaluation +implies that +.Ic -depth +would never be evaluated. +This is not the case. +.Pp +The operator +.Cm -or +was implemented as +.Cm -o , +and the operator +.Cm -and +was implemented as +.Cm -a . +.Pp +Historic implementations of the +.Ic -exec +and +.Ic -ok +primaries did not replace the string +.Dq Li {} +in the utility name or the +utility arguments if it had preceding or following non-whitespace characters. +This version replaces it no matter where in the utility name or arguments +it appears. +.Pp +The +.Fl E +option was inspired by the equivalent +.Xr grep 1 +and +.Xr sed 1 +options. +.Pp +The +.Ic -perm +primary accepts a leading slash +.Pq Dq Li / +as an alias for a leading plus +.Pq Dq Li + +for its argument as an extension of +.St -p1003.1-2001 +to be compatible with GNU find. +.Sh HISTORY +A simple +.Nm +command appeared in +.At v1 +and was removed in +.At v3 . +It was rewritten for +.At v5 +and was later enhanced for the Programmer's Workbench (PWB). +These changes were later incorporated in +.At v7 . +.Sh BUGS +The special characters used by +.Nm +are also special characters to many shell programs. +In particular, the characters +.Dq Li * , +.Dq Li \&[ , +.Dq Li \&] , +.Dq Li \&? , +.Dq Li \&( , +.Dq Li \&) , +.Dq Li \&! , +.Dq Li \e +and +.Dq Li \&; +may have to be escaped from the shell. +.Pp +As there is no delimiter separating options and file names or file +names and the +.Ar expression , +it is difficult to specify files named +.Pa -xdev +or +.Pa \&! . +These problems are handled by the +.Fl f +option and the +.Xr getopt 3 +.Dq Fl Fl +construct. +.Pp +The +.Ic -delete +primary does not interact well with other options that cause the file system +tree traversal options to be changed. +.Pp +The +.Ic -mindepth +and +.Ic -maxdepth +primaries are actually global options (as documented above). +They should +probably be replaced by options which look like options. diff --git a/src/find/find.c b/src/find/find.c new file mode 100644 index 0000000..d7e0f96 --- /dev/null +++ b/src/find/find.c @@ -0,0 +1,243 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1991, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Cimarron D. Taylor of the University of California, Berkeley. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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 +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "find.h" + +static int find_compare(const FTSENT **s1, const FTSENT **s2); + +/* + * find_compare -- + * tell fts_open() how to order the traversal of the hierarchy. + * This variant gives lexicographical order, i.e., alphabetical + * order within each directory. + */ +static int +find_compare(const FTSENT **s1, const FTSENT **s2) +{ + + return (strcoll((*s1)->fts_name, (*s2)->fts_name)); +} + +/* + * find_formplan -- + * process the command line and create a "plan" corresponding to the + * command arguments. + */ +PLAN * +find_formplan(char *argv[]) +{ + PLAN *plan, *tail, *new; + + /* + * for each argument in the command line, determine what kind of node + * it is, create the appropriate node type and add the new plan node + * to the end of the existing plan. The resulting plan is a linked + * list of plan nodes. For example, the string: + * + * % find . -name foo -newer bar -print + * + * results in the plan: + * + * [-name foo]--> [-newer bar]--> [-print] + * + * in this diagram, `[-name foo]' represents the plan node generated + * by c_name() with an argument of foo and `-->' represents the + * plan->next pointer. + */ + for (plan = tail = NULL; *argv;) { + if (!(new = find_create(&argv))) + continue; + if (plan == NULL) + tail = plan = new; + else { + tail->next = new; + tail = new; + } + } + + /* + * if the user didn't specify one of -print, -ok or -exec, then -print + * is assumed so we bracket the current expression with parens, if + * necessary, and add a -print node on the end. + */ + if (!isoutput) { + OPTION *p; + char **argv1 = 0; + + if (plan == NULL) { + p = lookup_option("-print"); + new = (p->create)(p, &argv1); + tail = plan = new; + } else { + p = lookup_option("("); + new = (p->create)(p, &argv1); + new->next = plan; + plan = new; + p = lookup_option(")"); + new = (p->create)(p, &argv1); + tail->next = new; + tail = new; + p = lookup_option("-print"); + new = (p->create)(p, &argv1); + tail->next = new; + tail = new; + } + } + + /* + * the command line has been completely processed into a search plan + * except for the (, ), !, and -o operators. Rearrange the plan so + * that the portions of the plan which are affected by the operators + * are moved into operator nodes themselves. For example: + * + * [!]--> [-name foo]--> [-print] + * + * becomes + * + * [! [-name foo] ]--> [-print] + * + * and + * + * [(]--> [-depth]--> [-name foo]--> [)]--> [-print] + * + * becomes + * + * [expr [-depth]-->[-name foo] ]--> [-print] + * + * operators are handled in order of precedence. + */ + + plan = paren_squish(plan); /* ()'s */ + plan = not_squish(plan); /* !'s */ + plan = or_squish(plan); /* -o's */ + return (plan); +} + +FTS *tree; /* pointer to top of FTS hierarchy */ + +/* + * find_execute -- + * take a search plan and an array of search paths and executes the plan + * over all FTSENT's returned for the given search paths. + */ +int +find_execute(PLAN *plan, char *paths[]) +{ + FTSENT *entry; + PLAN *p; + size_t counter = 0; + int e; + + tree = fts_open(paths, ftsoptions, (issort ? find_compare : NULL)); + if (tree == NULL) + err(1, "ftsopen"); + + exitstatus = 0; + while (errno = 0, (entry = fts_read(tree)) != NULL) { + if (maxdepth != -1 && entry->fts_level >= maxdepth) { + if (fts_set(tree, entry, FTS_SKIP)) + err(1, "%s", entry->fts_path); + } + + switch (entry->fts_info) { + case FTS_D: + if (isdepth) + continue; + break; + case FTS_DP: + if (!isdepth) + continue; + break; + case FTS_DNR: + case FTS_NS: + if (ignore_readdir_race && + entry->fts_errno == ENOENT && entry->fts_level > 0) + continue; + /* FALLTHROUGH */ + case FTS_ERR: + (void)fflush(stdout); + warnx("%s: %s", + entry->fts_path, strerror(entry->fts_errno)); + exitstatus = 1; + continue; +#if defined(FTS_W) && defined(FTS_WHITEOUT) + case FTS_W: + if (ftsoptions & FTS_WHITEOUT) + break; + continue; +#endif /* FTS_W */ + } + + if (showinfo) { + fprintf(stderr, "Scanning: %s\n", entry->fts_path); + fprintf(stderr, "Scanned: %zu\n\n", counter); + showinfo = 0; + } + ++counter; + +#define BADCH " \t\n\\'\"" + if (isxargs && strpbrk(entry->fts_path, BADCH)) { + (void)fflush(stdout); + warnx("%s: illegal path", entry->fts_path); + exitstatus = 1; + continue; + } + + if (mindepth != -1 && entry->fts_level < mindepth) + continue; + + /* + * Call all the functions in the execution plan until one is + * false or all have been executed. This is where we do all + * the work specified by the user on the command line. + */ + for (p = plan; p && (p->execute)(p, entry); p = p->next); + } + e = errno; + finish_execplus(); + if (e && (!ignore_readdir_race || e != ENOENT)) + errc(1, e, "fts_read"); + return (exitstatus); +} diff --git a/src/find/find.h b/src/find/find.h new file mode 100644 index 0000000..9767304 --- /dev/null +++ b/src/find/find.h @@ -0,0 +1,179 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Cimarron D. Taylor of the University of California, Berkeley. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + * + * @(#)find.h 8.1 (Berkeley) 6/6/93 + */ + +#include +#include + +#ifdef __linux__ +#include +#include +#define HAVE_STRUCT_STAT_ST_FLAGS 0 +#define HAVE_STRUCT_STAT_ST_BIRTHTIME 1 /* via statx() */ +#define HAVE_STRUCT_STATFS_F_FSTYPENAME 0 +#define HAVE_LINUX_STATX 1 +#else +#include +#ifdef UF_SETTABLE +#define HAVE_STRUCT_STAT_ST_FLAGS 1 +#else +#define HAVE_STRUCT_STAT_ST_FLAGS 0 +#endif +#if defined(st_birthtime) || defined(st_birthtimespec) +#define HAVE_STRUCT_STAT_ST_BIRTHTIME 1 +#else +#define HAVE_STRUCT_STAT_ST_BIRTHTIME 0 +#endif +#if defined(MFSNAMELEN) || defined(MFSTYPENAMELEN) +#define HAVE_STRUCT_STATFS_F_FSTYPENAME 1 +#else +#define HAVE_STRUCT_STATFS_F_FSTYPENAME 0 +#endif +#endif /* __linux__ */ + +#ifndef HAVE_LINUX_STATX +#define HAVE_LINUX_STATX 0 +#endif + +/* forward declarations */ +struct _plandata; +struct _option; + +/* execute function */ +typedef int exec_f(struct _plandata *, FTSENT *); +/* create function */ +typedef struct _plandata *creat_f(struct _option *, char ***); + +/* function modifiers */ +#define F_NEEDOK 0x00000001 /* -ok vs. -exec */ +#define F_EXECDIR 0x00000002 /* -execdir vs. -exec */ +#define F_TIME_A 0x00000004 /* one of -atime, -anewer, -newera* */ +#define F_TIME_C 0x00000008 /* one of -ctime, -cnewer, -newerc* */ +#define F_TIME2_A 0x00000010 /* one of -newer?a */ +#define F_TIME2_C 0x00000020 /* one of -newer?c */ +#define F_TIME2_T 0x00000040 /* one of -newer?t */ +#define F_MAXDEPTH F_TIME_A /* maxdepth vs. mindepth */ +#define F_DEPTH F_TIME_A /* -depth n vs. -d */ +/* command line function modifiers */ +#define F_EQUAL 0x00000000 /* [acm]min [acm]time inum links size */ +#define F_LESSTHAN 0x00000100 +#define F_GREATER 0x00000200 +#define F_ELG_MASK 0x00000300 +#define F_ATLEAST 0x00000400 /* flags perm */ +#define F_ANY 0x00000800 /* perm */ +#define F_MTMASK 0x00003000 +#define F_MTFLAG 0x00000000 /* fstype */ +#define F_MTTYPE 0x00001000 +#define F_MTUNKNOWN 0x00002000 +#define F_IGNCASE 0x00010000 /* iname ipath iregex */ +#define F_EXACTTIME F_IGNCASE /* -[acm]time units syntax */ +#define F_EXECPLUS 0x00020000 /* -exec ... {} + */ +#if HAVE_STRUCT_STAT_ST_BIRTHTIME +#define F_TIME_B 0x00040000 /* one of -Btime, -Bnewer, -newerB* */ +#define F_TIME2_B 0x00080000 /* one of -newer?B */ +#endif +#define F_LINK 0x00100000 /* lname or ilname */ + +/* node definition */ +typedef struct _plandata { + struct _plandata *next; /* next node */ + exec_f *execute; /* node evaluation function */ + int flags; /* private flags */ + union { + gid_t _g_data; /* gid */ + ino_t _i_data; /* inode */ + mode_t _m_data; /* mode mask */ + struct { + u_long _f_flags; + u_long _f_notflags; + } fl; + nlink_t _l_data; /* link count */ + short _d_data; /* level depth (-1 to N) */ + off_t _o_data; /* file size */ + struct timespec _t_data; /* time value */ + uid_t _u_data; /* uid */ + short _mt_data; /* mount flags */ + struct _plandata *_p_data[2]; /* PLAN trees */ + struct _ex { + char **_e_argv; /* argv array */ + char **_e_orig; /* original strings */ + int *_e_len; /* allocated length */ + int _e_pbnum; /* base num. of args. used */ + int _e_ppos; /* number of arguments used */ + int _e_pnummax; /* max. number of arguments */ + int _e_psize; /* number of bytes of args. */ + int _e_pbsize; /* base num. of bytes of args */ + int _e_psizemax; /* max num. of bytes of args */ + struct _plandata *_e_next;/* next F_EXECPLUS in tree */ + } ex; + char *_a_data[2]; /* array of char pointers */ + char *_c_data; /* char pointer */ + regex_t *_re_data; /* regex */ + } p_un; +} PLAN; +#define a_data p_un._a_data +#define c_data p_un._c_data +#define d_data p_un._d_data +#define fl_flags p_un.fl._f_flags +#define fl_notflags p_un.fl._f_notflags +#define g_data p_un._g_data +#define i_data p_un._i_data +#define l_data p_un._l_data +#define m_data p_un._m_data +#define mt_data p_un._mt_data +#define o_data p_un._o_data +#define p_data p_un._p_data +#define t_data p_un._t_data +#define u_data p_un._u_data +#define re_data p_un._re_data +#define e_argv p_un.ex._e_argv +#define e_orig p_un.ex._e_orig +#define e_len p_un.ex._e_len +#define e_pbnum p_un.ex._e_pbnum +#define e_ppos p_un.ex._e_ppos +#define e_pnummax p_un.ex._e_pnummax +#define e_psize p_un.ex._e_psize +#define e_pbsize p_un.ex._e_pbsize +#define e_psizemax p_un.ex._e_psizemax +#define e_next p_un.ex._e_next + +typedef struct _option { + const char *name; /* option name */ + creat_f *create; /* create function */ + exec_f *execute; /* execute function */ + int flags; +} OPTION; + +#include "extern.h" diff --git a/src/find/function.c b/src/find/function.c new file mode 100644 index 0000000..e8b2979 --- /dev/null +++ b/src/find/function.c @@ -0,0 +1,2304 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Cimarron D. Taylor of the University of California, Berkeley. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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 +#include +#include + +#ifdef __linux__ +#include +#include +#include +#include +#include +#include +#else +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "find.h" + +#if HAVE_LINUX_STATX +/* + * Linux doesn't have st_birthtime in struct stat. + * Use statx() to retrieve birth time. + */ +static int +get_birthtime(const char *path, struct timespec *ts) +{ + struct statx sx; + if (statx(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW, STATX_BTIME, &sx) != 0) + return -1; + if (!(sx.stx_mask & STATX_BTIME)) + return -1; + ts->tv_sec = sx.stx_btime.tv_sec; + ts->tv_nsec = sx.stx_btime.tv_nsec; + return 0; +} +#endif + +static PLAN *palloc(OPTION *); +static long long find_parsenum(PLAN *, const char *, char *, char *); +static long long find_parsetime(PLAN *, const char *, char *); +static char *nextarg(OPTION *, char ***); +static void print_escaped_char(int); +static void print_ctime_like(const struct timespec *); +static void print_time_field(const struct timespec *, int); +static int get_entry_timespec(FTSENT *, int, struct timespec *); +static void print_printf_directive(int, FTSENT *); + +extern char **environ; + +static PLAN *lastexecplus = NULL; + +#define COMPARE(a, b) do { \ + switch (plan->flags & F_ELG_MASK) { \ + case F_EQUAL: \ + return (a == b); \ + case F_LESSTHAN: \ + return (a < b); \ + case F_GREATER: \ + return (a > b); \ + default: \ + abort(); \ + } \ +} while(0) + +static void +print_escaped_char(int ch) +{ + switch (ch) { + case 'a': fputc('\a', stdout); break; + case 'b': fputc('\b', stdout); break; + case 'c': fflush(stdout); exit(0); + case 'f': fputc('\f', stdout); break; + case 'n': fputc('\n', stdout); break; + case 'r': fputc('\r', stdout); break; + case 't': fputc('\t', stdout); break; + case 'v': fputc('\v', stdout); break; + case '0': fputc('\0', stdout); break; + case '\\': fputc('\\', stdout); break; + default: fputc(ch, stdout); break; + } +} + +static void +print_ctime_like(const struct timespec *ts) +{ + char *s; + size_t len; + + s = ctime(&ts->tv_sec); + if (s == NULL) + return; + len = strlen(s); + if (len > 0 && s[len - 1] == '\n') + len--; + (void)fwrite(s, 1, len, stdout); +} + +static void +print_time_field(const struct timespec *ts, int field) +{ + char buf[128]; + char fmt[3] = {'%', (char)field, '\0'}; + struct tm tm; + + if (localtime_r(&ts->tv_sec, &tm) == NULL) + return; + if (strftime(buf, sizeof(buf), fmt, &tm) == 0) + return; + fputs(buf, stdout); +} + +static int +get_entry_timespec(FTSENT *entry, int which, struct timespec *ts) +{ + switch (which) { + case 'A': + ts->tv_sec = entry->fts_statp->st_atime; + ts->tv_nsec = entry->fts_statp->st_atim.tv_nsec; + return 0; + case 'C': + ts->tv_sec = entry->fts_statp->st_ctime; + ts->tv_nsec = entry->fts_statp->st_ctim.tv_nsec; + return 0; + case 'T': + ts->tv_sec = entry->fts_statp->st_mtime; + ts->tv_nsec = entry->fts_statp->st_mtim.tv_nsec; + return 0; + case 'B': +#if HAVE_STRUCT_STAT_ST_BIRTHTIME +#if HAVE_LINUX_STATX + return get_birthtime(entry->fts_accpath, ts); +#else + *ts = entry->fts_statp->st_birthtim; + return 0; +#endif +#else + return -1; +#endif + default: + return -1; + } +} + +static void +print_printf_directive(int code, FTSENT *entry) +{ + char buf[16]; + char linkbuf[MAXPATHLEN + 1]; + char *slash; + ssize_t len; + + switch (code) { + case '%': + fputc('%', stdout); + break; + case 'b': + fprintf(stdout, "%jd", (intmax_t)entry->fts_statp->st_blocks); + break; + case 'd': + fprintf(stdout, "%d", entry->fts_level); + break; + case 'f': + slash = strrchr(entry->fts_path, '/'); + fputs(slash ? slash + 1 : entry->fts_path, stdout); + break; + case 'g': + fputs(group_from_gid(entry->fts_statp->st_gid, 0), stdout); + break; + case 'G': + fprintf(stdout, "%ju", (uintmax_t)entry->fts_statp->st_gid); + break; + case 'h': + slash = strrchr(entry->fts_path, '/'); + if (slash == NULL) + fputc('.', stdout); + else if (slash == entry->fts_path) + fputc('/', stdout); + else + (void)fwrite(entry->fts_path, 1, (size_t)(slash - entry->fts_path), stdout); + break; + case 'i': + fprintf(stdout, "%ju", (uintmax_t)entry->fts_statp->st_ino); + break; + case 'k': + fprintf(stdout, "%jd", (intmax_t)((entry->fts_statp->st_blocks + 1) / 2)); + break; + case 'l': + if (S_ISLNK(entry->fts_statp->st_mode)) { + len = readlink(entry->fts_accpath, linkbuf, sizeof(linkbuf) - 1); + if (len >= 0) { + linkbuf[len] = '\0'; + fputs(linkbuf, stdout); + } + } + break; + case 'm': + fprintf(stdout, "%#o", entry->fts_statp->st_mode & 07777); + break; + case 'M': + strmode(entry->fts_statp->st_mode, buf); + buf[10] = '\0'; + fputs(buf, stdout); + break; + case 'n': + fprintf(stdout, "%ju", (uintmax_t)entry->fts_statp->st_nlink); + break; + case 'p': + fputs(entry->fts_path, stdout); + break; + case 'P': + if (strncmp(entry->fts_path, "./", 2) == 0) + fputs(entry->fts_path + 2, stdout); + else + fputs(entry->fts_path, stdout); + break; + case 's': + fprintf(stdout, "%jd", (intmax_t)entry->fts_statp->st_size); + break; + case 'u': + fputs(user_from_uid(entry->fts_statp->st_uid, 0), stdout); + break; + case 'U': + fprintf(stdout, "%ju", (uintmax_t)entry->fts_statp->st_uid); + break; + case 'y': + if (S_ISREG(entry->fts_statp->st_mode)) + fputc('f', stdout); + else if (S_ISDIR(entry->fts_statp->st_mode)) + fputc('d', stdout); + else if (S_ISLNK(entry->fts_statp->st_mode)) + fputc('l', stdout); + else if (S_ISCHR(entry->fts_statp->st_mode)) + fputc('c', stdout); + else if (S_ISBLK(entry->fts_statp->st_mode)) + fputc('b', stdout); + else if (S_ISFIFO(entry->fts_statp->st_mode)) + fputc('p', stdout); + else if (S_ISSOCK(entry->fts_statp->st_mode)) + fputc('s', stdout); + else + fputc('?', stdout); + break; + case 'a': { + struct timespec ts; + if (get_entry_timespec(entry, 'A', &ts) == 0) + print_ctime_like(&ts); + break; + } + case 'c': { + struct timespec ts; + if (get_entry_timespec(entry, 'C', &ts) == 0) + print_ctime_like(&ts); + break; + } + case 't': { + struct timespec ts; + if (get_entry_timespec(entry, 'T', &ts) == 0) + print_ctime_like(&ts); + break; + } + default: + fputc('%', stdout); + fputc(code, stdout); + break; + } +} + +static PLAN * +palloc(OPTION *option) +{ + PLAN *new; + + if ((new = malloc(sizeof(PLAN))) == NULL) + err(1, NULL); + new->execute = option->execute; + new->flags = option->flags; + new->next = NULL; + return new; +} + +/* + * find_parsenum -- + * Parse a string of the form [+-]# and return the value. + */ +static long long +find_parsenum(PLAN *plan, const char *option, char *vp, char *endch) +{ + long long value; + char *endchar, *str; /* Pointer to character ending conversion. */ + + /* Determine comparison from leading + or -. */ + str = vp; + switch (*str) { + case '+': + ++str; + plan->flags |= F_GREATER; + break; + case '-': + ++str; + plan->flags |= F_LESSTHAN; + break; + default: + plan->flags |= F_EQUAL; + break; + } + + /* + * Convert the string with strtoq(). Note, if strtoq() returns zero + * and endchar points to the beginning of the string we know we have + * a syntax error. + */ + value = strtoq(str, &endchar, 10); + if (value == 0 && endchar == str) + errx(1, "%s: %s: illegal numeric value", option, vp); + if (endchar[0] && endch == NULL) + errx(1, "%s: %s: illegal trailing character", option, vp); + if (endch) + *endch = endchar[0]; + return value; +} + +/* + * find_parsetime -- + * Parse a string of the form [+-]([0-9]+[smhdw]?)+ and return the value. + */ +static long long +find_parsetime(PLAN *plan, const char *option, char *vp) +{ + long long secs, value; + char *str, *unit; /* Pointer to character ending conversion. */ + + /* Determine comparison from leading + or -. */ + str = vp; + switch (*str) { + case '+': + ++str; + plan->flags |= F_GREATER; + break; + case '-': + ++str; + plan->flags |= F_LESSTHAN; + break; + default: + plan->flags |= F_EQUAL; + break; + } + + value = strtoq(str, &unit, 10); + if (value == 0 && unit == str) { + errx(1, "%s: %s: illegal time value", option, vp); + /* NOTREACHED */ + } + if (*unit == '\0') + return value; + + /* Units syntax. */ + secs = 0; + for (;;) { + switch(*unit) { + case 's': /* seconds */ + secs += value; + break; + case 'm': /* minutes */ + secs += value * 60; + break; + case 'h': /* hours */ + secs += value * 3600; + break; + case 'd': /* days */ + secs += value * 86400; + break; + case 'w': /* weeks */ + secs += value * 604800; + break; + default: + errx(1, "%s: %s: bad unit '%c'", option, vp, *unit); + /* NOTREACHED */ + } + str = unit + 1; + if (*str == '\0') /* EOS */ + break; + value = strtoq(str, &unit, 10); + if (value == 0 && unit == str) { + errx(1, "%s: %s: illegal time value", option, vp); + /* NOTREACHED */ + } + if (*unit == '\0') { + errx(1, "%s: %s: missing trailing unit", option, vp); + /* NOTREACHED */ + } + } + plan->flags |= F_EXACTTIME; + return secs; +} + +/* + * nextarg -- + * Check that another argument still exists, return a pointer to it, + * and increment the argument vector pointer. + */ +static char * +nextarg(OPTION *option, char ***argvp) +{ + char *arg; + + if ((arg = **argvp) == NULL) + errx(1, "%s: requires additional arguments", option->name); + (*argvp)++; + return arg; +} /* nextarg() */ + +/* + * The value of n for the inode times (atime, birthtime, ctime, mtime) is a + * range, i.e. n matches from (n - 1) to n 24 hour periods. This interacts + * with -n, such that "-mtime -1" would be less than 0 days, which isn't what + * the user wanted. Correct so that -1 is "less than 1". + */ +#define TIME_CORRECT(p) \ + if (((p)->flags & F_ELG_MASK) == F_LESSTHAN) \ + ++((p)->t_data.tv_sec); + +/* + * -[acm]min n functions -- + * + * True if the difference between the + * file access time (-amin) + * file birth time (-Bmin) + * last change of file status information (-cmin) + * file modification time (-mmin) + * and the current time is n min periods. + */ +int +f_Xmin(PLAN *plan, FTSENT *entry) +{ + time_t ref = now; + + if (plan->flags & F_TIME_C) { + COMPARE((ref - entry->fts_statp->st_ctime + + 60 - 1) / 60, plan->t_data.tv_sec); + } else if (plan->flags & F_TIME_A) { + COMPARE((ref - entry->fts_statp->st_atime + + 60 - 1) / 60, plan->t_data.tv_sec); +#if HAVE_STRUCT_STAT_ST_BIRTHTIME + } else if (plan->flags & F_TIME_B) { +#if HAVE_LINUX_STATX + struct timespec bts; + if (get_birthtime(entry->fts_accpath, &bts) == 0) { + COMPARE((ref - bts.tv_sec + 60 - 1) / 60, plan->t_data.tv_sec); + } + return 0; +#else + COMPARE((ref - entry->fts_statp->st_birthtime + + 60 - 1) / 60, plan->t_data.tv_sec); +#endif +#endif + } else { + COMPARE((ref - entry->fts_statp->st_mtime + + 60 - 1) / 60, plan->t_data.tv_sec); + } +} + +PLAN * +c_Xmin(OPTION *option, char ***argvp) +{ + char *nmins; + PLAN *new; + + nmins = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + new->t_data.tv_sec = find_parsenum(new, option->name, nmins, NULL); + new->t_data.tv_nsec = 0; + TIME_CORRECT(new); + return new; +} + +/* + * -[acm]time n functions -- + * + * True if the difference between the + * file access time (-atime) + * file birth time (-Btime) + * last change of file status information (-ctime) + * file modification time (-mtime) + * and the current time is n 24 hour periods. + */ + +int +f_Xtime(PLAN *plan, FTSENT *entry) +{ + time_t ref = now; + time_t xtime; + + if (plan->flags & F_TIME_A) + xtime = entry->fts_statp->st_atime; +#if HAVE_STRUCT_STAT_ST_BIRTHTIME + else if (plan->flags & F_TIME_B) { +#if HAVE_LINUX_STATX + struct timespec bts; + if (get_birthtime(entry->fts_accpath, &bts) == 0) + xtime = bts.tv_sec; + else + xtime = 0; +#else + xtime = entry->fts_statp->st_birthtime; +#endif + } +#endif + else if (plan->flags & F_TIME_C) + xtime = entry->fts_statp->st_ctime; + else + xtime = entry->fts_statp->st_mtime; + + if (plan->flags & F_EXACTTIME) + COMPARE(ref - xtime, plan->t_data.tv_sec); + else + COMPARE((ref - xtime + 86400 - 1) / 86400, plan->t_data.tv_sec); +} + +PLAN * +c_Xtime(OPTION *option, char ***argvp) +{ + char *value; + PLAN *new; + + value = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + new->t_data.tv_sec = find_parsetime(new, option->name, value); + new->t_data.tv_nsec = 0; + if (!(new->flags & F_EXACTTIME)) + TIME_CORRECT(new); + return new; +} + +/* + * -maxdepth/-mindepth n functions -- + * + * Does the same as -prune if the level of the current file is + * greater/less than the specified maximum/minimum depth. + * + * Note that -maxdepth and -mindepth are handled specially in + * find_execute() so their f_* functions are set to f_always_true(). + */ +PLAN * +c_mXXdepth(OPTION *option, char ***argvp) +{ + char *dstr; + PLAN *new; + + dstr = nextarg(option, argvp); + if (dstr[0] == '-') + /* all other errors handled by find_parsenum() */ + errx(1, "%s: %s: value must be positive", option->name, dstr); + + new = palloc(option); + if (option->flags & F_MAXDEPTH) + maxdepth = find_parsenum(new, option->name, dstr, NULL); + else + mindepth = find_parsenum(new, option->name, dstr, NULL); + return new; +} + +#if defined(ACL_TYPE_NFS4) || defined(__linux__) +/* + * -acl function -- + * + * Show files with EXTENDED ACL attributes. + */ +int +f_acl(PLAN *plan __unused, FTSENT *entry) +{ + acl_t facl; + int ret; + + if (S_ISLNK(entry->fts_statp->st_mode)) + return 0; + +#ifdef __linux__ + /* Linux: check for non-trivial POSIX ACL (more than base 3 entries) */ + facl = acl_get_file(entry->fts_accpath, ACL_TYPE_ACCESS); + if (facl == NULL) { + if (errno == ENOTSUP || errno == ENOSYS) + return 0; + warn("%s", entry->fts_accpath); + return 0; + } + ret = acl_entries(facl); + acl_free(facl); + /* A trivial POSIX ACL has exactly 3 entries (user, group, other) */ + return (ret > 3); +#else + { + acl_type_t acl_type; + int acl_supported = 0, trivial; + + ret = pathconf(entry->fts_accpath, _PC_ACL_NFS4); + if (ret > 0) { + acl_supported = 1; + acl_type = ACL_TYPE_NFS4; + } else if (ret < 0 && errno != EINVAL) { + warn("%s", entry->fts_accpath); + return (0); + } + if (acl_supported == 0) { + ret = pathconf(entry->fts_accpath, _PC_ACL_EXTENDED); + if (ret > 0) { + acl_supported = 1; + acl_type = ACL_TYPE_ACCESS; + } else if (ret < 0 && errno != EINVAL) { + warn("%s", entry->fts_accpath); + return (0); + } + } + if (acl_supported == 0) + return (0); + + facl = acl_get_file(entry->fts_accpath, acl_type); + if (facl == NULL) { + warn("%s", entry->fts_accpath); + return (0); + } + ret = acl_is_trivial_np(facl, &trivial); + acl_free(facl); + if (ret) { + warn("%s", entry->fts_accpath); + return (0); + } + if (trivial) + return (0); + return (1); + } +#endif +} +#endif + +PLAN * +c_acl(OPTION *option, char ***argvp __unused) +{ + ftsoptions &= ~FTS_NOSTAT; + return (palloc(option)); +} + +/* + * -delete functions -- + * + * True always. Makes its best shot and continues on regardless. + */ +int +f_delete(PLAN *plan __unused, FTSENT *entry) +{ + /* ignore these from fts */ + if (strcmp(entry->fts_accpath, ".") == 0 || + strcmp(entry->fts_accpath, "..") == 0) + return 1; + + /* sanity check */ + if (isdepth == 0 || /* depth off */ + (ftsoptions & FTS_NOSTAT)) /* not stat()ing */ + errx(1, "-delete: insecure options got turned on"); + + if (!(ftsoptions & FTS_PHYSICAL) || /* physical off */ + (ftsoptions & FTS_LOGICAL)) /* or finally, logical on */ + errx(1, "-delete: forbidden when symlinks are followed"); + + /* Potentially unsafe - do not accept relative paths whatsoever */ + if (entry->fts_level > FTS_ROOTLEVEL && + strchr(entry->fts_accpath, '/') != NULL) + errx(1, "-delete: %s: relative path potentially not safe", + entry->fts_accpath); + +#if HAVE_STRUCT_STAT_ST_FLAGS + /* Turn off user immutable bits if running as root */ + if ((entry->fts_statp->st_flags & (UF_APPEND|UF_IMMUTABLE)) && + !(entry->fts_statp->st_flags & (SF_APPEND|SF_IMMUTABLE)) && + geteuid() == 0) + lchflags(entry->fts_accpath, + entry->fts_statp->st_flags &= ~(UF_APPEND|UF_IMMUTABLE)); +#endif + + /* rmdir directories, unlink everything else */ + if (S_ISDIR(entry->fts_statp->st_mode)) { + if (rmdir(entry->fts_accpath) < 0 && errno != ENOTEMPTY) + warn("-delete: rmdir(%s)", entry->fts_path); + } else { + if (unlink(entry->fts_accpath) < 0) + warn("-delete: unlink(%s)", entry->fts_path); + } + + /* "succeed" */ + return 1; +} + +PLAN * +c_delete(OPTION *option, char ***argvp __unused) +{ + + ftsoptions &= ~FTS_NOSTAT; /* no optimise */ + isoutput = 1; /* possible output */ + isdepth = 1; /* -depth implied */ + + /* + * Try to avoid the confusing error message about relative paths + * being potentially not safe. + */ + if (ftsoptions & FTS_NOCHDIR) + errx(1, "%s: forbidden when the current directory cannot be opened", + "-delete"); + + return palloc(option); +} + + +/* + * always_true -- + * + * Always true, used for -maxdepth, -mindepth, -xdev, -follow, and -true + */ +int +f_always_true(PLAN *plan __unused, FTSENT *entry __unused) +{ + return 1; +} + +/* + * -depth functions -- + * + * With argument: True if the file is at level n. + * Without argument: Always true, causes descent of the directory hierarchy + * to be done so that all entries in a directory are acted on before the + * directory itself. + */ +int +f_depth(PLAN *plan, FTSENT *entry) +{ + if (plan->flags & F_DEPTH) + COMPARE(entry->fts_level, plan->d_data); + else + return 1; +} + +PLAN * +c_depth(OPTION *option, char ***argvp) +{ + PLAN *new; + char *str; + + new = palloc(option); + + str = **argvp; + if (str && !(new->flags & F_DEPTH)) { + /* skip leading + or - */ + if (*str == '+' || *str == '-') + str++; + /* skip sign */ + if (*str == '+' || *str == '-') + str++; + if (isdigit(*str)) + new->flags |= F_DEPTH; + } + + if (new->flags & F_DEPTH) { /* -depth n */ + char *ndepth; + + ndepth = nextarg(option, argvp); + new->d_data = find_parsenum(new, option->name, ndepth, NULL); + } else { /* -d */ + isdepth = 1; + } + + return new; +} + +PLAN * +c_daystart(OPTION *option, char ***argvp __unused) +{ + struct tm tm; + + if (localtime_r(&now, &tm) == NULL) + return palloc(option); + tm.tm_hour = 0; + tm.tm_min = 0; + tm.tm_sec = 0; + now = mktime(&tm); + daystart = 1; + return palloc(option); +} + +/* + * -empty functions -- + * + * True if the file or directory is empty + */ +int +f_empty(PLAN *plan __unused, FTSENT *entry) +{ + if (S_ISREG(entry->fts_statp->st_mode) && + entry->fts_statp->st_size == 0) + return 1; + if (S_ISDIR(entry->fts_statp->st_mode)) { + struct dirent *dp; + int empty; + DIR *dir; + + empty = 1; + dir = opendir(entry->fts_accpath); + if (dir == NULL) + return 0; + for (dp = readdir(dir); dp; dp = readdir(dir)) + if (dp->d_name[0] != '.' || + (dp->d_name[1] != '\0' && + (dp->d_name[1] != '.' || dp->d_name[2] != '\0'))) { + empty = 0; + break; + } + closedir(dir); + return empty; + } + return 0; +} + +PLAN * +c_empty(OPTION *option, char ***argvp __unused) +{ + ftsoptions &= ~FTS_NOSTAT; + + return palloc(option); +} + +/* + * [-exec | -execdir | -ok] utility [arg ... ] ; functions -- + * + * True if the executed utility returns a zero value as exit status. + * The end of the primary expression is delimited by a semicolon. If + * "{}" occurs anywhere, it gets replaced by the current pathname, + * or, in the case of -execdir, the current basename (filename + * without leading directory prefix). For -exec and -ok, + * the current directory for the execution of utility is the same as + * the current directory when the find utility was started, whereas + * for -execdir, it is the directory the file resides in. + * + * The primary -ok differs from -exec in that it requests affirmation + * of the user before executing the utility. + */ +int +f_exec(PLAN *plan, FTSENT *entry) +{ + int cnt; + pid_t pid; + int status; + char *file; + + if (entry == NULL && plan->flags & F_EXECPLUS) { + if (plan->e_ppos == plan->e_pbnum) + return (1); + plan->e_argv[plan->e_ppos] = NULL; + goto doexec; + } + + /* XXX - if file/dir ends in '/' this will not work -- can it? */ + if ((plan->flags & F_EXECDIR) && \ + (file = strrchr(entry->fts_path, '/'))) + file++; + else + file = entry->fts_path; + + if (plan->flags & F_EXECPLUS) { + if ((plan->e_argv[plan->e_ppos] = strdup(file)) == NULL) + err(1, NULL); + plan->e_len[plan->e_ppos] = strlen(file); + plan->e_psize += plan->e_len[plan->e_ppos]; + if (++plan->e_ppos < plan->e_pnummax && + plan->e_psize < plan->e_psizemax) + return (1); + plan->e_argv[plan->e_ppos] = NULL; + } else { + for (cnt = 0; plan->e_argv[cnt]; ++cnt) + if (plan->e_len[cnt]) + brace_subst(plan->e_orig[cnt], + &plan->e_argv[cnt], file, + plan->e_len[cnt]); + } + +doexec: if ((plan->flags & F_NEEDOK) && !queryuser(plan->e_argv)) + return 0; + + /* make sure find output is interspersed correctly with subprocesses */ + fflush(stdout); + fflush(stderr); + + switch (pid = fork()) { + case -1: + err(1, "fork"); + /* NOTREACHED */ + case 0: + /* change dir back from where we started */ + if (!(plan->flags & F_EXECDIR) && + !(ftsoptions & FTS_NOCHDIR) && fchdir(dotfd)) { + warn("chdir"); + _exit(1); + } + execvp(plan->e_argv[0], plan->e_argv); + warn("%s", plan->e_argv[0]); + _exit(1); + } + if (plan->flags & F_EXECPLUS) { + while (--plan->e_ppos >= plan->e_pbnum) + free(plan->e_argv[plan->e_ppos]); + plan->e_ppos = plan->e_pbnum; + plan->e_psize = plan->e_pbsize; + } + pid = waitpid(pid, &status, 0); + if (pid != -1 && WIFEXITED(status) && !WEXITSTATUS(status)) + return (1); + if (plan->flags & F_EXECPLUS) { + exitstatus = 1; + return (1); + } + return (0); +} + +/* + * c_exec, c_execdir, c_ok -- + * build three parallel arrays, one with pointers to the strings passed + * on the command line, one with (possibly duplicated) pointers to the + * argv array, and one with integer values that are lengths of the + * strings, but also flags meaning that the string has to be massaged. + */ +PLAN * +c_exec(OPTION *option, char ***argvp) +{ + PLAN *new; /* node returned */ + long argmax; + int cnt, i; + char **argv, **ap, **ep, *p; + + /* This would defeat -execdir's intended security. */ + if (option->flags & F_EXECDIR && ftsoptions & FTS_NOCHDIR) + errx(1, "%s: forbidden when the current directory cannot be opened", + "-execdir"); + + /* XXX - was in c_execdir, but seems unnecessary!? + ftsoptions &= ~FTS_NOSTAT; + */ + isoutput = 1; + + /* XXX - this is a change from the previous coding */ + new = palloc(option); + + for (ap = argv = *argvp;; ++ap) { + if (!*ap) + errx(1, + "%s: no terminating \";\" or \"+\"", option->name); + if (**ap == ';') + break; + if (**ap == '+' && ap != argv && strcmp(*(ap - 1), "{}") == 0) { + new->flags |= F_EXECPLUS; + break; + } + } + + if (ap == argv) + errx(1, "%s: no command specified", option->name); + + cnt = ap - *argvp + 1; + if (new->flags & F_EXECPLUS) { + new->e_ppos = new->e_pbnum = cnt - 2; + if ((argmax = sysconf(_SC_ARG_MAX)) == -1) { + warn("sysconf(_SC_ARG_MAX)"); + argmax = _POSIX_ARG_MAX; + } + argmax -= 1024; + for (ep = environ; *ep != NULL; ep++) + argmax -= strlen(*ep) + 1 + sizeof(*ep); + argmax -= 1 + sizeof(*ep); + /* + * Ensure that -execdir ... {} + does not mix files + * from different directories in one invocation. + * Files from the same directory should be handled + * in one invocation but there is no code for it. + */ + new->e_pnummax = new->flags & F_EXECDIR ? 1 : argmax / 16; + argmax -= sizeof(char *) * new->e_pnummax; + if (argmax <= 0) + errx(1, "no space for arguments"); + new->e_psizemax = argmax; + new->e_pbsize = 0; + cnt += new->e_pnummax + 1; + new->e_next = lastexecplus; + lastexecplus = new; + } + if ((new->e_argv = malloc(cnt * sizeof(char *))) == NULL) + err(1, NULL); + if ((new->e_orig = malloc(cnt * sizeof(char *))) == NULL) + err(1, NULL); + if ((new->e_len = malloc(cnt * sizeof(int))) == NULL) + err(1, NULL); + + for (argv = *argvp, cnt = 0; argv < ap; ++argv, ++cnt) { + new->e_orig[cnt] = *argv; + if (new->flags & F_EXECPLUS) + new->e_pbsize += strlen(*argv) + 1; + for (p = *argv; *p; ++p) + if (!(new->flags & F_EXECPLUS) && p[0] == '{' && + p[1] == '}') { + if ((new->e_argv[cnt] = + malloc(MAXPATHLEN)) == NULL) + err(1, NULL); + new->e_len[cnt] = MAXPATHLEN; + break; + } + if (!*p) { + new->e_argv[cnt] = *argv; + new->e_len[cnt] = 0; + } + } + if (new->flags & F_EXECPLUS) { + new->e_psize = new->e_pbsize; + cnt--; + for (i = 0; i < new->e_pnummax; i++) { + new->e_argv[cnt] = NULL; + new->e_len[cnt] = 0; + cnt++; + } + argv = ap; + goto done; + } + new->e_argv[cnt] = new->e_orig[cnt] = NULL; + +done: *argvp = argv + 1; + return new; +} + +/* Finish any pending -exec ... {} + functions. */ +void +finish_execplus(void) +{ + PLAN *p; + + p = lastexecplus; + while (p != NULL) { + (p->execute)(p, NULL); + p = p->e_next; + } +} + +#if HAVE_STRUCT_STAT_ST_FLAGS +int +f_flags(PLAN *plan, FTSENT *entry) +{ + u_long flags; + + flags = entry->fts_statp->st_flags; + if (plan->flags & F_ATLEAST) + return (flags | plan->fl_flags) == flags && + !(flags & plan->fl_notflags); + else if (plan->flags & F_ANY) + return (flags & plan->fl_flags) || + (flags | plan->fl_notflags) != flags; + else + return flags == plan->fl_flags && + !(plan->fl_flags & plan->fl_notflags); +} + +PLAN * +c_flags(OPTION *option, char ***argvp) +{ + char *flags_str; + PLAN *new; + u_long flags, notflags; + + flags_str = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + + if (*flags_str == '-') { + new->flags |= F_ATLEAST; + flags_str++; + } else if (*flags_str == '+') { + new->flags |= F_ANY; + flags_str++; + } + if (strtofflags(&flags_str, &flags, ¬flags) == 1) + errx(1, "%s: %s: illegal flags string", option->name, flags_str); + + new->fl_flags = flags; + new->fl_notflags = notflags; + return new; +} +#endif + +/* + * -follow functions -- + * + * Always true, causes symbolic links to be followed on a global + * basis. + */ +PLAN * +c_follow(OPTION *option, char ***argvp __unused) +{ + ftsoptions &= ~FTS_PHYSICAL; + ftsoptions |= FTS_LOGICAL; + + return palloc(option); +} + +#if HAVE_STRUCT_STATFS_F_FSTYPENAME || defined(__linux__) +/* + * -fstype functions -- + * + * True if the file is of a certain type. + */ + +#ifdef __linux__ +/* + * Linux doesn't have f_fstypename in struct statfs. + * Read /proc/self/mountinfo to find the filesystem type by matching + * the longest mount point prefix for the given path. + */ +static const char * +linux_fstype_for_path(const char *path) +{ + static char result[64]; + static char cached_path[PATH_MAX]; + FILE *fp; + char line[2048]; + char resolved[PATH_MAX]; + char best_mount[PATH_MAX] = ""; + char best_fstype[64] = ""; + size_t best_len = 0; + + /* Resolve to absolute path */ + if (realpath(path, resolved) == NULL) + strlcpy(resolved, path, sizeof(resolved)); + + if (strcmp(resolved, cached_path) == 0 && result[0] != '\0') + return result; + + fp = fopen("/proc/self/mountinfo", "r"); + if (fp == NULL) + return NULL; + + while (fgets(line, sizeof(line), fp) != NULL) { + /* + * mountinfo format: + * id parent maj:min root mount_point opts ... - fstype source super_opts + */ + char mount_point[PATH_MAX]; + char *dash, *fstype; + int n; + + /* Skip id, parent, maj:min, root; extract mount_point */ + n = 0; + char *p = line; + /* Skip 4 fields: id, parent, maj:min, root */ + for (int i = 0; i < 4 && *p; i++) { + while (*p && *p != ' ') p++; + while (*p == ' ') p++; + } + /* p now points to mount_point */ + n = 0; + while (p[n] && p[n] != ' ') n++; + if (n == 0 || (size_t)n >= sizeof(mount_point)) + continue; + memcpy(mount_point, p, n); + mount_point[n] = '\0'; + + /* Check if this mount point is a prefix of the resolved path */ + size_t mlen = strlen(mount_point); + if (mlen > best_len && + strncmp(resolved, mount_point, mlen) == 0 && + (resolved[mlen] == '/' || resolved[mlen] == '\0' || mlen == 1)) { + dash = strstr(line, " - "); + if (dash == NULL) + continue; + dash += 3; + fstype = dash; + while (*dash && *dash != ' ') + dash++; + *dash = '\0'; + best_len = mlen; + strlcpy(best_mount, mount_point, sizeof(best_mount)); + strlcpy(best_fstype, fstype, sizeof(best_fstype)); + } + } + fclose(fp); + + if (best_len == 0) + return NULL; + + strlcpy(result, best_fstype, sizeof(result)); + strlcpy(cached_path, resolved, sizeof(cached_path)); + return result; +} +#endif /* __linux__ */ + +int +f_fstype(PLAN *plan, FTSENT *entry) +{ + static dev_t curdev; + static int first = 1; + struct statfs sb; + static int val_flags; + static char fstype[64]; + char *p, save[2] = {0,0}; + + if ((plan->flags & F_MTMASK) == F_MTUNKNOWN) + return 0; + + if (first || curdev != entry->fts_statp->st_dev) { + curdev = entry->fts_statp->st_dev; + + if (entry->fts_info == FTS_SL || + entry->fts_info == FTS_SLNONE) { + if ((p = strrchr(entry->fts_accpath, '/')) != NULL) + ++p; + else + p = entry->fts_accpath; + save[0] = p[0]; + p[0] = '.'; + save[1] = p[1]; + p[1] = '\0'; + } else + p = NULL; + + if (statfs(entry->fts_accpath, &sb)) { + if (!ignore_readdir_race || errno != ENOENT) { + warn("statfs: %s", entry->fts_accpath); + exitstatus = 1; + } + return 0; + } + + if (p) { + p[0] = save[0]; + p[1] = save[1]; + } + + first = 0; + + val_flags = sb.f_flags; +#if HAVE_STRUCT_STATFS_F_FSTYPENAME + strlcpy(fstype, sb.f_fstypename, sizeof(fstype)); +#else + { + const char *name = linux_fstype_for_path(entry->fts_accpath); + if (name != NULL) + strlcpy(fstype, name, sizeof(fstype)); + else + strlcpy(fstype, "unknown", sizeof(fstype)); + } +#endif + } + switch (plan->flags & F_MTMASK) { + case F_MTFLAG: + return val_flags & plan->mt_data; + case F_MTTYPE: + return (strncmp(fstype, plan->c_data, sizeof(fstype)) == 0); + default: + abort(); + } +} + +PLAN * +c_fstype(OPTION *option, char ***argvp) +{ + char *fsname; + PLAN *new; + + fsname = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + switch (*fsname) { + case 'l': + if (!strcmp(fsname, "local")) { + new->flags |= F_MTFLAG; +#ifdef MNT_LOCAL + new->mt_data = MNT_LOCAL; +#else + new->mt_data = 0; +#endif + return new; + } + break; + case 'r': + if (!strcmp(fsname, "rdonly")) { + new->flags |= F_MTFLAG; +#ifdef MNT_RDONLY + new->mt_data = MNT_RDONLY; +#elif defined(ST_RDONLY) + new->mt_data = ST_RDONLY; +#else + new->mt_data = 1; +#endif + return new; + } + break; + } + + new->flags |= F_MTTYPE; + new->c_data = fsname; + return new; +} +#endif + +/* + * -group gname functions -- + * + * True if the file belongs to the group gname. If gname is numeric and + * an equivalent of the getgrnam() function does not return a valid group + * name, gname is taken as a group ID. + */ +int +f_group(PLAN *plan, FTSENT *entry) +{ + COMPARE(entry->fts_statp->st_gid, plan->g_data); +} + +PLAN * +c_group(OPTION *option, char ***argvp) +{ + char *gname; + PLAN *new; + struct group *g; + gid_t gid; + + gname = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + g = getgrnam(gname); + if (g == NULL) { + char* cp = gname; + if (gname[0] == '-' || gname[0] == '+') + gname++; + gid = atoi(gname); + if (gid == 0 && gname[0] != '0') + errx(1, "%s: %s: no such group", option->name, gname); + gid = find_parsenum(new, option->name, cp, NULL); + } else + gid = g->gr_gid; + + new->g_data = gid; + return new; +} + +/* + * -ignore_readdir_race functions -- + * + * Always true. Ignore errors which occur if a file or a directory + * in a starting point gets deleted between reading the name and calling + * stat on it while find is traversing the starting point. + */ + +PLAN * +c_ignore_readdir_race(OPTION *option, char ***argvp __unused) +{ + if (strcmp(option->name, "-ignore_readdir_race") == 0) + ignore_readdir_race = 1; + else + ignore_readdir_race = 0; + + return palloc(option); +} + +/* + * -inum n functions -- + * + * True if the file has inode # n. + */ +int +f_inum(PLAN *plan, FTSENT *entry) +{ + COMPARE(entry->fts_statp->st_ino, plan->i_data); +} + +PLAN * +c_inum(OPTION *option, char ***argvp) +{ + char *inum_str; + PLAN *new; + + inum_str = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + new->i_data = find_parsenum(new, option->name, inum_str, NULL); + return new; +} + +/* + * -samefile FN + * + * True if the file has the same inode (eg hard link) FN + */ + +/* f_samefile is just f_inum */ +PLAN * +c_samefile(OPTION *option, char ***argvp) +{ + char *fn; + PLAN *new; + struct stat sb; + int error; + + fn = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + if (ftsoptions & FTS_PHYSICAL) + error = lstat(fn, &sb); + else + error = stat(fn, &sb); + if (error != 0) + err(1, "%s", fn); + new->i_data = sb.st_ino; + return new; +} + +/* + * -links n functions -- + * + * True if the file has n links. + */ +int +f_links(PLAN *plan, FTSENT *entry) +{ + COMPARE(entry->fts_statp->st_nlink, plan->l_data); +} + +PLAN * +c_links(OPTION *option, char ***argvp) +{ + char *nlinks; + PLAN *new; + + nlinks = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + new->l_data = (nlink_t)find_parsenum(new, option->name, nlinks, NULL); + return new; +} + +/* + * -ls functions -- + * + * Always true - prints the current entry to stdout in "ls" format. + */ +int +f_ls(PLAN *plan __unused, FTSENT *entry) +{ + printlong(entry->fts_path, entry->fts_accpath, entry->fts_statp); + return 1; +} + +PLAN * +c_ls(OPTION *option, char ***argvp __unused) +{ + ftsoptions &= ~FTS_NOSTAT; + isoutput = 1; + + return palloc(option); +} + +/* + * -name functions -- + * + * True if the basename of the filename being examined + * matches pattern using Pattern Matching Notation S3.14 + */ +int +f_name(PLAN *plan, FTSENT *entry) +{ + char fn[PATH_MAX]; + const char *name; + ssize_t len; + + if (plan->flags & F_LINK) { + /* + * The below test both avoids obviously useless readlink() + * calls and ensures that symlinks with existent target do + * not match if symlinks are being followed. + * Assumption: fts will stat all symlinks that are to be + * followed and will return the stat information. + */ + if (entry->fts_info != FTS_NSOK && entry->fts_info != FTS_SL && + entry->fts_info != FTS_SLNONE) + return 0; + len = readlink(entry->fts_accpath, fn, sizeof(fn) - 1); + if (len == -1) + return 0; + fn[len] = '\0'; + name = fn; + } else + name = entry->fts_name; + return !fnmatch(plan->c_data, name, + plan->flags & F_IGNCASE ? FNM_CASEFOLD : 0); +} + +PLAN * +c_name(OPTION *option, char ***argvp) +{ + char *pattern; + PLAN *new; + + pattern = nextarg(option, argvp); + new = palloc(option); + new->c_data = pattern; + return new; +} + +/* + * -newer file functions -- + * + * True if the current file has been modified more recently + * then the modification time of the file named by the pathname + * file. + */ +int +f_newer(PLAN *plan, FTSENT *entry) +{ + struct timespec ft; + + if (plan->flags & F_TIME_C) + ft = entry->fts_statp->st_ctim; +#if HAVE_STRUCT_STAT_ST_BIRTHTIME + else if (plan->flags & F_TIME_A) + ft = entry->fts_statp->st_atim; + else if (plan->flags & F_TIME_B) { +#if HAVE_LINUX_STATX + if (get_birthtime(entry->fts_accpath, &ft) != 0) + memset(&ft, 0, sizeof(ft)); +#else + ft = entry->fts_statp->st_birthtim; +#endif + } +#endif + else + ft = entry->fts_statp->st_mtim; + return (ft.tv_sec > plan->t_data.tv_sec || + (ft.tv_sec == plan->t_data.tv_sec && + ft.tv_nsec > plan->t_data.tv_nsec)); +} + +PLAN * +c_newer(OPTION *option, char ***argvp) +{ + char *fn_or_tspec; + PLAN *new; + struct stat sb; + int error; + + fn_or_tspec = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + /* compare against what */ + if (option->flags & F_TIME2_T) { + new->t_data.tv_sec = get_date(fn_or_tspec); + if (new->t_data.tv_sec == (time_t) -1) + errx(1, "Can't parse date/time: %s", fn_or_tspec); + /* Use the seconds only in the comparison. */ + new->t_data.tv_nsec = 999999999; + } else { + if (ftsoptions & FTS_PHYSICAL) + error = lstat(fn_or_tspec, &sb); + else + error = stat(fn_or_tspec, &sb); + if (error != 0) + err(1, "%s", fn_or_tspec); + if (option->flags & F_TIME2_C) + new->t_data = sb.st_ctim; + else if (option->flags & F_TIME2_A) + new->t_data = sb.st_atim; +#if HAVE_STRUCT_STAT_ST_BIRTHTIME + else if (option->flags & F_TIME2_B) { +#if HAVE_LINUX_STATX + struct timespec bts; + if (get_birthtime(fn_or_tspec, &bts) == 0) + new->t_data = bts; + else + err(1, "%s", fn_or_tspec); +#else + new->t_data = sb.st_birthtim; +#endif + } +#endif + else + new->t_data = sb.st_mtim; + } + return new; +} + +/* + * -nogroup functions -- + * + * True if file belongs to a user ID for which the equivalent + * of the getgrnam() 9.2.1 [POSIX.1] function returns NULL. + */ +int +f_nogroup(PLAN *plan __unused, FTSENT *entry) +{ + return group_from_gid(entry->fts_statp->st_gid, 1) == NULL; +} + +PLAN * +c_nogroup(OPTION *option, char ***argvp __unused) +{ + ftsoptions &= ~FTS_NOSTAT; + + return palloc(option); +} + +/* + * -nouser functions -- + * + * True if file belongs to a user ID for which the equivalent + * of the getpwuid() 9.2.2 [POSIX.1] function returns NULL. + */ +int +f_nouser(PLAN *plan __unused, FTSENT *entry) +{ + return user_from_uid(entry->fts_statp->st_uid, 1) == NULL; +} + +PLAN * +c_nouser(OPTION *option, char ***argvp __unused) +{ + ftsoptions &= ~FTS_NOSTAT; + + return palloc(option); +} + +/* + * -path functions -- + * + * True if the path of the filename being examined + * matches pattern using Pattern Matching Notation S3.14 + */ +int +f_path(PLAN *plan, FTSENT *entry) +{ + return !fnmatch(plan->c_data, entry->fts_path, + plan->flags & F_IGNCASE ? FNM_CASEFOLD : 0); +} + +/* c_path is the same as c_name */ + +/* + * -perm functions -- + * + * The mode argument is used to represent file mode bits. If it starts + * with a leading digit, it's treated as an octal mode, otherwise as a + * symbolic mode. + */ +int +f_perm(PLAN *plan, FTSENT *entry) +{ + mode_t mode; + + mode = entry->fts_statp->st_mode & + (S_ISUID|S_ISGID|S_ISTXT|S_IRWXU|S_IRWXG|S_IRWXO); + if (plan->flags & F_ATLEAST) + return (plan->m_data | mode) == mode; + else if (plan->flags & F_ANY) + return (mode & plan->m_data); + else + return mode == plan->m_data; + /* NOTREACHED */ +} + +PLAN * +c_perm(OPTION *option, char ***argvp) +{ + char *perm; + PLAN *new; + mode_t *set; + + perm = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + + if (*perm == '-') { + new->flags |= F_ATLEAST; + ++perm; + } else if (*perm == '+' || *perm == '/') { + new->flags |= F_ANY; + ++perm; + } + + if ((set = setmode(perm)) == NULL) + errx(1, "%s: %s: illegal mode string", option->name, perm); + + new->m_data = getmode(set, 0); + free(set); + return new; +} + +/* + * -print functions -- + * + * Always true, causes the current pathname to be written to + * standard output. + */ +int +f_print(PLAN *plan __unused, FTSENT *entry) +{ + (void)puts(entry->fts_path); + return 1; +} + +PLAN * +c_print(OPTION *option, char ***argvp __unused) +{ + isoutput = 1; + + return palloc(option); +} + +/* + * -print0 functions -- + * + * Always true, causes the current pathname to be written to + * standard output followed by a NUL character + */ +int +f_print0(PLAN *plan __unused, FTSENT *entry) +{ + fputs(entry->fts_path, stdout); + fputc('\0', stdout); + return 1; +} + +/* c_print0 is the same as c_print */ + +int +f_printf(PLAN *plan, FTSENT *entry) +{ + const char *fmt; + + for (fmt = plan->c_data; *fmt != '\0'; fmt++) { + if (*fmt == '\\') { + if (fmt[1] != '\0') + print_escaped_char(*++fmt); + else + fputc('\\', stdout); + continue; + } + if (*fmt != '%') { + fputc(*fmt, stdout); + continue; + } + fmt++; + if (*fmt == '\0') { + fputc('%', stdout); + break; + } + if (*fmt == 'A' || *fmt == 'B' || *fmt == 'C' || *fmt == 'T') { + struct timespec ts; + int which = *fmt; + int field = *++fmt; + + if (field == '\0') { + fputc('%', stdout); + fputc(which, stdout); + break; + } + if (get_entry_timespec(entry, which, &ts) == 0) + print_time_field(&ts, field); + continue; + } + print_printf_directive(*fmt, entry); + } + return 1; +} + +PLAN * +c_printf(OPTION *option, char ***argvp) +{ + PLAN *new; + + new = palloc(option); + ftsoptions &= ~FTS_NOSTAT; + new->c_data = nextarg(option, argvp); + isoutput = 1; + return new; +} + +/* + * -prune functions -- + * + * Prune a portion of the hierarchy. + */ +int +f_prune(PLAN *plan __unused, FTSENT *entry) +{ + if (fts_set(tree, entry, FTS_SKIP)) + err(1, "%s", entry->fts_path); + return 1; +} + +/* c_prune == c_simple */ + +/* + * -regex functions -- + * + * True if the whole path of the file matches pattern using + * regular expression. + */ +int +f_regex(PLAN *plan, FTSENT *entry) +{ + char *str; + int len; + regex_t *pre; + regmatch_t pmatch; + int errcode; + char errbuf[LINE_MAX]; + int matched; + + pre = plan->re_data; + str = entry->fts_path; + len = strlen(str); + matched = 0; + + pmatch.rm_so = 0; + pmatch.rm_eo = len; + + errcode = regexec(pre, str, 1, &pmatch, REG_STARTEND); + + if (errcode != 0 && errcode != REG_NOMATCH) { + regerror(errcode, pre, errbuf, sizeof errbuf); + errx(1, "%s: %s", + plan->flags & F_IGNCASE ? "-iregex" : "-regex", errbuf); + } + + if (errcode == 0 && pmatch.rm_so == 0 && pmatch.rm_eo == len) + matched = 1; + + return matched; +} + +PLAN * +c_regex(OPTION *option, char ***argvp) +{ + PLAN *new; + char *pattern; + regex_t *pre; + int errcode; + char errbuf[LINE_MAX]; + + if ((pre = malloc(sizeof(regex_t))) == NULL) + err(1, NULL); + + pattern = nextarg(option, argvp); + + if ((errcode = regcomp(pre, pattern, + regexp_flags | (option->flags & F_IGNCASE ? REG_ICASE : 0))) != 0) { + regerror(errcode, pre, errbuf, sizeof errbuf); + errx(1, "%s: %s: %s", + option->flags & F_IGNCASE ? "-iregex" : "-regex", + pattern, errbuf); + } + + new = palloc(option); + new->re_data = pre; + + return new; +} + +/* c_simple covers c_prune, c_openparen, c_closeparen, c_not, c_or, c_true, c_false */ + +PLAN * +c_simple(OPTION *option, char ***argvp __unused) +{ + return palloc(option); +} + +/* + * -size n[c] functions -- + * + * True if the file size in bytes, divided by an implementation defined + * value and rounded up to the next integer, is n. If n is followed by + * one of c k M G T P, the size is in bytes, kilobytes, + * megabytes, gigabytes, terabytes or petabytes respectively. + */ +#define FIND_SIZE 512 +static int divsize = 1; + +int +f_size(PLAN *plan, FTSENT *entry) +{ + off_t size; + + size = divsize ? (entry->fts_statp->st_size + FIND_SIZE - 1) / + FIND_SIZE : entry->fts_statp->st_size; + COMPARE(size, plan->o_data); +} + +PLAN * +c_size(OPTION *option, char ***argvp) +{ + char *size_str; + PLAN *new; + char endch; + off_t scale; + + size_str = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + endch = 'c'; + new->o_data = find_parsenum(new, option->name, size_str, &endch); + if (endch != '\0') { + divsize = 0; + + switch (endch) { + case 'c': /* characters */ + scale = 0x1LL; + break; + case 'k': /* kilobytes 1<<10 */ + scale = 0x400LL; + break; + case 'M': /* megabytes 1<<20 */ + scale = 0x100000LL; + break; + case 'G': /* gigabytes 1<<30 */ + scale = 0x40000000LL; + break; + case 'T': /* terabytes 1<<40 */ + scale = 0x10000000000LL; + break; + case 'P': /* petabytes 1<<50 */ + scale = 0x4000000000000LL; + break; + default: + errx(1, "%s: %s: illegal trailing character", + option->name, size_str); + break; + } + if (new->o_data > QUAD_MAX / scale) + errx(1, "%s: %s: value too large", + option->name, size_str); + new->o_data *= scale; + } + return new; +} + +/* + * -sparse functions -- + * + * Check if a file is sparse by finding if it occupies fewer blocks + * than we expect based on its size. + */ +int +f_sparse(PLAN *plan __unused, FTSENT *entry) +{ + off_t expected_blocks; + + expected_blocks = (entry->fts_statp->st_size + 511) / 512; + return entry->fts_statp->st_blocks < expected_blocks; +} + +PLAN * +c_sparse(OPTION *option, char ***argvp __unused) +{ + ftsoptions &= ~FTS_NOSTAT; + + return palloc(option); +} + +/* + * -type c functions -- + * + * True if the type of the file is c, where c is b, c, d, p, f or w + * for block special file, character special file, directory, FIFO, + * regular file or whiteout respectively. + */ +int +f_type(PLAN *plan, FTSENT *entry) +{ + if (plan->m_data == S_IFDIR) + return (entry->fts_info == FTS_D || entry->fts_info == FTS_DC || + entry->fts_info == FTS_DNR || entry->fts_info == FTS_DOT || + entry->fts_info == FTS_DP); + else + return (entry->fts_statp->st_mode & S_IFMT) == plan->m_data; +} + +PLAN * +c_type(OPTION *option, char ***argvp) +{ + char *typestring; + PLAN *new; + mode_t mask; + + typestring = nextarg(option, argvp); + if (typestring[0] != 'd') + ftsoptions &= ~FTS_NOSTAT; + + switch (typestring[0]) { + case 'b': + mask = S_IFBLK; + break; + case 'c': + mask = S_IFCHR; + break; + case 'd': + mask = S_IFDIR; + break; + case 'f': + mask = S_IFREG; + break; + case 'l': + mask = S_IFLNK; + break; + case 'p': + mask = S_IFIFO; + break; + case 's': + mask = S_IFSOCK; + break; +#if defined(FTS_WHITEOUT) && defined(S_IFWHT) + case 'w': + mask = S_IFWHT; + ftsoptions |= FTS_WHITEOUT; + break; +#endif /* FTS_WHITEOUT */ + default: + errx(1, "%s: %s: unknown type", option->name, typestring); + } + + new = palloc(option); + new->m_data = mask; + return new; +} + +/* + * -user uname functions -- + * + * True if the file belongs to the user uname. If uname is numeric and + * an equivalent of the getpwnam() S9.2.2 [POSIX.1] function does not + * return a valid user name, uname is taken as a user ID. + */ +int +f_user(PLAN *plan, FTSENT *entry) +{ + COMPARE(entry->fts_statp->st_uid, plan->u_data); +} + +PLAN * +c_user(OPTION *option, char ***argvp) +{ + char *username; + PLAN *new; + struct passwd *p; + uid_t uid; + + username = nextarg(option, argvp); + ftsoptions &= ~FTS_NOSTAT; + + new = palloc(option); + p = getpwnam(username); + if (p == NULL) { + char* cp = username; + if( username[0] == '-' || username[0] == '+' ) + username++; + uid = atoi(username); + if (uid == 0 && username[0] != '0') + errx(1, "%s: %s: no such user", option->name, username); + uid = find_parsenum(new, option->name, cp, NULL); + } else + uid = p->pw_uid; + + new->u_data = uid; + return new; +} + +/* + * -xdev functions -- + * + * Always true, causes find not to descend past directories that have a + * different device ID (st_dev, see stat() S5.6.2 [POSIX.1]) + */ +PLAN * +c_xdev(OPTION *option, char ***argvp __unused) +{ + ftsoptions |= FTS_XDEV; + + return palloc(option); +} + +/* + * ( expression ) functions -- + * + * True if expression is true. + */ +int +f_expr(PLAN *plan, FTSENT *entry) +{ + PLAN *p; + int state = 0; + + for (p = plan->p_data[0]; + p && (state = (p->execute)(p, entry)); p = p->next); + return state; +} + +/* + * f_openparen and f_closeparen nodes are temporary place markers. They are + * eliminated during phase 2 of find_formplan() --- the '(' node is converted + * to a f_expr node containing the expression and the ')' node is discarded. + * The functions themselves are only used as constants. + */ + +int +f_openparen(PLAN *plan __unused, FTSENT *entry __unused) +{ + abort(); +} + +int +f_closeparen(PLAN *plan __unused, FTSENT *entry __unused) +{ + abort(); +} + +/* c_openparen == c_simple */ +/* c_closeparen == c_simple */ + +/* + * AND operator. Since AND is implicit, no node is allocated. + */ +PLAN * +c_and(OPTION *option __unused, char ***argvp __unused) +{ + return NULL; +} + +/* + * ! expression functions -- + * + * Negation of a primary; the unary NOT operator. + */ +int +f_not(PLAN *plan, FTSENT *entry) +{ + PLAN *p; + int state = 0; + + for (p = plan->p_data[0]; + p && (state = (p->execute)(p, entry)); p = p->next); + return !state; +} + +/* c_not == c_simple */ + +/* + * expression -o expression functions -- + * + * Alternation of primaries; the OR operator. The second expression is + * not evaluated if the first expression is true. + */ +int +f_or(PLAN *plan, FTSENT *entry) +{ + PLAN *p; + int state = 0; + + for (p = plan->p_data[0]; + p && (state = (p->execute)(p, entry)); p = p->next); + + if (state) + return 1; + + for (p = plan->p_data[1]; + p && (state = (p->execute)(p, entry)); p = p->next); + return state; +} + +/* c_or == c_simple */ + +/* + * -false + * + * Always false. + */ +int +f_false(PLAN *plan __unused, FTSENT *entry __unused) +{ + return 0; +} + +/* c_false == c_simple */ + +/* + * -quit + * + * Exits the program + */ +int +f_quit(PLAN *plan __unused, FTSENT *entry __unused) +{ + finish_execplus(); + exit(exitstatus); +} + +/* c_quit == c_simple */ + +/* + * -readable + * + * File is readable + */ +int +f_readable(PLAN *plan __unused, FTSENT *entry) +{ + return (access(entry->fts_path, R_OK) == 0); +} + +/* c_readable == c_simple */ + +/* + * -writable + * + * File is writable + */ +int +f_writable(PLAN *plan __unused, FTSENT *entry) +{ + return (access(entry->fts_path, W_OK) == 0); +} + +/* c_writable == c_simple */ + +/* + * -executable + * + * File is executable + */ +int +f_executable(PLAN *plan __unused, FTSENT *entry) +{ + return (access(entry->fts_path, X_OK) == 0); +} + +/* c_executable == c_simple */ diff --git a/src/find/getdate.y b/src/find/getdate.y new file mode 100644 index 0000000..d743f42 --- /dev/null +++ b/src/find/getdate.y @@ -0,0 +1,945 @@ +%{ +/* +** Originally written by Steven M. Bellovin while +** at the University of North Carolina at Chapel Hill. Later tweaked by +** a couple of people on Usenet. Completely overhauled by Rich $alz +** and Jim Berets in August, 1990; +** +** This grammar has 10 shift/reduce conflicts. +** +** This code is in the public domain and has no copyright. +*/ +/* SUPPRESS 287 on yaccpar_sccsid *//* Unused static variable */ +/* SUPPRESS 288 on yyerrlab *//* Label unused */ + +#include +#include + +/* The code at the top of get_date which figures out the offset of the + current time zone checks various CPP symbols to see if special + tricks are need, but defaults to using the gettimeofday system call. + Include if that will be used. */ + +# include +# include + +#if defined (__STDC__) || defined (USG) +#include +#endif + +#if defined (__STDC__) +#include +#endif + +/* NOTES on rebuilding getdate.c (particularly for inclusion in CVS + releases): + + We don't want to mess with all the portability hassles of alloca. + In particular, most (all?) versions of bison will use alloca in + their parser. If bison works on your system (e.g. it should work + with gcc), then go ahead and use it, but the more general solution + is to use byacc instead of bison, which should generate a portable + parser. I played with adding "#define alloca dont_use_alloca", to + give an error if the parser generator uses alloca (and thus detect + unportable getdate.c's), but that seems to cause as many problems + as it solves. */ + +#include + +#define yylex getdate_yylex +#define yyerror getdate_yyerror + +static int yylex(void); +static int yyerror(const char *); + +time_t get_date(char *); + +#define EPOCH 1970 +#define HOUR(x) ((time_t)(x) * 60) +#define SECSPERDAY (24L * 60L * 60L) + + +/* +** An entry in the lexical lookup table. +*/ +typedef struct _TABLE { + const char *name; + int type; + time_t value; +} TABLE; + + +/* +** Daylight-savings mode: on, off, or not yet known. +*/ +typedef enum _DSTMODE { + DSTon, DSToff, DSTmaybe +} DSTMODE; + +/* +** Meridian: am, pm, or 24-hour style. +*/ +typedef enum _MERIDIAN { + MERam, MERpm, MER24 +} MERIDIAN; + + +/* +** Global variables. We could get rid of most of these by using a good +** union as the yacc stack. (This routine was originally written before +** yacc had the %union construct.) Maybe someday; right now we only use +** the %union very rarely. +*/ +static char *yyInput; +static DSTMODE yyDSTmode; +static time_t yyDayOrdinal; +static time_t yyDayNumber; +static int yyHaveDate; +static int yyHaveDay; +static int yyHaveRel; +static int yyHaveTime; +static int yyHaveZone; +static time_t yyTimezone; +static time_t yyDay; +static time_t yyHour; +static time_t yyMinutes; +static time_t yyMonth; +static time_t yySeconds; +static time_t yyYear; +static MERIDIAN yyMeridian; +static time_t yyRelMonth; +static time_t yyRelSeconds; + +%} + +%union { + time_t Number; + enum _MERIDIAN Meridian; +} + +%token tAGO tDAY tDAYZONE tID tMERIDIAN tMINUTE_UNIT tMONTH tMONTH_UNIT +%token tSEC_UNIT tSNUMBER tUNUMBER tZONE tDST + +%type tDAY tDAYZONE tMINUTE_UNIT tMONTH tMONTH_UNIT +%type tSEC_UNIT tSNUMBER tUNUMBER tZONE +%type tMERIDIAN o_merid + +%% + +spec : /* NULL */ + | spec item + ; + +item : time { + yyHaveTime++; + } + | zone { + yyHaveZone++; + } + | date { + yyHaveDate++; + } + | day { + yyHaveDay++; + } + | rel { + yyHaveRel++; + } + | number + ; + +time : tUNUMBER tMERIDIAN { + yyHour = $1; + yyMinutes = 0; + yySeconds = 0; + yyMeridian = $2; + } + | tUNUMBER ':' tUNUMBER o_merid { + yyHour = $1; + yyMinutes = $3; + yySeconds = 0; + yyMeridian = $4; + } + | tUNUMBER ':' tUNUMBER tSNUMBER { + yyHour = $1; + yyMinutes = $3; + yyMeridian = MER24; + yyDSTmode = DSToff; + yyTimezone = - ($4 % 100 + ($4 / 100) * 60); + } + | tUNUMBER ':' tUNUMBER ':' tUNUMBER o_merid { + yyHour = $1; + yyMinutes = $3; + yySeconds = $5; + yyMeridian = $6; + } + | tUNUMBER ':' tUNUMBER ':' tUNUMBER tSNUMBER { + yyHour = $1; + yyMinutes = $3; + yySeconds = $5; + yyMeridian = MER24; + yyDSTmode = DSToff; + yyTimezone = - ($6 % 100 + ($6 / 100) * 60); + } + ; + +zone : tZONE { + yyTimezone = $1; + yyDSTmode = DSToff; + } + | tDAYZONE { + yyTimezone = $1; + yyDSTmode = DSTon; + } + | + tZONE tDST { + yyTimezone = $1; + yyDSTmode = DSTon; + } + ; + +day : tDAY { + yyDayOrdinal = 1; + yyDayNumber = $1; + } + | tDAY ',' { + yyDayOrdinal = 1; + yyDayNumber = $1; + } + | tUNUMBER tDAY { + yyDayOrdinal = $1; + yyDayNumber = $2; + } + ; + +date : tUNUMBER '/' tUNUMBER { + yyMonth = $1; + yyDay = $3; + } + | tUNUMBER '/' tUNUMBER '/' tUNUMBER { + if ($1 >= 100) { + yyYear = $1; + yyMonth = $3; + yyDay = $5; + } else { + yyMonth = $1; + yyDay = $3; + yyYear = $5; + } + } + | tUNUMBER tSNUMBER tSNUMBER { + /* ISO 8601 format. yyyy-mm-dd. */ + yyYear = $1; + yyMonth = -$2; + yyDay = -$3; + } + | tUNUMBER tMONTH tSNUMBER { + /* e.g. 17-JUN-1992. */ + yyDay = $1; + yyMonth = $2; + yyYear = -$3; + } + | tMONTH tUNUMBER { + yyMonth = $1; + yyDay = $2; + } + | tMONTH tUNUMBER ',' tUNUMBER { + yyMonth = $1; + yyDay = $2; + yyYear = $4; + } + | tUNUMBER tMONTH { + yyMonth = $2; + yyDay = $1; + } + | tUNUMBER tMONTH tUNUMBER { + yyMonth = $2; + yyDay = $1; + yyYear = $3; + } + ; + +rel : relunit tAGO { + yyRelSeconds = -yyRelSeconds; + yyRelMonth = -yyRelMonth; + } + | relunit + ; + +relunit : tUNUMBER tMINUTE_UNIT { + yyRelSeconds += $1 * $2 * 60L; + } + | tSNUMBER tMINUTE_UNIT { + yyRelSeconds += $1 * $2 * 60L; + } + | tMINUTE_UNIT { + yyRelSeconds += $1 * 60L; + } + | tSNUMBER tSEC_UNIT { + yyRelSeconds += $1; + } + | tUNUMBER tSEC_UNIT { + yyRelSeconds += $1; + } + | tSEC_UNIT { + yyRelSeconds++; + } + | tSNUMBER tMONTH_UNIT { + yyRelMonth += $1 * $2; + } + | tUNUMBER tMONTH_UNIT { + yyRelMonth += $1 * $2; + } + | tMONTH_UNIT { + yyRelMonth += $1; + } + ; + +number : tUNUMBER { + if (yyHaveTime && yyHaveDate && !yyHaveRel) + yyYear = $1; + else { + if($1>10000) { + yyHaveDate++; + yyDay= ($1)%100; + yyMonth= ($1/100)%100; + yyYear = $1/10000; + } + else { + yyHaveTime++; + if ($1 < 100) { + yyHour = $1; + yyMinutes = 0; + } + else { + yyHour = $1 / 100; + yyMinutes = $1 % 100; + } + yySeconds = 0; + yyMeridian = MER24; + } + } + } + ; + +o_merid : /* NULL */ { + $$ = MER24; + } + | tMERIDIAN { + $$ = $1; + } + ; + +%% + +/* Month and day table. */ +static TABLE const MonthDayTable[] = { + { "january", tMONTH, 1 }, + { "february", tMONTH, 2 }, + { "march", tMONTH, 3 }, + { "april", tMONTH, 4 }, + { "may", tMONTH, 5 }, + { "june", tMONTH, 6 }, + { "july", tMONTH, 7 }, + { "august", tMONTH, 8 }, + { "september", tMONTH, 9 }, + { "sept", tMONTH, 9 }, + { "october", tMONTH, 10 }, + { "november", tMONTH, 11 }, + { "december", tMONTH, 12 }, + { "sunday", tDAY, 0 }, + { "monday", tDAY, 1 }, + { "tuesday", tDAY, 2 }, + { "tues", tDAY, 2 }, + { "wednesday", tDAY, 3 }, + { "wednes", tDAY, 3 }, + { "thursday", tDAY, 4 }, + { "thur", tDAY, 4 }, + { "thurs", tDAY, 4 }, + { "friday", tDAY, 5 }, + { "saturday", tDAY, 6 }, + { NULL, 0, 0 } +}; + +/* Time units table. */ +static TABLE const UnitsTable[] = { + { "year", tMONTH_UNIT, 12 }, + { "month", tMONTH_UNIT, 1 }, + { "fortnight", tMINUTE_UNIT, 14 * 24 * 60 }, + { "week", tMINUTE_UNIT, 7 * 24 * 60 }, + { "day", tMINUTE_UNIT, 1 * 24 * 60 }, + { "hour", tMINUTE_UNIT, 60 }, + { "minute", tMINUTE_UNIT, 1 }, + { "min", tMINUTE_UNIT, 1 }, + { "second", tSEC_UNIT, 1 }, + { "sec", tSEC_UNIT, 1 }, + { NULL, 0, 0 } +}; + +/* Assorted relative-time words. */ +static TABLE const OtherTable[] = { + { "tomorrow", tMINUTE_UNIT, 1 * 24 * 60 }, + { "yesterday", tMINUTE_UNIT, -1 * 24 * 60 }, + { "today", tMINUTE_UNIT, 0 }, + { "now", tMINUTE_UNIT, 0 }, + { "last", tUNUMBER, -1 }, + { "this", tMINUTE_UNIT, 0 }, + { "next", tUNUMBER, 2 }, + { "first", tUNUMBER, 1 }, +/* { "second", tUNUMBER, 2 }, */ + { "third", tUNUMBER, 3 }, + { "fourth", tUNUMBER, 4 }, + { "fifth", tUNUMBER, 5 }, + { "sixth", tUNUMBER, 6 }, + { "seventh", tUNUMBER, 7 }, + { "eighth", tUNUMBER, 8 }, + { "ninth", tUNUMBER, 9 }, + { "tenth", tUNUMBER, 10 }, + { "eleventh", tUNUMBER, 11 }, + { "twelfth", tUNUMBER, 12 }, + { "ago", tAGO, 1 }, + { NULL, 0, 0 } +}; + +/* The timezone table. */ +/* Some of these are commented out because a time_t can't store a float. */ +static TABLE const TimezoneTable[] = { + { "gmt", tZONE, HOUR( 0) }, /* Greenwich Mean */ + { "ut", tZONE, HOUR( 0) }, /* Universal (Coordinated) */ + { "utc", tZONE, HOUR( 0) }, + { "wet", tZONE, HOUR( 0) }, /* Western European */ + { "bst", tDAYZONE, HOUR( 0) }, /* British Summer */ + { "wat", tZONE, HOUR( 1) }, /* West Africa */ + { "at", tZONE, HOUR( 2) }, /* Azores */ +#if 0 + /* For completeness. BST is also British Summer, and GST is + * also Guam Standard. */ + { "bst", tZONE, HOUR( 3) }, /* Brazil Standard */ + { "gst", tZONE, HOUR( 3) }, /* Greenland Standard */ +#endif +#if 0 + { "nft", tZONE, HOUR(3.5) }, /* Newfoundland */ + { "nst", tZONE, HOUR(3.5) }, /* Newfoundland Standard */ + { "ndt", tDAYZONE, HOUR(3.5) }, /* Newfoundland Daylight */ +#endif + { "ast", tZONE, HOUR( 4) }, /* Atlantic Standard */ + { "adt", tDAYZONE, HOUR( 4) }, /* Atlantic Daylight */ + { "est", tZONE, HOUR( 5) }, /* Eastern Standard */ + { "edt", tDAYZONE, HOUR( 5) }, /* Eastern Daylight */ + { "cst", tZONE, HOUR( 6) }, /* Central Standard */ + { "cdt", tDAYZONE, HOUR( 6) }, /* Central Daylight */ + { "mst", tZONE, HOUR( 7) }, /* Mountain Standard */ + { "mdt", tDAYZONE, HOUR( 7) }, /* Mountain Daylight */ + { "pst", tZONE, HOUR( 8) }, /* Pacific Standard */ + { "pdt", tDAYZONE, HOUR( 8) }, /* Pacific Daylight */ + { "yst", tZONE, HOUR( 9) }, /* Yukon Standard */ + { "ydt", tDAYZONE, HOUR( 9) }, /* Yukon Daylight */ + { "hst", tZONE, HOUR(10) }, /* Hawaii Standard */ + { "hdt", tDAYZONE, HOUR(10) }, /* Hawaii Daylight */ + { "cat", tZONE, HOUR(10) }, /* Central Alaska */ + { "ahst", tZONE, HOUR(10) }, /* Alaska-Hawaii Standard */ + { "nt", tZONE, HOUR(11) }, /* Nome */ + { "idlw", tZONE, HOUR(12) }, /* International Date Line West */ + { "cet", tZONE, -HOUR(1) }, /* Central European */ + { "met", tZONE, -HOUR(1) }, /* Middle European */ + { "mewt", tZONE, -HOUR(1) }, /* Middle European Winter */ + { "mest", tDAYZONE, -HOUR(1) }, /* Middle European Summer */ + { "swt", tZONE, -HOUR(1) }, /* Swedish Winter */ + { "sst", tDAYZONE, -HOUR(1) }, /* Swedish Summer */ + { "fwt", tZONE, -HOUR(1) }, /* French Winter */ + { "fst", tDAYZONE, -HOUR(1) }, /* French Summer */ + { "eet", tZONE, -HOUR(2) }, /* Eastern Europe, USSR Zone 1 */ + { "bt", tZONE, -HOUR(3) }, /* Baghdad, USSR Zone 2 */ +#if 0 + { "it", tZONE, -HOUR(3.5) },/* Iran */ +#endif + { "zp4", tZONE, -HOUR(4) }, /* USSR Zone 3 */ + { "zp5", tZONE, -HOUR(5) }, /* USSR Zone 4 */ +#if 0 + { "ist", tZONE, -HOUR(5.5) },/* Indian Standard */ +#endif + { "zp6", tZONE, -HOUR(6) }, /* USSR Zone 5 */ +#if 0 + /* For completeness. NST is also Newfoundland Stanard, and SST is + * also Swedish Summer. */ + { "nst", tZONE, -HOUR(6.5) },/* North Sumatra */ + { "sst", tZONE, -HOUR(7) }, /* South Sumatra, USSR Zone 6 */ +#endif /* 0 */ + { "wast", tZONE, -HOUR(7) }, /* West Australian Standard */ + { "wadt", tDAYZONE, -HOUR(7) }, /* West Australian Daylight */ +#if 0 + { "jt", tZONE, -HOUR(7.5) },/* Java (3pm in Cronusland!) */ +#endif + { "cct", tZONE, -HOUR(8) }, /* China Coast, USSR Zone 7 */ + { "jst", tZONE, -HOUR(9) }, /* Japan Standard, USSR Zone 8 */ +#if 0 + { "cast", tZONE, -HOUR(9.5) },/* Central Australian Standard */ + { "cadt", tDAYZONE, -HOUR(9.5) },/* Central Australian Daylight */ +#endif + { "east", tZONE, -HOUR(10) }, /* Eastern Australian Standard */ + { "eadt", tDAYZONE, -HOUR(10) }, /* Eastern Australian Daylight */ + { "gst", tZONE, -HOUR(10) }, /* Guam Standard, USSR Zone 9 */ + { "nzt", tZONE, -HOUR(12) }, /* New Zealand */ + { "nzst", tZONE, -HOUR(12) }, /* New Zealand Standard */ + { "nzdt", tDAYZONE, -HOUR(12) }, /* New Zealand Daylight */ + { "idle", tZONE, -HOUR(12) }, /* International Date Line East */ + { NULL, 0, 0 } +}; + +/* Military timezone table. */ +static TABLE const MilitaryTable[] = { + { "a", tZONE, HOUR( 1) }, + { "b", tZONE, HOUR( 2) }, + { "c", tZONE, HOUR( 3) }, + { "d", tZONE, HOUR( 4) }, + { "e", tZONE, HOUR( 5) }, + { "f", tZONE, HOUR( 6) }, + { "g", tZONE, HOUR( 7) }, + { "h", tZONE, HOUR( 8) }, + { "i", tZONE, HOUR( 9) }, + { "k", tZONE, HOUR( 10) }, + { "l", tZONE, HOUR( 11) }, + { "m", tZONE, HOUR( 12) }, + { "n", tZONE, HOUR(- 1) }, + { "o", tZONE, HOUR(- 2) }, + { "p", tZONE, HOUR(- 3) }, + { "q", tZONE, HOUR(- 4) }, + { "r", tZONE, HOUR(- 5) }, + { "s", tZONE, HOUR(- 6) }, + { "t", tZONE, HOUR(- 7) }, + { "u", tZONE, HOUR(- 8) }, + { "v", tZONE, HOUR(- 9) }, + { "w", tZONE, HOUR(-10) }, + { "x", tZONE, HOUR(-11) }, + { "y", tZONE, HOUR(-12) }, + { "z", tZONE, HOUR( 0) }, + { NULL, 0, 0 } +}; + + + + +/* ARGSUSED */ +static int +yyerror(const char *s __unused) +{ + return 0; +} + + +static time_t +ToSeconds(time_t Hours, time_t Minutes, time_t Seconds, MERIDIAN Meridian) +{ + if (Minutes < 0 || Minutes > 59 || Seconds < 0 || Seconds > 59) + return -1; + switch (Meridian) { + case MER24: + if (Hours < 0 || Hours > 23) + return -1; + return (Hours * 60L + Minutes) * 60L + Seconds; + case MERam: + if (Hours < 1 || Hours > 12) + return -1; + if (Hours == 12) + Hours = 0; + return (Hours * 60L + Minutes) * 60L + Seconds; + case MERpm: + if (Hours < 1 || Hours > 12) + return -1; + if (Hours == 12) + Hours = 0; + return ((Hours + 12) * 60L + Minutes) * 60L + Seconds; + default: + abort (); + } + /* NOTREACHED */ +} + + +/* Year is either + * A negative number, which means to use its absolute value (why?) + * A number from 0 to 99, which means a year from 1900 to 1999, or + * The actual year (>=100). */ +static time_t +Convert(time_t Month, time_t Day, time_t Year, + time_t Hours, time_t Minutes, time_t Seconds, + MERIDIAN Meridian, DSTMODE DSTmode) +{ + static int DaysInMonth[12] = { + 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + }; + time_t tod; + time_t Julian; + int i; + + if (Year < 0) + Year = -Year; + if (Year < 69) + Year += 2000; + else if (Year < 100) + Year += 1900; + DaysInMonth[1] = Year % 4 == 0 && (Year % 100 != 0 || Year % 400 == 0) + ? 29 : 28; + /* Checking for 2038 bogusly assumes that time_t is 32 bits. But + I'm too lazy to try to check for time_t overflow in another way. */ + if (Year < EPOCH || Year > 2038 + || Month < 1 || Month > 12 + /* Lint fluff: "conversion from long may lose accuracy" */ + || Day < 1 || Day > DaysInMonth[(int)--Month]) + return -1; + + for (Julian = Day - 1, i = 0; i < Month; i++) + Julian += DaysInMonth[i]; + for (i = EPOCH; i < Year; i++) + Julian += 365 + (i % 4 == 0); + Julian *= SECSPERDAY; + Julian += yyTimezone * 60L; + if ((tod = ToSeconds(Hours, Minutes, Seconds, Meridian)) < 0) + return -1; + Julian += tod; + if (DSTmode == DSTon + || (DSTmode == DSTmaybe && localtime(&Julian)->tm_isdst)) + Julian -= 60 * 60; + return Julian; +} + + +static time_t +DSTcorrect(time_t Start, time_t Future) +{ + time_t StartDay; + time_t FutureDay; + + StartDay = (localtime(&Start)->tm_hour + 1) % 24; + FutureDay = (localtime(&Future)->tm_hour + 1) % 24; + return (Future - Start) + (StartDay - FutureDay) * 60L * 60L; +} + + +static time_t +RelativeDate(time_t Start, time_t DayOrdinal, time_t DayNumber) +{ + struct tm *tm; + time_t now; + + now = Start; + tm = localtime(&now); + now += SECSPERDAY * ((DayNumber - tm->tm_wday + 7) % 7); + now += 7 * SECSPERDAY * (DayOrdinal <= 0 ? DayOrdinal : DayOrdinal - 1); + return DSTcorrect(Start, now); +} + + +static time_t +RelativeMonth(time_t Start, time_t RelMonth) +{ + struct tm *tm; + time_t Month; + time_t Year; + + if (RelMonth == 0) + return 0; + tm = localtime(&Start); + Month = 12 * (tm->tm_year + 1900) + tm->tm_mon + RelMonth; + Year = Month / 12; + Month = Month % 12 + 1; + return DSTcorrect(Start, + Convert(Month, (time_t)tm->tm_mday, Year, + (time_t)tm->tm_hour, (time_t)tm->tm_min, (time_t)tm->tm_sec, + MER24, DSTmaybe)); +} + + +static int +LookupWord(char *buff) +{ + char *p; + char *q; + const TABLE *tp; + int i; + int abbrev; + + /* Make it lowercase. */ + for (p = buff; *p; p++) + if (isupper(*p)) + *p = tolower(*p); + + if (strcmp(buff, "am") == 0 || strcmp(buff, "a.m.") == 0) { + yylval.Meridian = MERam; + return tMERIDIAN; + } + if (strcmp(buff, "pm") == 0 || strcmp(buff, "p.m.") == 0) { + yylval.Meridian = MERpm; + return tMERIDIAN; + } + + /* See if we have an abbreviation for a month. */ + if (strlen(buff) == 3) + abbrev = 1; + else if (strlen(buff) == 4 && buff[3] == '.') { + abbrev = 1; + buff[3] = '\0'; + } + else + abbrev = 0; + + for (tp = MonthDayTable; tp->name; tp++) { + if (abbrev) { + if (strncmp(buff, tp->name, 3) == 0) { + yylval.Number = tp->value; + return tp->type; + } + } + else if (strcmp(buff, tp->name) == 0) { + yylval.Number = tp->value; + return tp->type; + } + } + + for (tp = TimezoneTable; tp->name; tp++) + if (strcmp(buff, tp->name) == 0) { + yylval.Number = tp->value; + return tp->type; + } + + if (strcmp(buff, "dst") == 0) + return tDST; + + for (tp = UnitsTable; tp->name; tp++) + if (strcmp(buff, tp->name) == 0) { + yylval.Number = tp->value; + return tp->type; + } + + /* Strip off any plural and try the units table again. */ + i = strlen(buff) - 1; + if (buff[i] == 's') { + buff[i] = '\0'; + for (tp = UnitsTable; tp->name; tp++) + if (strcmp(buff, tp->name) == 0) { + yylval.Number = tp->value; + return tp->type; + } + buff[i] = 's'; /* Put back for "this" in OtherTable. */ + } + + for (tp = OtherTable; tp->name; tp++) + if (strcmp(buff, tp->name) == 0) { + yylval.Number = tp->value; + return tp->type; + } + + /* Military timezones. */ + if (buff[1] == '\0' && isalpha(*buff)) { + for (tp = MilitaryTable; tp->name; tp++) + if (strcmp(buff, tp->name) == 0) { + yylval.Number = tp->value; + return tp->type; + } + } + + /* Drop out any periods and try the timezone table again. */ + for (i = 0, p = q = buff; *q; q++) + if (*q != '.') + *p++ = *q; + else + i++; + *p = '\0'; + if (i) + for (tp = TimezoneTable; tp->name; tp++) + if (strcmp(buff, tp->name) == 0) { + yylval.Number = tp->value; + return tp->type; + } + + return tID; +} + + +static int +yylex(void) +{ + char c; + char *p; + char buff[20]; + int Count; + int sign; + + for ( ; ; ) { + while (isspace(*yyInput)) + yyInput++; + + if (isdigit(c = *yyInput) || c == '-' || c == '+') { + if (c == '-' || c == '+') { + sign = c == '-' ? -1 : 1; + if (!isdigit(*++yyInput)) + /* skip the '-' sign */ + continue; + } + else + sign = 0; + for (yylval.Number = 0; isdigit(c = *yyInput++); ) + yylval.Number = 10 * yylval.Number + c - '0'; + yyInput--; + if (sign < 0) + yylval.Number = -yylval.Number; + return sign ? tSNUMBER : tUNUMBER; + } + if (isalpha(c)) { + for (p = buff; isalpha(c = *yyInput++) || c == '.'; ) + if (p < &buff[sizeof buff - 1]) + *p++ = c; + *p = '\0'; + yyInput--; + return LookupWord(buff); + } + if (c != '(') + return *yyInput++; + Count = 0; + do { + c = *yyInput++; + if (c == '\0') + return c; + if (c == '(') + Count++; + else if (c == ')') + Count--; + } while (Count > 0); + } +} + +#define TM_YEAR_ORIGIN 1900 + +/* Yield A - B, measured in seconds. */ +static long +difftm (struct tm *a, struct tm *b) +{ + int ay = a->tm_year + (TM_YEAR_ORIGIN - 1); + int by = b->tm_year + (TM_YEAR_ORIGIN - 1); + int days = ( + /* difference in day of year */ + a->tm_yday - b->tm_yday + /* + intervening leap days */ + + ((ay >> 2) - (by >> 2)) + - (ay/100 - by/100) + + ((ay/100 >> 2) - (by/100 >> 2)) + /* + difference in years * 365 */ + + (long)(ay-by) * 365 + ); + return (60*(60*(24*days + (a->tm_hour - b->tm_hour)) + + (a->tm_min - b->tm_min)) + + (a->tm_sec - b->tm_sec)); +} + +time_t +get_date(char *p) +{ + struct tm *tm, *gmt_ptr, gmt; + int tzoff; + time_t Start; + time_t tod; + time_t nowtime; + + bzero (&gmt, sizeof(struct tm)); + yyInput = p; + + (void)time (&nowtime); + + gmt_ptr = gmtime (&nowtime); + if (gmt_ptr != NULL) + { + /* Make a copy, in case localtime modifies *tm (I think + that comment now applies to *gmt_ptr, but I am too + lazy to dig into how gmtime and locatime allocate the + structures they return pointers to). */ + gmt = *gmt_ptr; + } + + if (! (tm = localtime (&nowtime))) + return -1; + + if (gmt_ptr != NULL) + tzoff = difftm (&gmt, tm) / 60; + else + /* We are on a system like VMS, where the system clock is + in local time and the system has no concept of timezones. + Hopefully we can fake this out (for the case in which the + user specifies no timezone) by just saying the timezone + is zero. */ + tzoff = 0; + + if(tm->tm_isdst) + tzoff += 60; + + tm = localtime(&nowtime); + yyYear = tm->tm_year + 1900; + yyMonth = tm->tm_mon + 1; + yyDay = tm->tm_mday; + yyTimezone = tzoff; + yyDSTmode = DSTmaybe; + yyHour = 0; + yyMinutes = 0; + yySeconds = 0; + yyMeridian = MER24; + yyRelSeconds = 0; + yyRelMonth = 0; + yyHaveDate = 0; + yyHaveDay = 0; + yyHaveRel = 0; + yyHaveTime = 0; + yyHaveZone = 0; + + if (yyparse() + || yyHaveTime > 1 || yyHaveZone > 1 || yyHaveDate > 1 || yyHaveDay > 1) + return -1; + + if (yyHaveDate || yyHaveTime || yyHaveDay) { + Start = Convert(yyMonth, yyDay, yyYear, yyHour, yyMinutes, yySeconds, + yyMeridian, yyDSTmode); + if (Start < 0) + return -1; + } + else { + Start = nowtime; + if (!yyHaveRel) + Start -= ((tm->tm_hour * 60L + tm->tm_min) * 60L) + tm->tm_sec; + } + + Start += yyRelSeconds; + Start += RelativeMonth(Start, yyRelMonth); + + if (yyHaveDay && !yyHaveDate) { + tod = RelativeDate(Start, yyDayOrdinal, yyDayNumber); + Start += tod; + } + + /* Have to do *something* with a legitimate -1 so it's distinguishable + * from the error return value. (Alternately could set errno on error.) */ + return Start == -1 ? 0 : Start; +} + + +#if defined(TEST) + +/* ARGSUSED */ +int +main(int ac, char *av[]) +{ + char buff[128]; + time_t d; + + (void)printf("Enter date, or blank line to exit.\n\t> "); + (void)fflush(stdout); + while (gets(buff) && buff[0]) { + d = get_date(buff); + if (d == -1) + (void)printf("Bad format - couldn't convert.\n"); + else + (void)printf("%s", ctime(&d)); + (void)printf("\t> "); + (void)fflush(stdout); + } + exit(0); + /* NOTREACHED */ +} +#endif /* defined(TEST) */ diff --git a/src/find/ls.c b/src/find/ls.c new file mode 100644 index 0000000..345be28 --- /dev/null +++ b/src/find/ls.c @@ -0,0 +1,118 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1989, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "find.h" + +/* Derived from the print routines in the ls(1) source code. */ + +static void printlink(char *); +static void printtime(time_t); + +void +printlong(char *name, char *accpath, struct stat *sb) +{ + char modep[15]; + + (void)printf("%6ju %8"PRId64" ", (uintmax_t)sb->st_ino, sb->st_blocks); + (void)strmode(sb->st_mode, modep); + (void)printf("%s %3ju %-*s %-*s ", modep, (uintmax_t)sb->st_nlink, + MAXLOGNAME - 1, + user_from_uid(sb->st_uid, 0), MAXLOGNAME - 1, + group_from_gid(sb->st_gid, 0)); + + if (S_ISCHR(sb->st_mode) || S_ISBLK(sb->st_mode)) + (void)printf("%#8jx ", (uintmax_t)sb->st_rdev); + else + (void)printf("%8"PRId64" ", sb->st_size); + printtime(sb->st_mtime); + (void)printf("%s", name); + if (S_ISLNK(sb->st_mode)) + printlink(accpath); + (void)putchar('\n'); +} + +static void +printtime(time_t ftime) +{ + char longstring[80]; + static time_t lnow; + const char *format; + static int d_first = -1; + struct tm *tm; + +#ifdef D_MD_ORDER + if (d_first < 0) + d_first = (*nl_langinfo(D_MD_ORDER) == 'd'); +#endif + if (lnow == 0) + lnow = time(NULL); + +#define SIXMONTHS ((365 / 2) * 86400) + if (ftime + SIXMONTHS > lnow && ftime < lnow + SIXMONTHS) + /* mmm dd hh:mm || dd mmm hh:mm */ + format = d_first ? "%e %b %R " : "%b %e %R "; + else + /* mmm dd yyyy || dd mmm yyyy */ + format = d_first ? "%e %b %Y " : "%b %e %Y "; + if ((tm = localtime(&ftime)) != NULL) + strftime(longstring, sizeof(longstring), format, tm); + else + strlcpy(longstring, "bad date val ", sizeof(longstring)); + fputs(longstring, stdout); +} + +static void +printlink(char *name) +{ + ssize_t lnklen; + char path[MAXPATHLEN]; + + if ((lnklen = readlink(name, path, MAXPATHLEN - 1)) == -1) { + warn("%s", name); + return; + } + path[lnklen] = '\0'; + (void)printf(" -> %s", path); +} diff --git a/src/find/main.c b/src/find/main.c new file mode 100644 index 0000000..76cedd2 --- /dev/null +++ b/src/find/main.c @@ -0,0 +1,186 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Cimarron D. Taylor of the University of California, Berkeley. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "find.h" + +time_t now; /* time find was run */ +int dotfd; /* starting directory */ +int ftsoptions; /* options for the ftsopen(3) call */ +int ignore_readdir_race; /* ignore readdir race */ +int isdepth; /* do directories on post-order visit */ +int isoutput; /* user specified output operator */ +int issort; /* do hierarchies in lexicographical order */ +int isxargs; /* don't permit xargs delimiting chars */ +int daystart; /* adjust times relative to start of today */ +int mindepth = -1, maxdepth = -1; /* minimum and maximum depth */ +int regexp_flags = REG_BASIC; /* use the "basic" regexp by default*/ +int exitstatus; +volatile sig_atomic_t showinfo = 0; + +static void usage(void) __dead2; +static void siginfo_handler(int sig __unused); + +int +find_main(int argc, char *argv[]) +{ + char **p, **start; + int Hflag, Lflag, ch; + int ret; + + (void)setlocale(LC_ALL, ""); + + /* Reset global state for multicall use */ + now = 0; + if (dotfd >= 0) close(dotfd); + dotfd = -1; + ftsoptions = 0; + ignore_readdir_race = 0; + isdepth = 0; + isoutput = 0; + issort = 0; + isxargs = 0; + daystart = 0; + mindepth = -1; + maxdepth = -1; + regexp_flags = REG_BASIC; + exitstatus = 0; + showinfo = 0; + optind = 1; + + (void)time(&now); /* initialize the time-of-day */ + + (void)signal(SIGINFO, siginfo_handler); + + p = start = argv; + Hflag = Lflag = 0; + ftsoptions = FTS_NOSTAT | FTS_PHYSICAL; + while ((ch = getopt(argc, argv, "+EHLPXdf:sx")) != -1) + switch (ch) { + case 'E': + regexp_flags |= REG_EXTENDED; + break; + case 'H': + Hflag = 1; + Lflag = 0; + break; + case 'L': + Lflag = 1; + Hflag = 0; + break; + case 'P': + Hflag = Lflag = 0; + break; + case 'X': + isxargs = 1; + break; + case 'd': + isdepth = 1; + break; + case 'f': + *p++ = optarg; + break; + case 's': + issort = 1; + break; + case 'x': + ftsoptions |= FTS_XDEV; + break; + case '?': + default: + usage(); + } + + argc -= optind; + argv += optind; + + if (Hflag) + ftsoptions |= FTS_COMFOLLOW; + if (Lflag) { + ftsoptions &= ~FTS_PHYSICAL; + ftsoptions |= FTS_LOGICAL; + } + + /* + * Find first option to delimit the file list. The first argument + * that starts with a -, or is a ! or a ( must be interpreted as a + * part of the find expression, according to POSIX .2. + */ + for (; *argv != NULL; *p++ = *argv++) { + if (argv[0][0] == '-') + break; + if ((argv[0][0] == '!' || argv[0][0] == '(') && + argv[0][1] == '\0') + break; + } + + if (p == start) + usage(); + *p = NULL; + + if ((dotfd = open(".", O_RDONLY | O_CLOEXEC, 0)) < 0) + ftsoptions |= FTS_NOCHDIR; + + ret = find_execute(find_formplan(argv), start); + if (dotfd >= 0) { close(dotfd); dotfd = -1; } + return ret; +} + +static void +usage(void) +{ + (void)fprintf(stderr, "%s\n%s\n", +"usage: find [-H | -L | -P] [-EXdsx] [-f path] path ... [expression]", +" find [-H | -L | -P] [-EXdsx] -f path [path ...] [expression]"); + exit(1); +} + +static void +siginfo_handler(int sig __unused) +{ + showinfo = 1; +} diff --git a/src/find/misc.c b/src/find/misc.c new file mode 100644 index 0000000..7963e3b --- /dev/null +++ b/src/find/misc.c @@ -0,0 +1,107 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Cimarron D. Taylor of the University of California, Berkeley. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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 +#include + +#include +#include +#include +#include +#include +#include + +#include "find.h" + +/* + * brace_subst -- + * Replace occurrences of {} in s1 with s2 and return the result string. + */ +void +brace_subst(char *orig, char **store, char *path, size_t len) +{ + const char *pastorigend, *p, *q; + char *dst; + size_t newlen, plen; + + plen = strlen(path); + newlen = strlen(orig) + 1; + pastorigend = orig + newlen; + for (p = orig; (q = strstr(p, "{}")) != NULL; p = q + 2) { + if (plen > 2 && newlen + plen - 2 < newlen) + errx(2, "brace_subst overflow"); + newlen += plen - 2; + } + if (newlen > len) { + *store = reallocf(*store, newlen); + if (*store == NULL) + err(2, NULL); + } + dst = *store; + for (p = orig; (q = strstr(p, "{}")) != NULL; p = q + 2) { + memcpy(dst, p, q - p); + dst += q - p; + memcpy(dst, path, plen); + dst += plen; + } + memcpy(dst, p, pastorigend - p); +} + +/* + * queryuser -- + * print a message to standard error and then read input from standard + * input. If the input is an affirmative response (according to the + * current locale) then 1 is returned. + */ +int +queryuser(char *argv[]) +{ + char *p, resp[256]; + + (void)fprintf(stderr, "\"%s", *argv); + while (*++argv) + (void)fprintf(stderr, " %s", *argv); + (void)fprintf(stderr, "\"? "); + (void)fflush(stderr); + + if (fgets(resp, sizeof(resp), stdin) == NULL) + *resp = '\0'; + if ((p = strchr(resp, '\n')) != NULL) + *p = '\0'; + else { + (void)fprintf(stderr, "\n"); + (void)fflush(stderr); + } + return (rpmatch(resp) == 1); +} diff --git a/src/find/operator.c b/src/find/operator.c new file mode 100644 index 0000000..3b4936f --- /dev/null +++ b/src/find/operator.c @@ -0,0 +1,267 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Cimarron D. Taylor of the University of California, Berkeley. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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 + +#include +#include +#include +#include + +#include "find.h" + +static PLAN *yanknode(PLAN **); +static PLAN *yankexpr(PLAN **); + +/* + * yanknode -- + * destructively removes the top from the plan + */ +static PLAN * +yanknode(PLAN **planp) +{ + PLAN *node; /* top node removed from the plan */ + + if ((node = (*planp)) == NULL) + return (NULL); + (*planp) = (*planp)->next; + node->next = NULL; + return (node); +} + +/* + * yankexpr -- + * Removes one expression from the plan. This is used mainly by + * paren_squish. In comments below, an expression is either a + * simple node or a f_expr node containing a list of simple nodes. + */ +static PLAN * +yankexpr(PLAN **planp) +{ + PLAN *next; /* temp node holding subexpression results */ + PLAN *node; /* pointer to returned node or expression */ + PLAN *tail; /* pointer to tail of subplan */ + PLAN *subplan; /* pointer to head of ( ) expression */ + + /* first pull the top node from the plan */ + if ((node = yanknode(planp)) == NULL) + return (NULL); + + /* + * If the node is an '(' then we recursively slurp up expressions + * until we find its associated ')'. If it's a closing paren we + * just return it and unwind our recursion; all other nodes are + * complete expressions, so just return them. + */ + if (node->execute == f_openparen) + for (tail = subplan = NULL;;) { + if ((next = yankexpr(planp)) == NULL) + errx(1, "(: missing closing ')'"); + /* + * If we find a closing ')' we store the collected + * subplan in our '(' node and convert the node to + * a f_expr. The ')' we found is ignored. Otherwise, + * we just continue to add whatever we get to our + * subplan. + */ + if (next->execute == f_closeparen) { + if (subplan == NULL) + errx(1, "(): empty inner expression"); + node->p_data[0] = subplan; + node->execute = f_expr; + break; + } else { + if (subplan == NULL) + tail = subplan = next; + else { + tail->next = next; + tail = next; + } + tail->next = NULL; + } + } + return (node); +} + +/* + * paren_squish -- + * replaces "parenthesized" plans in our search plan with "expr" nodes. + */ +PLAN * +paren_squish(PLAN *plan) +{ + PLAN *expr; /* pointer to next expression */ + PLAN *tail; /* pointer to tail of result plan */ + PLAN *result; /* pointer to head of result plan */ + + result = tail = NULL; + + /* + * the basic idea is to have yankexpr do all our work and just + * collect its results together. + */ + while ((expr = yankexpr(&plan)) != NULL) { + /* + * if we find an unclaimed ')' it means there is a missing + * '(' someplace. + */ + if (expr->execute == f_closeparen) + errx(1, "): no beginning '('"); + + /* add the expression to our result plan */ + if (result == NULL) + tail = result = expr; + else { + tail->next = expr; + tail = expr; + } + tail->next = NULL; + } + return (result); +} + +/* + * not_squish -- + * compresses "!" expressions in our search plan. + */ +PLAN * +not_squish(PLAN *plan) +{ + PLAN *next; /* next node being processed */ + PLAN *node; /* temporary node used in f_not processing */ + PLAN *tail; /* pointer to tail of result plan */ + PLAN *result; /* pointer to head of result plan */ + + tail = result = NULL; + + while ((next = yanknode(&plan))) { + /* + * if we encounter a ( expression ) then look for nots in + * the expr subplan. + */ + if (next->execute == f_expr) + next->p_data[0] = not_squish(next->p_data[0]); + + /* + * if we encounter a not, then snag the next node and place + * it in the not's subplan. As an optimization we compress + * several not's to zero or one not. + */ + if (next->execute == f_not) { + int notlevel = 1; + + node = yanknode(&plan); + while (node != NULL && node->execute == f_not) { + ++notlevel; + node = yanknode(&plan); + } + if (node == NULL) + errx(1, "!: no following expression"); + if (node->execute == f_or) + errx(1, "!: nothing between ! and -o"); + /* + * If we encounter ! ( expr ) then look for nots in + * the expr subplan. + */ + if (node->execute == f_expr) + node->p_data[0] = not_squish(node->p_data[0]); + if (notlevel % 2 != 1) + next = node; + else + next->p_data[0] = node; + } + + /* add the node to our result plan */ + if (result == NULL) + tail = result = next; + else { + tail->next = next; + tail = next; + } + tail->next = NULL; + } + return (result); +} + +/* + * or_squish -- + * compresses -o expressions in our search plan. + */ +PLAN * +or_squish(PLAN *plan) +{ + PLAN *next; /* next node being processed */ + PLAN *tail; /* pointer to tail of result plan */ + PLAN *result; /* pointer to head of result plan */ + + tail = result = next = NULL; + + while ((next = yanknode(&plan)) != NULL) { + /* + * if we encounter a ( expression ) then look for or's in + * the expr subplan. + */ + if (next->execute == f_expr) + next->p_data[0] = or_squish(next->p_data[0]); + + /* if we encounter a not then look for or's in the subplan */ + if (next->execute == f_not) + next->p_data[0] = or_squish(next->p_data[0]); + + /* + * if we encounter an or, then place our collected plan in the + * or's first subplan and then recursively collect the + * remaining stuff into the second subplan and return the or. + */ + if (next->execute == f_or) { + if (result == NULL) + errx(1, "-o: no expression before -o"); + next->p_data[0] = result; + next->p_data[1] = or_squish(plan); + if (next->p_data[1] == NULL) + errx(1, "-o: no expression after -o"); + return (next); + } + + /* add the node to our result plan */ + if (result == NULL) + tail = result = next; + else { + tail->next = next; + tail = next; + } + tail->next = NULL; + } + return (result); +} diff --git a/src/find/option.c b/src/find/option.c new file mode 100644 index 0000000..b16c039 --- /dev/null +++ b/src/find/option.c @@ -0,0 +1,203 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Cimarron D. Taylor of the University of California, Berkeley. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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 +#include + +#include +#include +#include +#include +#include +#include + +#include "find.h" + +static int typecompare(const void *, const void *); + +/* NB: the following table must be sorted lexically. */ +/* Options listed with C++ comments are in gnu find, but not our find */ +static OPTION const options[] = { + { "!", c_simple, f_not, 0 }, + { "(", c_simple, f_openparen, 0 }, + { ")", c_simple, f_closeparen, 0 }, +#if HAVE_STRUCT_STAT_ST_BIRTHTIME + { "-Bmin", c_Xmin, f_Xmin, F_TIME_B }, + { "-Bnewer", c_newer, f_newer, F_TIME_B }, + { "-Btime", c_Xtime, f_Xtime, F_TIME_B }, +#endif + { "-a", c_and, NULL, 0 }, +#if defined(ACL_TYPE_NFS4) || defined(__linux__) + { "-acl", c_acl, f_acl, 0 }, +#endif + { "-amin", c_Xmin, f_Xmin, F_TIME_A }, + { "-and", c_and, NULL, 0 }, + { "-anewer", c_newer, f_newer, F_TIME_A }, + { "-atime", c_Xtime, f_Xtime, F_TIME_A }, + { "-cmin", c_Xmin, f_Xmin, F_TIME_C }, + { "-cnewer", c_newer, f_newer, F_TIME_C }, + { "-ctime", c_Xtime, f_Xtime, F_TIME_C }, + { "-d", c_depth, f_depth, 0 }, + { "-daystart", c_daystart, f_always_true, 0 }, + { "-delete", c_delete, f_delete, 0 }, + { "-depth", c_depth, f_depth, 0 }, + { "-empty", c_empty, f_empty, 0 }, + { "-exec", c_exec, f_exec, 0 }, + { "-execdir", c_exec, f_exec, F_EXECDIR }, + { "-executable", c_simple, f_executable, 0 }, + { "-false", c_simple, f_false, 0 }, +#if HAVE_STRUCT_STAT_ST_FLAGS + { "-flags", c_flags, f_flags, 0 }, +#endif + { "-follow", c_follow, f_always_true, 0 }, +#if HAVE_STRUCT_STATFS_F_FSTYPENAME || defined(__linux__) + { "-fstype", c_fstype, f_fstype, 0 }, +#endif + { "-gid", c_group, f_group, 0 }, + { "-group", c_group, f_group, 0 }, + { "-ignore_readdir_race",c_ignore_readdir_race, f_always_true,0 }, + { "-ilname", c_name, f_name, F_LINK | F_IGNCASE }, + { "-iname", c_name, f_name, F_IGNCASE }, + { "-inum", c_inum, f_inum, 0 }, + { "-ipath", c_name, f_path, F_IGNCASE }, + { "-iregex", c_regex, f_regex, F_IGNCASE }, + { "-iwholename",c_name, f_path, F_IGNCASE }, + { "-links", c_links, f_links, 0 }, + { "-lname", c_name, f_name, F_LINK }, + { "-ls", c_ls, f_ls, 0 }, + { "-maxdepth", c_mXXdepth, f_always_true, F_MAXDEPTH }, + { "-mindepth", c_mXXdepth, f_always_true, 0 }, + { "-mmin", c_Xmin, f_Xmin, 0 }, + { "-mnewer", c_newer, f_newer, 0 }, + { "-mount", c_xdev, f_always_true, 0 }, + { "-mtime", c_Xtime, f_Xtime, 0 }, + { "-name", c_name, f_name, 0 }, + { "-newer", c_newer, f_newer, 0 }, +#if HAVE_STRUCT_STAT_ST_BIRTHTIME + { "-newerBB", c_newer, f_newer, F_TIME_B | F_TIME2_B }, + { "-newerBa", c_newer, f_newer, F_TIME_B | F_TIME2_A }, + { "-newerBc", c_newer, f_newer, F_TIME_B | F_TIME2_C }, + { "-newerBm", c_newer, f_newer, F_TIME_B }, + { "-newerBt", c_newer, f_newer, F_TIME_B | F_TIME2_T }, + { "-neweraB", c_newer, f_newer, F_TIME_A | F_TIME2_B }, +#endif + { "-neweraa", c_newer, f_newer, F_TIME_A | F_TIME2_A }, + { "-newerac", c_newer, f_newer, F_TIME_A | F_TIME2_C }, + { "-neweram", c_newer, f_newer, F_TIME_A }, + { "-newerat", c_newer, f_newer, F_TIME_A | F_TIME2_T }, +#if HAVE_STRUCT_STAT_ST_BIRTHTIME + { "-newercB", c_newer, f_newer, F_TIME_C | F_TIME2_B }, +#endif + { "-newerca", c_newer, f_newer, F_TIME_C | F_TIME2_A }, + { "-newercc", c_newer, f_newer, F_TIME_C | F_TIME2_C }, + { "-newercm", c_newer, f_newer, F_TIME_C }, + { "-newerct", c_newer, f_newer, F_TIME_C | F_TIME2_T }, +#if HAVE_STRUCT_STAT_ST_BIRTHTIME + { "-newermB", c_newer, f_newer, F_TIME2_B }, +#endif + { "-newerma", c_newer, f_newer, F_TIME2_A }, + { "-newermc", c_newer, f_newer, F_TIME2_C }, + { "-newermm", c_newer, f_newer, 0 }, + { "-newermt", c_newer, f_newer, F_TIME2_T }, + { "-nogroup", c_nogroup, f_nogroup, 0 }, + { "-noignore_readdir_race",c_ignore_readdir_race, f_always_true,0 }, + { "-noleaf", c_simple, f_always_true, 0 }, + { "-not", c_simple, f_not, 0 }, + { "-nouser", c_nouser, f_nouser, 0 }, + { "-o", c_simple, f_or, 0 }, + { "-ok", c_exec, f_exec, F_NEEDOK }, + { "-okdir", c_exec, f_exec, F_NEEDOK | F_EXECDIR }, + { "-or", c_simple, f_or, 0 }, + { "-path", c_name, f_path, 0 }, + { "-perm", c_perm, f_perm, 0 }, + { "-print", c_print, f_print, 0 }, + { "-print0", c_print, f_print0, 0 }, + { "-printf", c_printf, f_printf, 0 }, + { "-prune", c_simple, f_prune, 0 }, + { "-quit", c_simple, f_quit, 0 }, + { "-readable", c_simple, f_readable, 0 }, + { "-regex", c_regex, f_regex, 0 }, + { "-samefile", c_samefile, f_inum, 0 }, + { "-size", c_size, f_size, 0 }, + { "-sparse", c_sparse, f_sparse, 0 }, + { "-true", c_simple, f_always_true, 0 }, + { "-type", c_type, f_type, 0 }, + { "-uid", c_user, f_user, 0 }, + { "-user", c_user, f_user, 0 }, + { "-wholename", c_name, f_path, 0 }, + { "-writable", c_simple, f_writable, 0 }, + { "-xdev", c_xdev, f_always_true, 0 }, +}; + +/* + * find_create -- + * create a node corresponding to a command line argument. + * + * TODO: + * add create/process function pointers to node, so we can skip + * this switch stuff. + */ +PLAN * +find_create(char ***argvp) +{ + OPTION *p; + PLAN *new; + char **argv; + + argv = *argvp; + + if ((p = lookup_option(*argv)) == NULL) + errx(1, "%s: unknown primary or operator", *argv); + ++argv; + + new = (p->create)(p, &argv); + *argvp = argv; + return (new); +} + +OPTION * +lookup_option(const char *name) +{ + OPTION tmp; + + tmp.name = name; + return ((OPTION *)bsearch(&tmp, options, + sizeof(options)/sizeof(OPTION), sizeof(OPTION), typecompare)); +} + +static int +typecompare(const void *a, const void *b) +{ + return (strcmp(((const OPTION *)a)->name, ((const OPTION *)b)->name)); +} diff --git a/src/grep/file.c b/src/grep/file.c new file mode 100644 index 0000000..7751740 --- /dev/null +++ b/src/grep/file.c @@ -0,0 +1,253 @@ +/* $NetBSD: file.c,v 1.5 2011/02/16 18:35:39 joerg Exp $ */ +/* $OpenBSD: file.c,v 1.11 2010/07/02 20:48:48 nicm Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 1999 James Howard and Dag-Erling Smørgrav + * Copyright (C) 2008-2010 Gabor Kovesdan + * Copyright (C) 2010 Dimitry Andric + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "grep.h" + +#define MAXBUFSIZ (32 * 1024) +#define LNBUFBUMP 80 + +static char *buffer; +static char *bufpos; +static size_t bufrem; +static size_t fsiz; + +static char *lnbuf; +static size_t lnbuflen; + +static inline int +grep_refill(struct file *f) +{ + ssize_t nr; + + if (filebehave == FILE_MMAP) + return (0); + + bufpos = buffer; + bufrem = 0; + + nr = read(f->fd, buffer, MAXBUFSIZ); + if (nr < 0 && errno == EISDIR) + nr = 0; + if (nr < 0) + return (-1); + + bufrem = nr; + return (0); +} + +static inline int +grep_lnbufgrow(size_t newlen) +{ + + if (lnbuflen < newlen) { + lnbuf = grep_realloc(lnbuf, newlen); + lnbuflen = newlen; + } + + return (0); +} + +char * +grep_fgetln(struct file *f, struct parsec *pc) +{ + char *p; + size_t len; + size_t off; + ptrdiff_t diff; + + /* Fill the buffer, if necessary */ + if (bufrem == 0 && grep_refill(f) != 0) + goto error; + + if (bufrem == 0) { + /* Return zero length to indicate EOF */ + pc->ln.len= 0; + return (bufpos); + } + + /* Look for a newline in the remaining part of the buffer */ + if ((p = memchr(bufpos, fileeol, bufrem)) != NULL) { + ++p; /* advance over newline */ + len = p - bufpos; + if (grep_lnbufgrow(len + 1)) + goto error; + memcpy(lnbuf, bufpos, len); + bufrem -= len; + bufpos = p; + pc->ln.len = len; + lnbuf[len] = '\0'; + return (lnbuf); + } + + /* We have to copy the current buffered data to the line buffer */ + for (len = bufrem, off = 0; ; len += bufrem) { + /* Make sure there is room for more data */ + if (grep_lnbufgrow(len + LNBUFBUMP)) + goto error; + memcpy(lnbuf + off, bufpos, len - off); + /* With FILE_MMAP, this is EOF; there's no more to refill */ + if (filebehave == FILE_MMAP) { + bufrem -= len; + break; + } + off = len; + /* Fetch more to try and find EOL/EOF */ + if (grep_refill(f) != 0) + goto error; + if (bufrem == 0) + /* EOF: return partial line */ + break; + if ((p = memchr(bufpos, fileeol, bufrem)) == NULL) + continue; + /* got it: finish up the line (like code above) */ + ++p; + diff = p - bufpos; + len += diff; + if (grep_lnbufgrow(len + 1)) + goto error; + memcpy(lnbuf + off, bufpos, diff); + bufrem -= diff; + bufpos = p; + break; + } + pc->ln.len = len; + lnbuf[len] = '\0'; + return (lnbuf); + +error: + pc->ln.len = 0; + return (NULL); +} + +/* + * Opens a file for processing. + */ +struct file * +grep_open(const char *path) +{ + struct file *f; + + f = grep_malloc(sizeof *f); + memset(f, 0, sizeof *f); + if (path == NULL) { + /* Processing stdin implies --line-buffered. */ + lbflag = true; + f->fd = STDIN_FILENO; + } else if ((f->fd = open(path, O_RDONLY)) == -1) + goto error1; + + if (filebehave == FILE_MMAP) { + struct stat st; + + if (fstat(f->fd, &st) == -1 || !S_ISREG(st.st_mode)) + filebehave = FILE_STDIO; + else { + int flags = MAP_PRIVATE; +#ifdef MAP_NOCORE + flags |= MAP_NOCORE; +#endif +#ifdef MAP_NOSYNC + flags |= MAP_NOSYNC; +#endif +#ifdef MAP_PREFAULT_READ + flags |= MAP_PREFAULT_READ; +#endif + fsiz = st.st_size; + buffer = mmap(NULL, fsiz, PROT_READ, flags, + f->fd, (off_t)0); + if (buffer == MAP_FAILED) + filebehave = FILE_STDIO; + else { + bufrem = st.st_size; + bufpos = buffer; + madvise(buffer, st.st_size, MADV_SEQUENTIAL); + } + } + } + + if ((buffer == NULL) || (buffer == MAP_FAILED)) + buffer = grep_malloc(MAXBUFSIZ); + + /* Fill read buffer, also catches errors early */ + if (bufrem == 0 && grep_refill(f) != 0) + goto error2; + + /* Check for binary stuff, if necessary */ + if (binbehave != BINFILE_TEXT && fileeol != '\0' && + memchr(bufpos, '\0', bufrem) != NULL) + f->binary = true; + + return (f); + +error2: + close(f->fd); +error1: + free(f); + return (NULL); +} + +/* + * Closes a file. + */ +void +grep_close(struct file *f) +{ + + close(f->fd); + + /* Reset read buffer and line buffer */ + if (filebehave == FILE_MMAP) { + munmap(buffer, fsiz); + buffer = NULL; + } + bufpos = buffer; + bufrem = 0; + + free(lnbuf); + lnbuf = NULL; + lnbuflen = 0; +} diff --git a/src/grep/grep.1 b/src/grep/grep.1 new file mode 100644 index 0000000..04159cc --- /dev/null +++ b/src/grep/grep.1 @@ -0,0 +1,594 @@ +.\" $NetBSD: grep.1,v 1.2 2011/02/16 01:31:33 joerg Exp $ +.\" $OpenBSD: grep.1,v 1.38 2010/04/05 06:30:59 jmc Exp $ +.\" Copyright (c) 1980, 1990, 1993 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. +.\" +.\" @(#)grep.1 8.3 (Berkeley) 4/18/94 +.\" +.Dd December 18, 2022 +.Dt GREP 1 +.Os +.Sh NAME +.Nm grep , +.Nm egrep , +.Nm fgrep , +.Nm rgrep +.Nd file pattern searcher +.Sh SYNOPSIS +.Nm grep +.Bk -words +.Op Fl abcdDEFGHhIiLlmnOopqRSsUVvwxz +.Op Fl A Ar num +.Op Fl B Ar num +.Op Fl C Ar num +.Op Fl e Ar pattern +.Op Fl f Ar file +.Op Fl Fl binary-files= Ns Ar value +.Op Fl Fl color Ns Op Cm = Ns Ar when +.Op Fl Fl colour Ns Op Cm = Ns Ar when +.Op Fl Fl context= Ns Ar num +.Op Fl Fl label +.Op Fl Fl line-buffered +.Op Fl Fl null +.Op Ar pattern +.Op Ar +.Ek +.Sh DESCRIPTION +The +.Nm grep +utility searches any given input files, +selecting lines that match one or more patterns. +By default, a pattern matches an input line if the regular expression +(RE) in the pattern matches the input line +without its trailing newline. +An empty expression matches every line. +Each input line that matches at least one of the patterns is written +to the standard output. +.Pp +.Nm grep +is used for simple patterns and +basic regular expressions +.Pq BREs ; +.Nm egrep +can handle extended regular expressions +.Pq EREs . +See +.Xr re_format 7 +for more information on regular expressions. +.Nm fgrep +is quicker than both +.Nm grep +and +.Nm egrep , +but can only handle fixed patterns +(i.e., it does not interpret regular expressions). +Patterns may consist of one or more lines, +allowing any of the pattern lines to match a portion of the input. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl A Ar num , Fl Fl after-context= Ns Ar num +Print +.Ar num +lines of trailing context after each match. +See also the +.Fl B +and +.Fl C +options. +.It Fl a , Fl Fl text +Treat all files as ASCII text. +Normally +.Nm +will simply print +.Dq Binary file ... matches +if files contain binary characters. +Use of this option forces +.Nm +to output lines matching the specified pattern. +.It Fl B Ar num , Fl Fl before-context= Ns Ar num +Print +.Ar num +lines of leading context before each match. +See also the +.Fl A +and +.Fl C +options. +.It Fl b , Fl Fl byte-offset +The offset in bytes of a matched pattern is +displayed in front of the respective matched line. +.It Fl C Ar num , Fl Fl context= Ns Ar num +Print +.Ar num +lines of leading and trailing context surrounding each match. +See also the +.Fl A +and +.Fl B +options. +.It Fl c , Fl Fl count +Only a count of selected lines is written to standard output. +.It Fl Fl colour= Ns Oo Ar when Oc , Fl Fl color= Ns Oo Ar when Oc +Mark up the matching text with the expression stored in the +.Ev GREP_COLOR +environment variable. +The possible values of +.Ar when +are +.Dq Cm never , +.Dq Cm always +and +.Dq Cm auto . +.It Fl D Ar action , Fl Fl devices= Ns Ar action +Specify the demanded +.Ar action +for devices, FIFOs and sockets. +The default +.Ar action +is +.Dq Cm read , +which means, that they are read as if they were normal files. +If the +.Ar action +is set to +.Dq Cm skip , +devices are silently skipped. +.It Fl d Ar action , Fl Fl directories= Ns Ar action +Specify the demanded +.Ar action +for directories. +It is +.Dq Cm read +by default, which means that the directories +are read in the same manner as normal files. +Other possible values are +.Dq Cm skip +to silently ignore the directories, and +.Dq Cm recurse +to read them recursively, which has the same effect as the +.Fl R +and +.Fl r +option. +.It Fl E , Fl Fl extended-regexp +Interpret +.Ar pattern +as an extended regular expression +(i.e., force +.Nm grep +to behave as +.Nm egrep ) . +.It Fl e Ar pattern , Fl Fl regexp= Ns Ar pattern +Specify a +.Ar pattern +used during the search of the input: +an input line is selected if it matches any of the specified patterns. +This option is most useful when multiple +.Fl e +options are used to specify multiple patterns, +or when a +.Ar pattern +begins with a dash +.Pq Sq - . +.It Fl Fl exclude Ar pattern +If specified, it excludes files matching the given +filename +.Ar pattern +from the search. +Note that +.Fl Fl exclude +and +.Fl Fl include +patterns are processed in the order given. +If a name matches multiple patterns, the latest matching rule wins. +If no +.Fl Fl include +pattern is specified, all files are searched that are +not excluded. +Patterns are matched to the full path specified, +not only to the filename component. +.It Fl Fl exclude-dir Ar pattern +If +.Fl R +is specified, it excludes directories matching the +given filename +.Ar pattern +from the search. +Note that +.Fl Fl exclude-dir +and +.Fl Fl include-dir +patterns are processed in the order given. +If a name matches multiple patterns, the latest matching rule wins. +If no +.Fl Fl include-dir +pattern is specified, all directories are searched that are +not excluded. +.It Fl F , Fl Fl fixed-strings +Interpret +.Ar pattern +as a set of fixed strings +(i.e., force +.Nm grep +to behave as +.Nm fgrep ) . +.It Fl f Ar file , Fl Fl file= Ns Ar file +Read one or more newline separated patterns from +.Ar file . +Empty pattern lines match every input line. +Newlines are not considered part of a pattern. +If +.Ar file +is empty, nothing is matched. +.It Fl G , Fl Fl basic-regexp +Interpret +.Ar pattern +as a basic regular expression +(i.e., force +.Nm grep +to behave as traditional +.Nm grep ) . +.It Fl H +Always print filename headers with output lines. +.It Fl h , Fl Fl no-filename +Never print filename headers +.Pq i.e., filenames +with output lines. +.It Fl Fl help +Print a brief help message. +.It Fl I +Ignore binary files. +This option is equivalent to the +.Dq Fl Fl binary-files= Ns Cm without-match +option. +.It Fl i , Fl Fl ignore-case +Perform case insensitive matching. +By default, +.Nm grep +is case sensitive. +.It Fl Fl include Ar pattern +If specified, only files matching the given filename +.Ar pattern +are searched. +Note that +.Fl Fl include +and +.Fl Fl exclude +patterns are processed in the order given. +If a name matches multiple patterns, the latest matching rule wins. +Patterns are matched to the full path specified, +not only to the filename component. +.It Fl Fl include-dir Ar pattern +If +.Fl R +is specified, only directories matching the given filename +.Ar pattern +are searched. +Note that +.Fl Fl include-dir +and +.Fl Fl exclude-dir +patterns are processed in the order given. +If a name matches multiple patterns, the latest matching rule wins. +.It Fl L , Fl Fl files-without-match +Only the names of files not containing selected lines are written to +standard output. +Pathnames are listed once per file searched. +If the standard input is searched, the string +.Dq (standard input) +is written unless a +.Fl Fl label +is specified. +.It Fl l , Fl Fl files-with-matches +Only the names of files containing selected lines are written to +standard output. +.Nm grep +will only search a file until a match has been found, +making searches potentially less expensive. +Pathnames are listed once per file searched. +If the standard input is searched, the string +.Dq (standard input) +is written unless a +.Fl Fl label +is specified. +.It Fl Fl label +Label to use in place of +.Dq (standard input) +for a file name where a file name would normally be printed. +This option applies to +.Fl H , +.Fl L , +and +.Fl l . +.It Fl Fl mmap +Use +.Xr mmap 2 +instead of +.Xr read 2 +to read input, which can result in better performance under some +circumstances but can cause undefined behaviour. +.It Fl m Ar num , Fl Fl max-count= Ns Ar num +Stop reading the file after +.Ar num +matches. +.It Fl n , Fl Fl line-number +Each output line is preceded by its relative line number in the file, +starting at line 1. +The line number counter is reset for each file processed. +This option is ignored if +.Fl c , +.Fl L , +.Fl l , +or +.Fl q +is +specified. +.It Fl Fl null +Prints a zero-byte after the file name. +.It Fl O +If +.Fl R +is specified, follow symbolic links only if they were explicitly listed +on the command line. +The default is not to follow symbolic links. +.It Fl o , Fl Fl only-matching +Prints only the matching part of the lines. +.It Fl p +If +.Fl R +is specified, no symbolic links are followed. +This is the default. +.It Fl q , Fl Fl quiet , Fl Fl silent +Quiet mode: +suppress normal output. +.Nm grep +will only search a file until a match has been found, +making searches potentially less expensive. +.It Fl R , Fl r , Fl Fl recursive +Recursively search subdirectories listed. +(i.e., force +.Nm grep +to behave as +.Nm rgrep ) . +.It Fl S +If +.Fl R +is specified, all symbolic links are followed. +The default is not to follow symbolic links. +.It Fl s , Fl Fl no-messages +Silent mode. +Nonexistent and unreadable files are ignored +(i.e., their error messages are suppressed). +.It Fl U , Fl Fl binary +Search binary files, but do not attempt to print them. +.It Fl u +This option has no effect and is provided only for compatibility with GNU grep. +.It Fl V , Fl Fl version +Display version information and exit. +.It Fl v , Fl Fl invert-match +Selected lines are those +.Em not +matching any of the specified patterns. +.It Fl w , Fl Fl word-regexp +The expression is searched for as a word (as if surrounded by +.Sq [[:<:]] +and +.Sq [[:>:]] ; +see +.Xr re_format 7 ) . +This option has no effect if +.Fl x +is also specified. +.It Fl x , Fl Fl line-regexp +Only input lines selected against an entire fixed string or regular +expression are considered to be matching lines. +.It Fl y +Equivalent to +.Fl i . +Obsoleted. +.It Fl z , Fl Fl null-data +Treat input and output data as sequences of lines terminated by a +zero-byte instead of a newline. +.It Fl Fl binary-files= Ns Ar value +Controls searching and printing of binary files. +Options are: +.Bl -tag -compact -width "binary (default)" +.It Cm binary No (default) +Search binary files but do not print them. +.It Cm without-match +Do not search binary files. +.It Cm text +Treat all files as text. +.El +.It Fl Fl line-buffered +Force output to be line buffered. +By default, output is line buffered when standard output is a terminal +and block buffered otherwise. +.El +.Pp +If no file arguments are specified, the standard input is used. +Additionally, +.Dq Cm - +may be used in place of a file name, anywhere that a file name is accepted, to +read from standard input. +This includes both +.Fl f +and file arguments. +.Sh ENVIRONMENT +The following environment variables affect the execution of +.Nm : +.Bl -tag -width "GREP_OPTIONS" +.It Ev GREP_COLOR +This variable specifies the color used to highlight matched (non-empty) text. +.It Ev GREP_OPTIONS +This variable specifies default options +to be placed in front of any explicit options. +It may cause problems with portable scripts. +.It Ev TERM +This variable specifies the type name of the terminal, console or display-device +type to be used. +See +.Xr term 7 . +.El +.Sh EXIT STATUS +The +.Nm grep +utility exits with one of the following values: +.Pp +.Bl -tag -width flag -compact +.It Li 0 +One or more lines were selected. +.It Li 1 +No lines were selected. +.It Li \*(Gt1 +An error occurred. +.El +.Sh EXAMPLES +.Bl -dash +.It +Find all occurrences of the pattern +.Sq patricia +in a file: +.Pp +.Dl $ grep 'patricia' myfile +.It +Same as above but looking only for complete words: +.Pp +.Dl $ grep -w 'patricia' myfile +.It +Count occurrences of the exact pattern +.Sq FOO +: +.Pp +.Dl $ grep -c FOO myfile +.It +Same as above but ignoring case: +.Pp +.Dl $ grep -c -i FOO myfile +.It +Find all occurrences of the pattern +.Ql .Pp +at the beginning of a line: +.Pp +.Dl $ grep '^\e.Pp' myfile +.Pp +The apostrophes ensure the entire expression is evaluated by +.Nm grep +instead of by the user's shell. +The caret +.Ql ^ +matches the null string at the beginning of a line, +and the +.Ql \e +escapes the +.Ql \&. , +which would otherwise match any character. +.It +Find all lines in a file which do not contain the words +.Sq foo +or +.Sq bar : +.Pp +.Dl $ grep -v -e 'foo' -e 'bar' myfile +.It +Peruse the file +.Sq calendar +looking for either 19, 20, or 25 using extended regular expressions: +.Pp +.Dl $ egrep '19|20|25' calendar +.It +Show matching lines and the name of the +.Sq *.h +files which contain the pattern +.Sq FIXME . +Do the search recursively from the +.Pa /usr/src/sys/arm +directory +.Pp +.Dl $ grep -H -R FIXME --include="*.h" /usr/src/sys/arm/ +.It +Same as above but show only the name of the matching file: +.Pp +.Dl $ grep -l -R FIXME --include="*.h" /usr/src/sys/arm/ +.It +Show lines containing the text +.Sq foo . +The matching part of the output is colored and every line is prefixed with +the line number and the offset in the file for those lines that matched. +.Pp +.Dl $ grep -b --colour -n foo myfile +.It +Show lines that match the extended regular expression patterns read from the +standard input: +.Pp +.Dl $ echo -e 'Free\enBSD\enAll.*reserved' | grep -E -f - myfile +.It +Show lines from the output of the +.Xr pciconf 8 +command matching the specified extended regular expression along with +three lines of leading context and one line of trailing context: +.Pp +.Dl $ pciconf -lv | grep -B3 -A1 -E 'class.*=.*storage' +.It +Suppress any output and use the exit status to show an appropriate message: +.Pp +.Dl $ grep -q foo myfile && echo File matches +.El +.Sh SEE ALSO +.Xr ed 1 , +.Xr ex 1 , +.Xr sed 1 , +.Xr zgrep 1 , +.Xr re_format 7 +.Sh STANDARDS +The +.Nm +utility is compliant with the +.St -p1003.1-2008 +specification. +.Pp +The flags +.Op Fl AaBbCDdGHhILmopRSUVw +are extensions to that specification, and the behaviour of the +.Fl f +flag when used with an empty pattern file is left undefined. +.Pp +All long options are provided for compatibility with +GNU versions of this utility. +.Pp +Historic versions of the +.Nm grep +utility also supported the flags +.Op Fl ruy . +This implementation supports those options; +however, their use is strongly discouraged. +.Sh HISTORY +The +.Nm grep +command first appeared in +.At v6 . diff --git a/src/grep/grep.c b/src/grep/grep.c new file mode 100644 index 0000000..2016c39 --- /dev/null +++ b/src/grep/grep.c @@ -0,0 +1,756 @@ +/* $NetBSD: grep.c,v 1.6 2011/04/18 03:48:23 joerg Exp $ */ +/* $OpenBSD: grep.c,v 1.42 2010/07/02 22:18:03 tedu Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 1999 James Howard and Dag-Erling Smørgrav + * Copyright (C) 2008-2009 Gabor Kovesdan + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "grep.h" + +const char *errstr[] = { + "", +/* 1*/ "(standard input)", +/* 2*/ "unknown %s option", +/* 3*/ "usage: %s [-abcDEFGHhIiLlmnOopqRSsUVvwxz] [-A num] [-B num] [-C num]\n", +/* 4*/ "\t[-e pattern] [-f file] [--binary-files=value] [--color=when]\n", +/* 5*/ "\t[--context=num] [--directories=action] [--label] [--line-buffered]\n", +/* 6*/ "\t[--null] [pattern] [file ...]\n", +/* 7*/ "Binary file %s matches\n", +/* 8*/ "%s (BSD grep, GNU compatible) %s\n", +}; + +/* Flags passed to regcomp() and regexec() */ +int cflags = REG_NOSUB | REG_NEWLINE; +int eflags = REG_STARTEND; + +bool matchall; + +/* Searching patterns */ +unsigned int patterns; +static unsigned int pattern_sz; +struct pat *pattern; +regex_t *r_pattern; + +/* Filename exclusion/inclusion patterns */ +unsigned int fpatterns, dpatterns; +static unsigned int fpattern_sz, dpattern_sz; +struct epat *dpattern, *fpattern; + +/* For regex errors */ +char re_error[RE_ERROR_BUF + 1]; + +/* Command-line flags */ +long long Aflag; /* -A x: print x lines trailing each match */ +long long Bflag; /* -B x: print x lines leading each match */ +bool Hflag; /* -H: always print file name */ +bool Lflag; /* -L: only show names of files with no matches */ +bool grep_bflag; /* -b: show block numbers for each match */ +bool grep_cflag; /* -c: only show a count of matching lines */ +bool hflag; /* -h: don't print filename headers */ +bool iflag; /* -i: ignore case */ +bool lflag; /* -l: only show names of files with matches */ +bool mflag; /* -m x: stop reading the files after x matches */ +long long mcount; /* count for -m */ +long long mlimit; /* requested value for -m */ +char fileeol; /* indicator for eol */ +bool nflag; /* -n: show line numbers in front of matching lines */ +bool oflag; /* -o: print only matching part */ +bool qflag; /* -q: quiet mode (don't output anything) */ +bool sflag; /* -s: silent mode (ignore errors) */ +bool vflag; /* -v: only show non-matching lines */ +bool wflag; /* -w: pattern must start and end on word boundaries */ +bool grep_xflag; /* -x: pattern must match entire line */ +bool lbflag; /* --line-buffered */ +bool nullflag; /* --null */ +char *grep_label; /* --label */ +const char *grep_color; /* --color */ +int grepbehave = GREP_BASIC; /* -EFG: type of the regex */ +int binbehave = BINFILE_BIN; /* -aIU: handling of binary files */ +int filebehave = FILE_STDIO; +int devbehave = DEV_READ; /* -D: handling of devices */ +int dirbehave = DIR_READ; /* -dRr: handling of directories */ +int linkbehave = LINK_SKIP; /* -OpS: handling of symlinks */ + +bool dexclude, dinclude; /* --exclude-dir and --include-dir */ +bool fexclude, finclude; /* --exclude and --include */ + +enum { + BIN_OPT = CHAR_MAX + 1, + COLOR_OPT, + HELP_OPT, + MMAP_OPT, + LINEBUF_OPT, + LABEL_OPT, + NULL_OPT, + R_EXCLUDE_OPT, + R_INCLUDE_OPT, + R_DEXCLUDE_OPT, + R_DINCLUDE_OPT +}; + +static inline const char *init_color(const char *); + +/* Housekeeping */ +bool file_err; /* file reading error */ + +/* + * Prints usage information and returns 2. + */ +static void +usage(void) +{ + fprintf(stderr, errstr[3], "grep"); + fprintf(stderr, "%s", errstr[4]); + fprintf(stderr, "%s", errstr[5]); + fprintf(stderr, "%s", errstr[6]); + exit(2); +} + +static const char *optstr = "0123456789A:B:C:D:EFGHILOSRUVabcd:e:f:hilm:nopqrsuvwxyz"; + +static const struct option long_options[] = +{ + {"binary-files", required_argument, NULL, BIN_OPT}, + {"help", no_argument, NULL, HELP_OPT}, + {"mmap", no_argument, NULL, MMAP_OPT}, + {"line-buffered", no_argument, NULL, LINEBUF_OPT}, + {"label", required_argument, NULL, LABEL_OPT}, + {"null", no_argument, NULL, NULL_OPT}, + {"color", optional_argument, NULL, COLOR_OPT}, + {"colour", optional_argument, NULL, COLOR_OPT}, + {"exclude", required_argument, NULL, R_EXCLUDE_OPT}, + {"include", required_argument, NULL, R_INCLUDE_OPT}, + {"exclude-dir", required_argument, NULL, R_DEXCLUDE_OPT}, + {"include-dir", required_argument, NULL, R_DINCLUDE_OPT}, + {"after-context", required_argument, NULL, 'A'}, + {"text", no_argument, NULL, 'a'}, + {"before-context", required_argument, NULL, 'B'}, + {"byte-offset", no_argument, NULL, 'b'}, + {"context", optional_argument, NULL, 'C'}, + {"count", no_argument, NULL, 'c'}, + {"devices", required_argument, NULL, 'D'}, + {"directories", required_argument, NULL, 'd'}, + {"extended-regexp", no_argument, NULL, 'E'}, + {"regexp", required_argument, NULL, 'e'}, + {"fixed-strings", no_argument, NULL, 'F'}, + {"file", required_argument, NULL, 'f'}, + {"basic-regexp", no_argument, NULL, 'G'}, + {"no-filename", no_argument, NULL, 'h'}, + {"with-filename", no_argument, NULL, 'H'}, + {"ignore-case", no_argument, NULL, 'i'}, + {"files-with-matches", no_argument, NULL, 'l'}, + {"files-without-match", no_argument, NULL, 'L'}, + {"max-count", required_argument, NULL, 'm'}, + {"line-number", no_argument, NULL, 'n'}, + {"only-matching", no_argument, NULL, 'o'}, + {"quiet", no_argument, NULL, 'q'}, + {"silent", no_argument, NULL, 'q'}, + {"recursive", no_argument, NULL, 'r'}, + {"no-messages", no_argument, NULL, 's'}, + {"binary", no_argument, NULL, 'U'}, + {"unix-byte-offsets", no_argument, NULL, 'u'}, + {"invert-match", no_argument, NULL, 'v'}, + {"version", no_argument, NULL, 'V'}, + {"word-regexp", no_argument, NULL, 'w'}, + {"line-regexp", no_argument, NULL, 'x'}, + {"null-data", no_argument, NULL, 'z'}, + {NULL, no_argument, NULL, 0} +}; + +/* + * Adds a searching pattern to the internal array. + */ +static void +add_pattern(char *pat, size_t len) +{ + + /* Check if we can do a shortcut */ + if (len == 0) { + matchall = true; + return; + } + /* Increase size if necessary */ + if (patterns == pattern_sz) { + pattern_sz *= 2; + pattern = grep_realloc(pattern, ++pattern_sz * + sizeof(struct pat)); + } + if (len > 0 && pat[len - 1] == '\n') + --len; + /* pat may not be NUL-terminated */ + pattern[patterns].pat = grep_malloc(len + 1); + memcpy(pattern[patterns].pat, pat, len); + pattern[patterns].len = len; + pattern[patterns].pat[len] = '\0'; + ++patterns; +} + +/* + * Adds a file include/exclude pattern to the internal array. + */ +static void +add_fpattern(const char *pat, int mode) +{ + + /* Increase size if necessary */ + if (fpatterns == fpattern_sz) { + fpattern_sz *= 2; + fpattern = grep_realloc(fpattern, ++fpattern_sz * + sizeof(struct epat)); + } + fpattern[fpatterns].pat = grep_strdup(pat); + fpattern[fpatterns].mode = mode; + ++fpatterns; +} + +/* + * Adds a directory include/exclude pattern to the internal array. + */ +static void +add_dpattern(const char *pat, int mode) +{ + + /* Increase size if necessary */ + if (dpatterns == dpattern_sz) { + dpattern_sz *= 2; + dpattern = grep_realloc(dpattern, ++dpattern_sz * + sizeof(struct epat)); + } + dpattern[dpatterns].pat = grep_strdup(pat); + dpattern[dpatterns].mode = mode; + ++dpatterns; +} + +/* + * Reads searching patterns from a file and adds them with add_pattern(). + */ +static void +read_patterns(const char *fn) +{ + struct stat st; + FILE *f; + char *line; + size_t len; + ssize_t rlen; + + if (strcmp(fn, "-") == 0) + f = stdin; + else if ((f = fopen(fn, "r")) == NULL) + err(2, "%s", fn); + if ((fstat(fileno(f), &st) == -1) || (S_ISDIR(st.st_mode))) { + fclose(f); + return; + } + len = 0; + line = NULL; + while ((rlen = getline(&line, &len, f)) != -1) { + if (line[0] == '\0') + continue; + add_pattern(line, line[0] == '\n' ? 0 : (size_t)rlen); + } + + free(line); + if (ferror(f)) + err(2, "%s", fn); + if (strcmp(fn, "-") != 0) + fclose(f); +} + +static inline const char * +init_color(const char *d) +{ + char *c; + + c = getenv("GREP_COLOR"); + return (c != NULL && c[0] != '\0' ? c : d); +} + +int +grep_main(int argc, char *argv[]) +{ + char **aargv, **eargv, *eopts; + char *ep; + const char *pn; + long long l; + unsigned int aargc, eargc, i; + int c, lastc, needpattern, newarg, prevoptind; + bool matched; + + /* Reset all global state for multicall use */ + cflags = REG_NOSUB | REG_NEWLINE; + eflags = REG_STARTEND; + matchall = false; + patterns = 0; + pattern_sz = 0; + free(pattern); pattern = NULL; + free(r_pattern); r_pattern = NULL; + fpatterns = 0; dpatterns = 0; + fpattern_sz = 0; dpattern_sz = 0; + free(dpattern); dpattern = NULL; + free(fpattern); fpattern = NULL; + Aflag = 0; Bflag = 0; + Hflag = false; Lflag = false; + grep_bflag = false; grep_cflag = false; hflag = false; iflag = false; + lflag = false; mflag = false; nflag = false; oflag = false; + qflag = false; sflag = false; vflag = false; wflag = false; + grep_xflag = false; lbflag = false; nullflag = false; + mcount = 0; mlimit = 0; + fileeol = '\n'; + grep_label = NULL; + grep_color = NULL; + grepbehave = GREP_BASIC; + binbehave = BINFILE_BIN; + filebehave = FILE_STDIO; + devbehave = DEV_READ; + dirbehave = DIR_READ; + linkbehave = LINK_SKIP; + dexclude = false; dinclude = false; + fexclude = false; finclude = false; + file_err = false; + optind = 1; + + setlocale(LC_ALL, ""); + + /* + * Check how we've bene invoked to determine the behavior we should + * exhibit. In this way we can have all the functionalities in one + * binary without the need of scripting and using ugly hacks. + */ + pn = basename(argv[0]); + switch (pn[0]) { + case 'e': + grepbehave = GREP_EXTENDED; + break; + case 'f': + grepbehave = GREP_FIXED; + break; + case 'r': + dirbehave = DIR_RECURSE; + Hflag = true; + break; + } + + lastc = '\0'; + newarg = 1; + prevoptind = 1; + needpattern = 1; + fileeol = '\n'; + + eopts = getenv("GREP_OPTIONS"); + + /* support for extra arguments in GREP_OPTIONS */ + eargc = 0; + if (eopts != NULL && eopts[0] != '\0') { + char *str; + + /* make an estimation of how many extra arguments we have */ + for (unsigned int j = 0; j < strlen(eopts); j++) + if (eopts[j] == ' ') + eargc++; + + eargv = (char **)grep_malloc(sizeof(char *) * (eargc + 1)); + + eargc = 0; + /* parse extra arguments */ + while ((str = strsep(&eopts, " ")) != NULL) + if (str[0] != '\0') + eargv[eargc++] = grep_strdup(str); + + aargv = (char **)grep_calloc(eargc + argc + 1, + sizeof(char *)); + + aargv[0] = argv[0]; + for (i = 0; i < eargc; i++) + aargv[i + 1] = eargv[i]; + for (int j = 1; j < argc; j++, i++) + aargv[i + 1] = argv[j]; + + aargc = eargc + argc; + } else { + aargv = argv; + aargc = argc; + } + + while (((c = getopt_long(aargc, aargv, optstr, long_options, NULL)) != + -1)) { + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + if (newarg || !isdigit(lastc)) + Aflag = 0; + else if (Aflag > LLONG_MAX / 10 - 1) { + errno = ERANGE; + err(2, NULL); + } + + Aflag = Bflag = (Aflag * 10) + (c - '0'); + break; + case 'C': + if (optarg == NULL) { + Aflag = Bflag = 2; + break; + } + /* FALLTHROUGH */ + case 'A': + /* FALLTHROUGH */ + case 'B': + errno = 0; + l = strtoll(optarg, &ep, 10); + if (errno == ERANGE || errno == EINVAL) + err(2, NULL); + else if (ep[0] != '\0') { + errno = EINVAL; + err(2, NULL); + } else if (l < 0) { + errno = EINVAL; + err(2, "context argument must be non-negative"); + } + + if (c == 'A') + Aflag = l; + else if (c == 'B') + Bflag = l; + else + Aflag = Bflag = l; + break; + case 'a': + binbehave = BINFILE_TEXT; + break; + case 'b': + grep_bflag = true; + break; + case 'c': + grep_cflag = true; + break; + case 'D': + if (strcasecmp(optarg, "skip") == 0) + devbehave = DEV_SKIP; + else if (strcasecmp(optarg, "read") == 0) + devbehave = DEV_READ; + else + errx(2, errstr[2], "--devices"); + break; + case 'd': + if (strcasecmp("recurse", optarg) == 0) { + Hflag = true; + dirbehave = DIR_RECURSE; + } else if (strcasecmp("skip", optarg) == 0) + dirbehave = DIR_SKIP; + else if (strcasecmp("read", optarg) == 0) + dirbehave = DIR_READ; + else + errx(2, errstr[2], "--directories"); + break; + case 'E': + grepbehave = GREP_EXTENDED; + break; + case 'e': + { + char *token; + char *string = optarg; + + while ((token = strsep(&string, "\n")) != NULL) + add_pattern(token, strlen(token)); + } + needpattern = 0; + break; + case 'F': + grepbehave = GREP_FIXED; + break; + case 'f': + read_patterns(optarg); + needpattern = 0; + break; + case 'G': + grepbehave = GREP_BASIC; + break; + case 'H': + Hflag = true; + break; + case 'h': + Hflag = false; + hflag = true; + break; + case 'I': + binbehave = BINFILE_SKIP; + break; + case 'i': + case 'y': + iflag = true; + cflags |= REG_ICASE; + break; + case 'L': + lflag = false; + Lflag = true; + break; + case 'l': + Lflag = false; + lflag = true; + break; + case 'm': + mflag = true; + errno = 0; + mlimit = mcount = strtoll(optarg, &ep, 10); + if (((errno == ERANGE) && (mcount == LLONG_MAX)) || + ((errno == EINVAL) && (mcount == 0))) + err(2, NULL); + else if (ep[0] != '\0') { + errno = EINVAL; + err(2, NULL); + } + break; + case 'n': + nflag = true; + break; + case 'O': + linkbehave = LINK_EXPLICIT; + break; + case 'o': + oflag = true; + cflags &= ~REG_NOSUB; + break; + case 'p': + linkbehave = LINK_SKIP; + break; + case 'q': + qflag = true; + break; + case 'S': + linkbehave = LINK_READ; + break; + case 'R': + case 'r': + dirbehave = DIR_RECURSE; + Hflag = true; + break; + case 's': + sflag = true; + break; + case 'U': + binbehave = BINFILE_BIN; + break; + case 'u': + case MMAP_OPT: + filebehave = FILE_MMAP; + break; + case 'V': + printf(errstr[8], "grep", VERSION); + exit(0); + case 'v': + vflag = true; + break; + case 'w': + wflag = true; + cflags &= ~REG_NOSUB; + break; + case 'x': + grep_xflag = true; + cflags &= ~REG_NOSUB; + break; + case 'z': + fileeol = '\0'; + cflags &= ~REG_NEWLINE; + break; + case BIN_OPT: + if (strcasecmp("binary", optarg) == 0) + binbehave = BINFILE_BIN; + else if (strcasecmp("without-match", optarg) == 0) + binbehave = BINFILE_SKIP; + else if (strcasecmp("text", optarg) == 0) + binbehave = BINFILE_TEXT; + else + errx(2, errstr[2], "--binary-files"); + break; + case COLOR_OPT: + grep_color = NULL; + if (optarg == NULL || strcasecmp("auto", optarg) == 0 || + strcasecmp("tty", optarg) == 0 || + strcasecmp("if-tty", optarg) == 0) { + char *term; + + term = getenv("TERM"); + if (isatty(STDOUT_FILENO) && term != NULL && + strcasecmp(term, "dumb") != 0) + grep_color = init_color("01;31"); + } else if (strcasecmp("always", optarg) == 0 || + strcasecmp("yes", optarg) == 0 || + strcasecmp("force", optarg) == 0) { + grep_color = init_color("01;31"); + } else if (strcasecmp("never", optarg) != 0 && + strcasecmp("none", optarg) != 0 && + strcasecmp("no", optarg) != 0) + errx(2, errstr[2], "--color"); + cflags &= ~REG_NOSUB; + break; + case LABEL_OPT: + grep_label = optarg; + break; + case LINEBUF_OPT: + lbflag = true; + break; + case NULL_OPT: + nullflag = true; + break; + case R_INCLUDE_OPT: + finclude = true; + add_fpattern(optarg, INCL_PAT); + break; + case R_EXCLUDE_OPT: + fexclude = true; + add_fpattern(optarg, EXCL_PAT); + break; + case R_DINCLUDE_OPT: + dinclude = true; + add_dpattern(optarg, INCL_PAT); + break; + case R_DEXCLUDE_OPT: + dexclude = true; + add_dpattern(optarg, EXCL_PAT); + break; + case HELP_OPT: + default: + usage(); + } + lastc = c; + newarg = optind != prevoptind; + prevoptind = optind; + } + aargc -= optind; + aargv += optind; + + /* grep_xflag takes precedence, don't confuse the matching bits. */ + if (wflag && grep_xflag) + wflag = false; + + /* Fail if we don't have any pattern */ + if (aargc == 0 && needpattern) + usage(); + + /* Process patterns from command line */ + if (aargc != 0 && needpattern) { + char *token; + char *string = *aargv; + + while ((token = strsep(&string, "\n")) != NULL) + add_pattern(token, strlen(token)); + --aargc; + ++aargv; + } + + switch (grepbehave) { + case GREP_BASIC: + break; + case GREP_FIXED: + /* + * regex(3) implementations that support fixed-string searches generally + * define either REG_NOSPEC or REG_LITERAL. Set the appropriate flag + * here. If neither are defined, GREP_FIXED later implies that the + * internal literal matcher should be used. Other cflags that have + * the same interpretation as REG_NOSPEC and REG_LITERAL should be + * similarly added here, and grep.h should be amended to take this into + * consideration when defining WITH_INTERNAL_NOSPEC. + */ +#if defined(REG_NOSPEC) + cflags |= REG_NOSPEC; +#elif defined(REG_LITERAL) + cflags |= REG_LITERAL; +#endif + break; + case GREP_EXTENDED: + cflags |= REG_EXTENDED; + break; + default: + /* NOTREACHED */ + usage(); + } + + r_pattern = grep_calloc(patterns, sizeof(*r_pattern)); + +#ifdef WITH_INTERNAL_NOSPEC + if (grepbehave != GREP_FIXED) { +#else + { +#endif + /* Check if cheating is allowed (always is for fgrep). */ + for (i = 0; i < patterns; ++i) { + c = regcomp(&r_pattern[i], pattern[i].pat, cflags); + if (c != 0) { + regerror(c, &r_pattern[i], re_error, + RE_ERROR_BUF); + errx(2, "%s", re_error); + } + } + } + + if (lbflag) + setlinebuf(stdout); + + if ((aargc == 0 || aargc == 1) && !Hflag) + hflag = true; + + initqueue(); + + if (aargc == 0 && dirbehave != DIR_RECURSE) + return !procfile("-"); + + if (dirbehave == DIR_RECURSE) + matched = grep_tree(aargv); + else + for (matched = false; aargc--; ++aargv) { + if ((finclude || fexclude) && !file_matching(*aargv)) + continue; + if (procfile(*aargv)) + matched = true; + } + + if (Lflag) + matched = !matched; + + /* + * Calculate the correct return value according to the + * results and the command line option. + */ + return (matched ? (file_err ? (qflag ? 0 : 2) : 0) : (file_err ? 2 : 1)); +} diff --git a/src/grep/grep.h b/src/grep/grep.h new file mode 100644 index 0000000..7759b32 --- /dev/null +++ b/src/grep/grep.h @@ -0,0 +1,157 @@ +/* $NetBSD: grep.h,v 1.5 2011/02/27 17:33:37 joerg Exp $ */ +/* $OpenBSD: grep.h,v 1.15 2010/04/05 03:03:55 tedu Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 1999 James Howard and Dag-Erling Smørgrav + * Copyright (c) 2008-2009 Gabor Kovesdan + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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 +#include +#include +#include + +extern const char *errstr[]; + +#define VERSION "2.6.0-FreeBSD" + +#define GREP_FIXED 0 +#define GREP_BASIC 1 +#define GREP_EXTENDED 2 + +#if !defined(REG_NOSPEC) && !defined(REG_LITERAL) +#define WITH_INTERNAL_NOSPEC +#endif + +#define BINFILE_BIN 0 +#define BINFILE_SKIP 1 +#define BINFILE_TEXT 2 + +#define FILE_STDIO 0 +#define FILE_MMAP 1 + +#define DIR_READ 0 +#define DIR_SKIP 1 +#define DIR_RECURSE 2 + +#define DEV_READ 0 +#define DEV_SKIP 1 + +#define LINK_READ 0 +#define LINK_EXPLICIT 1 +#define LINK_SKIP 2 + +#define EXCL_PAT 0 +#define INCL_PAT 1 + +#define MAX_MATCHES 32 + +struct file { + int fd; + bool binary; +}; + +struct str { + off_t boff; + off_t off; + size_t len; + char *dat; + char *file; + int line_no; +}; + +struct pat { + char *pat; + int len; +}; + +struct epat { + char *pat; + int mode; +}; + +/* + * Parsing context; used to hold things like matches made and + * other useful bits + */ +struct parsec { + regmatch_t matches[MAX_MATCHES]; /* Matches made */ + /* XXX TODO: This should be a chunk, not a line */ + struct str ln; /* Current line */ + size_t lnstart; /* Position in line */ + size_t matchidx; /* Latest match index */ + int printed; /* Metadata printed? */ + bool binary; /* Binary file? */ + bool cntlines; /* Count lines? */ +}; + +/* Flags passed to regcomp() and regexec() */ +extern int cflags, eflags; + +/* Command line flags */ +extern bool Eflag, Fflag, Gflag, Hflag, Lflag, + grep_bflag, grep_cflag, hflag, iflag, lflag, mflag, nflag, oflag, + qflag, sflag, vflag, wflag, grep_xflag; +extern bool dexclude, dinclude, fexclude, finclude, lbflag, nullflag; +extern long long Aflag, Bflag; +extern long long mcount; +extern long long mlimit; +extern char fileeol; +extern char *grep_label; +extern const char *grep_color; +extern int binbehave, devbehave, dirbehave, filebehave, grepbehave, linkbehave; + +extern bool file_err, matchall; +extern unsigned int dpatterns, fpatterns, patterns; +extern struct pat *pattern; +extern struct epat *dpattern, *fpattern; +extern regex_t *er_pattern, *r_pattern; + +/* For regex errors */ +#define RE_ERROR_BUF 512 +extern char re_error[RE_ERROR_BUF + 1]; /* Seems big enough */ + +/* util.c */ +bool file_matching(const char *fname); +bool procfile(const char *fn); +bool grep_tree(char **argv); +void *grep_malloc(size_t size); +void *grep_calloc(size_t nmemb, size_t size); +void *grep_realloc(void *ptr, size_t size); +char *grep_strdup(const char *str); +void grep_printline(struct str *line, int sep); + +/* queue.c */ +void initqueue(void); +bool enqueue(struct str *x); +void printqueue(void); +void clearqueue(void); + +/* file.c */ +void grep_close(struct file *f); +struct file *grep_open(const char *path); +char *grep_fgetln(struct file *f, struct parsec *pc); diff --git a/src/grep/queue.c b/src/grep/queue.c new file mode 100644 index 0000000..e371ae0 --- /dev/null +++ b/src/grep/queue.c @@ -0,0 +1,139 @@ +/* $NetBSD: queue.c,v 1.5 2011/08/31 16:24:57 plunky Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 1999 James Howard and Dag-Erling Smørgrav + * All rights reserved. + * Copyright (c) 2020 Kyle Evans + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +/* + * A really poor man's queue. It does only what it has to and gets out of + * Dodge. It is used in place of to get a better performance. + */ + +#include +#include + +#include "grep.h" + +typedef struct str qentry_t; + +static long long filled; +static qentry_t *qend, *qpool; + +/* + * qnext is the next entry to populate. qlist is where the list actually + * starts, for the purposes of printing. + */ +static qentry_t *qlist, *qnext; + +void +initqueue(void) +{ + + qlist = qnext = qpool = grep_calloc(Bflag, sizeof(*qpool)); + qend = qpool + (Bflag - 1); +} + +static qentry_t * +advqueue(qentry_t *itemp) +{ + + if (itemp == qend) + return (qpool); + return (itemp + 1); +} + +/* + * Enqueue another line; return true if we've dequeued a line as a result + */ +bool +enqueue(struct str *x) +{ + qentry_t *item; + bool rotated; + + item = qnext; + qnext = advqueue(qnext); + rotated = false; + + if (filled < Bflag) { + filled++; + } else if (filled == Bflag) { + /* We had already filled up coming in; just rotate. */ + qlist = advqueue(qlist); + rotated = true; + free(item->dat); + } + /* len + 1 for NUL-terminator */ + item->dat = grep_malloc(sizeof(char) * x->len + 1); + item->len = x->len; + item->line_no = x->line_no; + item->boff = x->boff; + item->off = x->off; + memcpy(item->dat, x->dat, x->len); + item->dat[x->len] = '\0'; + item->file = x->file; + + return (rotated); +} + +void +printqueue(void) +{ + qentry_t *item; + + item = qlist; + do { + /* Buffer must have ended early. */ + if (item->dat == NULL) + break; + + grep_printline(item, '-'); + free(item->dat); + item->dat = NULL; + item = advqueue(item); + } while (item != qlist); + + qlist = qnext = qpool; + filled = 0; +} + +void +clearqueue(void) +{ + qentry_t *item; + + item = qlist; + do { + free(item->dat); + item->dat = NULL; + item = advqueue(item); + } while (item != qlist); + + qlist = qnext = qpool; + filled = 0; +} diff --git a/src/grep/util.c b/src/grep/util.c new file mode 100644 index 0000000..f2c7a48 --- /dev/null +++ b/src/grep/util.c @@ -0,0 +1,850 @@ +/* $NetBSD: util.c,v 1.9 2011/02/27 17:33:37 joerg Exp $ */ +/* $OpenBSD: util.c,v 1.39 2010/07/02 22:18:03 tedu Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 1999 James Howard and Dag-Erling Smørgrav + * Copyright (C) 2008-2010 Gabor Kovesdan + * Copyright (C) 2017 Kyle Evans + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "grep.h" + +static bool first_match = true; + +/* + * Match printing context + */ +struct mprintc { + long long tail; /* Number of trailing lines to record */ + int last_outed; /* Number of lines since last output */ + bool doctx; /* Printing context? */ + bool printmatch; /* Printing matches? */ + bool same_file; /* Same file as previously printed? */ +}; + +static void procmatch_match(struct mprintc *mc, struct parsec *pc); +static void procmatch_nomatch(struct mprintc *mc, struct parsec *pc); +static bool procmatches(struct mprintc *mc, struct parsec *pc, bool matched); +#ifdef WITH_INTERNAL_NOSPEC +static int litexec(const struct pat *pat, const char *string, + size_t nmatch, regmatch_t pmatch[]); +#endif +static bool procline(struct parsec *pc); +static bool printline(struct parsec *pc, int sep, size_t *last_out); +static void printline_metadata(struct str *line, int sep); + +bool +file_matching(const char *fname) +{ + char *fname_base, *fname_buf; + bool ret; + + ret = finclude ? false : true; + fname_buf = strdup(fname); + if (fname_buf == NULL) + err(2, "strdup"); + fname_base = basename(fname_buf); + + for (unsigned int i = 0; i < fpatterns; ++i) { + if (fnmatch(fpattern[i].pat, fname, 0) == 0 || + fnmatch(fpattern[i].pat, fname_base, 0) == 0) + /* + * The last pattern matched wins exclusion/inclusion + * rights, so we can't reasonably bail out early here. + */ + ret = (fpattern[i].mode != EXCL_PAT); + } + free(fname_buf); + return (ret); +} + +static inline bool +dir_matching(const char *dname) +{ + bool ret; + + ret = dinclude ? false : true; + + for (unsigned int i = 0; i < dpatterns; ++i) { + if (dname != NULL && fnmatch(dpattern[i].pat, dname, 0) == 0) + /* + * The last pattern matched wins exclusion/inclusion + * rights, so we can't reasonably bail out early here. + */ + ret = (dpattern[i].mode != EXCL_PAT); + } + return (ret); +} + +/* + * Processes a directory when a recursive search is performed with + * the -R option. Each appropriate file is passed to procfile(). + */ +bool +grep_tree(char **argv) +{ + FTS *fts; + FTSENT *p; + int fts_flags; + bool matched, ok; + const char *wd[] = { ".", NULL }; + + matched = false; + + /* This switch effectively initializes 'fts_flags' */ + switch(linkbehave) { + case LINK_EXPLICIT: + fts_flags = FTS_COMFOLLOW | FTS_PHYSICAL; + break; + case LINK_SKIP: + fts_flags = FTS_PHYSICAL; + break; + default: + fts_flags = FTS_LOGICAL | FTS_NOSTAT; + } + + fts_flags |= FTS_NOCHDIR; + + fts = fts_open((argv[0] == NULL) ? + (char * const *)wd : argv, fts_flags, NULL); + if (fts == NULL) + err(2, "fts_open"); + while (errno = 0, (p = fts_read(fts)) != NULL) { + switch (p->fts_info) { + case FTS_DNR: + case FTS_ERR: + case FTS_NS: + file_err = true; + if(!sflag) + warnc(p->fts_errno, "%s", p->fts_path); + break; + case FTS_D: + if (dexclude || dinclude) + if (!dir_matching(p->fts_name) || + !dir_matching(p->fts_path)) + fts_set(fts, p, FTS_SKIP); + break; + case FTS_DC: + /* Print a warning for recursive directory loop */ + warnx("warning: %s: recursive directory loop", + p->fts_path); + break; + case FTS_DP: + break; + case FTS_SL: + /* + * Skip symlinks for LINK_EXPLICIT and + * LINK_SKIP. Note that due to FTS_COMFOLLOW, + * symlinks on the command line are followed + * for LINK_EXPLICIT and not reported as + * symlinks. + */ + break; + default: + /* Check for file exclusion/inclusion */ + ok = true; + if (fexclude || finclude) + ok &= file_matching(p->fts_path); + + if (ok && procfile(p->fts_path)) + matched = true; + break; + } + } + if (errno != 0) + err(2, "fts_read"); + + fts_close(fts); + return (matched); +} + +static void +procmatch_match(struct mprintc *mc, struct parsec *pc) +{ + + if (mc->doctx) { + if (!first_match && (!mc->same_file || mc->last_outed > 0)) + printf("--\n"); + if (Bflag > 0) + printqueue(); + mc->tail = Aflag; + } + + /* Print the matching line, but only if not quiet/binary */ + if (mc->printmatch) { + size_t last_out; + bool terminated; + + last_out = 0; + terminated = printline(pc, ':', &last_out); + while (pc->matchidx >= MAX_MATCHES) { + /* Reset matchidx and try again */ + pc->matchidx = 0; + if (procline(pc) == !vflag) + terminated = printline(pc, ':', &last_out); + else + break; + } + + /* + * The above loop processes the entire line as long as we keep + * hitting the maximum match count. At this point, we know + * that there's nothing left to be printed and can terminate the + * line. + */ + if (!terminated) + printline(pc, ':', &last_out); + + first_match = false; + mc->same_file = true; + mc->last_outed = 0; + } +} + +static void +procmatch_nomatch(struct mprintc *mc, struct parsec *pc) +{ + + /* Deal with any -A context as needed */ + if (mc->tail > 0) { + grep_printline(&pc->ln, '-'); + mc->tail--; + if (Bflag > 0) + clearqueue(); + } else if (Bflag == 0 || (Bflag > 0 && enqueue(&pc->ln))) + /* + * Enqueue non-matching lines for -B context. If we're not + * actually doing -B context or if the enqueue resulted in a + * line being rotated out, then go ahead and increment + * last_outed to signify a gap between context/match. + */ + ++mc->last_outed; +} + +/* + * Process any matches in the current parsing context, return a boolean + * indicating whether we should halt any further processing or not. 'true' to + * continue processing, 'false' to halt. + */ +static bool +procmatches(struct mprintc *mc, struct parsec *pc, bool matched) +{ + + if (mflag && mcount <= 0) { + /* + * We already hit our match count, but we need to keep dumping + * lines until we've lost our tail. + */ + grep_printline(&pc->ln, '-'); + mc->tail--; + return (mc->tail != 0); + } + + /* + * XXX TODO: This should loop over pc->matches and handle things on a + * line-by-line basis, setting up a `struct str` as needed. + */ + /* Deal with any -B context or context separators */ + if (matched) { + procmatch_match(mc, pc); + + /* Count the matches if we have a match limit */ + if (mflag) { + /* XXX TODO: Decrement by number of matched lines */ + mcount -= 1; + if (mcount <= 0) + return (mc->tail != 0); + } + } else if (mc->doctx) + procmatch_nomatch(mc, pc); + + return (true); +} + +/* + * Opens a file and processes it. Each file is processed line-by-line + * passing the lines to procline(). + */ +bool +procfile(const char *fn) +{ + struct parsec pc; + struct mprintc mc; + struct file *f; + struct stat sb; + mode_t s; + int lines; + bool line_matched; + + if (strcmp(fn, "-") == 0) { + fn = grep_label != NULL ? grep_label : errstr[1]; + f = grep_open(NULL); + } else { + if (stat(fn, &sb) == 0) { + /* Check if we need to process the file */ + s = sb.st_mode & S_IFMT; + if (dirbehave == DIR_SKIP && s == S_IFDIR) + return (false); + if (devbehave == DEV_SKIP && (s == S_IFIFO || + s == S_IFCHR || s == S_IFBLK || s == S_IFSOCK)) + return (false); + } + f = grep_open(fn); + } + if (f == NULL) { + file_err = true; + if (!sflag) + warn("%s", fn); + return (false); + } + + pc.ln.file = grep_strdup(fn); + pc.ln.line_no = 0; + pc.ln.len = 0; + pc.ln.boff = 0; + pc.ln.off = -1; + pc.binary = f->binary; + pc.cntlines = false; + memset(&mc, 0, sizeof(mc)); + mc.printmatch = true; + if ((pc.binary && binbehave == BINFILE_BIN) || grep_cflag || qflag || + lflag || Lflag) + mc.printmatch = false; + if (mc.printmatch && (Aflag != 0 || Bflag != 0)) + mc.doctx = true; + if (mc.printmatch && (Aflag != 0 || Bflag != 0 || mflag || nflag)) + pc.cntlines = true; + mcount = mlimit; + + for (lines = 0; lines == 0 || !(lflag || qflag); ) { + /* + * XXX TODO: We need to revisit this in a chunking world. We're + * not going to be doing per-line statistics because of the + * overhead involved. procmatches can figure that stuff out as + * needed. */ + /* Reset per-line statistics */ + pc.printed = 0; + pc.matchidx = 0; + pc.lnstart = 0; + pc.ln.boff = 0; + pc.ln.off += pc.ln.len + 1; + /* XXX TODO: Grab a chunk */ + if ((pc.ln.dat = grep_fgetln(f, &pc)) == NULL || + pc.ln.len == 0) + break; + + if (pc.ln.len > 0 && pc.ln.dat[pc.ln.len - 1] == fileeol) + --pc.ln.len; + pc.ln.line_no++; + + /* Return if we need to skip a binary file */ + if (pc.binary && binbehave == BINFILE_SKIP) { + grep_close(f); + free(pc.ln.file); + free(f); + return (0); + } + + if (mflag && mcount <= 0) { + /* + * Short-circuit, already hit match count and now we're + * just picking up any remaining pieces. + */ + if (!procmatches(&mc, &pc, false)) + break; + continue; + } + line_matched = procline(&pc) == !vflag; + if (line_matched) + ++lines; + + /* Halt processing if we hit our match limit */ + if (!procmatches(&mc, &pc, line_matched)) + break; + } + if (Bflag > 0) + clearqueue(); + grep_close(f); + + if (grep_cflag && !qflag) { + if (!hflag) + printf("%s:", pc.ln.file); + printf("%u\n", lines); + } + if (lflag && !qflag && lines != 0) + printf("%s%c", fn, nullflag ? 0 : '\n'); + if (Lflag && !qflag && lines == 0) + printf("%s%c", fn, nullflag ? 0 : '\n'); + if (lines != 0 && !grep_cflag && !lflag && !Lflag && + binbehave == BINFILE_BIN && f->binary && !qflag) + printf(errstr[7], fn); + + free(pc.ln.file); + free(f); + return (lines != 0); +} + +#ifdef WITH_INTERNAL_NOSPEC +/* + * Internal implementation of literal string search within a string, modeled + * after regexec(3), for use when the regex(3) implementation doesn't offer + * either REG_NOSPEC or REG_LITERAL. This does not apply in the default FreeBSD + * config, but in other scenarios such as building against libgnuregex or on + * some non-FreeBSD OSes. + */ +static int +litexec(const struct pat *pat, const char *string, size_t nmatch, + regmatch_t pmatch[]) +{ + char *(*strstr_fn)(const char *, const char *); + char *sub, *subject; + const char *search; + size_t idx, n, ofs, stringlen; + + if (cflags & REG_ICASE) + strstr_fn = strcasestr; + else + strstr_fn = strstr; + idx = 0; + ofs = pmatch[0].rm_so; + stringlen = pmatch[0].rm_eo; + if (ofs >= stringlen) + return (REG_NOMATCH); + subject = strndup(string, stringlen); + if (subject == NULL) + return (REG_ESPACE); + for (n = 0; ofs < stringlen;) { + search = (subject + ofs); + if ((unsigned long)pat->len > strlen(search)) + break; + sub = strstr_fn(search, pat->pat); + /* + * Ignoring the empty string possibility due to context: grep optimizes + * for empty patterns and will never reach this point. + */ + if (sub == NULL) + break; + ++n; + /* Fill in pmatch if necessary */ + if (nmatch > 0) { + pmatch[idx].rm_so = ofs + (sub - search); + pmatch[idx].rm_eo = pmatch[idx].rm_so + pat->len; + if (++idx == nmatch) + break; + ofs = pmatch[idx].rm_so + 1; + } else + /* We only needed to know if we match or not */ + break; + } + free(subject); + if (n > 0 && nmatch > 0) + for (n = idx; n < nmatch; ++n) + pmatch[n].rm_so = pmatch[n].rm_eo = -1; + + return (n > 0 ? 0 : REG_NOMATCH); +} +#endif /* WITH_INTERNAL_NOSPEC */ + +#define iswword(x) (iswalnum((x)) || (x) == L'_') + +/* + * Processes a line comparing it with the specified patterns. Each pattern + * is looped to be compared along with the full string, saving each and every + * match, which is necessary to colorize the output and to count the + * matches. The matching lines are passed to printline() to display the + * appropriate output. + */ +static bool +procline(struct parsec *pc) +{ + regmatch_t pmatch, lastmatch, chkmatch; + wchar_t wbegin, wend; + size_t st, nst; + unsigned int i; + int r = 0, leflags = eflags; + size_t startm = 0, matchidx; + unsigned int retry; + bool lastmatched, matched; + + matchidx = pc->matchidx; + + /* Null pattern shortcuts. */ + if (matchall) { + if (grep_xflag && pc->ln.len == 0) { + /* Matches empty lines (-x). */ + return (true); + } else if (!wflag && !grep_xflag) { + /* Matches every line (no -w or -x). */ + return (true); + } + + /* + * If we only have the NULL pattern, whether we match or not + * depends on if we got here with -w or -x. If either is set, + * the answer is no. If we have other patterns, we'll defer + * to them. + */ + if (patterns == 0) { + return (!(wflag || grep_xflag)); + } + } else if (patterns == 0) { + /* Pattern file with no patterns. */ + return (false); + } + + matched = false; + st = pc->lnstart; + nst = 0; + /* Initialize to avoid a false positive warning from GCC. */ + lastmatch.rm_so = lastmatch.rm_eo = 0; + + /* Loop to process the whole line */ + while (st <= pc->ln.len) { + lastmatched = false; + startm = matchidx; + retry = 0; + if (st > 0 && pc->ln.dat[st - 1] != fileeol) + leflags |= REG_NOTBOL; + /* Loop to compare with all the patterns */ + for (i = 0; i < patterns; i++) { + pmatch.rm_so = st; + pmatch.rm_eo = pc->ln.len; +#ifdef WITH_INTERNAL_NOSPEC + if (grepbehave == GREP_FIXED) + r = litexec(&pattern[i], pc->ln.dat, 1, &pmatch); + else +#endif + r = regexec(&r_pattern[i], pc->ln.dat, 1, &pmatch, + leflags); + if (r != 0) + continue; + /* Check for full match */ + if (grep_xflag && (pmatch.rm_so != 0 || + (size_t)pmatch.rm_eo != pc->ln.len)) + continue; + /* Check for whole word match */ + if (wflag) { + wbegin = wend = L' '; + if (pmatch.rm_so != 0 && + sscanf(&pc->ln.dat[pmatch.rm_so - 1], + "%lc", &wbegin) != 1) + r = REG_NOMATCH; + else if ((size_t)pmatch.rm_eo != + pc->ln.len && + sscanf(&pc->ln.dat[pmatch.rm_eo], + "%lc", &wend) != 1) + r = REG_NOMATCH; + else if (iswword(wbegin) || + iswword(wend)) + r = REG_NOMATCH; + /* + * If we're doing whole word matching and we + * matched once, then we should try the pattern + * again after advancing just past the start of + * the earliest match. This allows the pattern + * to match later on in the line and possibly + * still match a whole word. + */ + if (r == REG_NOMATCH && + (retry == pc->lnstart || + (unsigned int)pmatch.rm_so + 1 < retry)) + retry = pmatch.rm_so + 1; + if (r == REG_NOMATCH) + continue; + } + lastmatched = true; + lastmatch = pmatch; + + if (matchidx == 0) + matched = true; + + /* + * Replace previous match if the new one is earlier + * and/or longer. This will lead to some amount of + * extra work if -o/--color are specified, but it's + * worth it from a correctness point of view. + */ + if (matchidx > startm) { + chkmatch = pc->matches[matchidx - 1]; + if (pmatch.rm_so < chkmatch.rm_so || + (pmatch.rm_so == chkmatch.rm_so && + (pmatch.rm_eo - pmatch.rm_so) > + (chkmatch.rm_eo - chkmatch.rm_so))) { + pc->matches[matchidx - 1] = pmatch; + nst = pmatch.rm_eo; + } + } else { + /* Advance as normal if not */ + pc->matches[matchidx++] = pmatch; + nst = pmatch.rm_eo; + } + /* avoid excessive matching - skip further patterns */ + if ((grep_color == NULL && !oflag) || qflag || lflag || + matchidx >= MAX_MATCHES) { + pc->lnstart = nst; + lastmatched = false; + break; + } + } + + /* + * Advance to just past the start of the earliest match, try + * again just in case we still have a chance to match later in + * the string. + */ + if (!lastmatched && retry > pc->lnstart) { + st = retry; + continue; + } + + /* XXX TODO: We will need to keep going, since we're chunky */ + /* One pass if we are not recording matches */ + if (!wflag && ((grep_color == NULL && !oflag) || qflag || lflag || Lflag)) + break; + + /* If we didn't have any matches or REG_NOSUB set */ + if (!lastmatched || (cflags & REG_NOSUB)) + nst = pc->ln.len; + + if (!lastmatched) + /* No matches */ + break; + else if (st == nst && lastmatch.rm_so == lastmatch.rm_eo) + /* Zero-length match -- advance one more so we don't get stuck */ + nst++; + + /* Advance st based on previous matches */ + st = nst; + pc->lnstart = st; + } + + /* Reflect the new matchidx in the context */ + pc->matchidx = matchidx; + return matched; +} + +/* + * Safe malloc() for internal use. + */ +void * +grep_malloc(size_t size) +{ + void *ptr; + + if (size == 0) + return (NULL); + if ((ptr = malloc(size)) == NULL) + err(2, "malloc"); + return (ptr); +} + +/* + * Safe calloc() for internal use. + */ +void * +grep_calloc(size_t nmemb, size_t size) +{ + void *ptr; + + if (nmemb == 0 || size == 0) + return (NULL); + if ((ptr = calloc(nmemb, size)) == NULL) + err(2, "calloc"); + return (ptr); +} + +/* + * Safe realloc() for internal use. + */ +void * +grep_realloc(void *ptr, size_t size) +{ + + if ((ptr = realloc(ptr, size)) == NULL) + err(2, "realloc"); + return (ptr); +} + +/* + * Safe strdup() for internal use. + */ +char * +grep_strdup(const char *str) +{ + char *ret; + + if ((ret = strdup(str)) == NULL) + err(2, "strdup"); + return (ret); +} + +/* + * Print an entire line as-is, there are no inline matches to consider. This is + * used for printing context. + */ +void grep_printline(struct str *line, int sep) { + printline_metadata(line, sep); + fwrite(line->dat, line->len, 1, stdout); + putchar(fileeol); +} + +static void +printline_metadata(struct str *line, int sep) +{ + bool printsep; + + printsep = false; + if (!hflag) { + if (!nullflag) { + fputs(line->file, stdout); + printsep = true; + } else { + printf("%s", line->file); + putchar(0); + } + } + if (nflag) { + if (printsep) + putchar(sep); + printf("%d", line->line_no); + printsep = true; + } + if (grep_bflag) { + if (printsep) + putchar(sep); + printf("%lld", (long long)(line->off + line->boff)); + printsep = true; + } + if (printsep) + putchar(sep); +} + +/* + * Prints a matching line according to the command line options. We need + * *last_out to be populated on entry in case this is just a continuation of + * matches within the same line. + * + * Returns true if the line was terminated, false if it was not. + */ +static bool +printline(struct parsec *pc, int sep, size_t *last_out) +{ + size_t a = *last_out; + size_t i, matchidx; + regmatch_t match; + bool terminated; + + /* + * Nearly all paths below will terminate the line by default, but it is + * avoided in some circumstances in case we don't have the full context + * available here. + */ + terminated = true; + + /* If matchall, everything matches but don't actually print for -o */ + if (oflag && matchall) + return (terminated); + + matchidx = pc->matchidx; + + /* --color and -o */ + if ((oflag || grep_color) && (pc->printed > 0 || matchidx > 0)) { + /* Only print metadata once per line if --color */ + if (!oflag && pc->printed == 0) { + printline_metadata(&pc->ln, sep); + } + for (i = 0; i < matchidx; i++) { + match = pc->matches[i]; + /* Don't output zero length matches */ + if (match.rm_so == match.rm_eo) + continue; + /* + * Metadata is printed on a per-line basis, so every + * match gets file metadata with the -o flag. + */ + if (oflag) { + pc->ln.boff = match.rm_so; + printline_metadata(&pc->ln, sep); + } else { + fwrite(pc->ln.dat + a, match.rm_so - a, 1, + stdout); + } + if (grep_color) + fprintf(stdout, "\33[%sm\33[K", grep_color); + fwrite(pc->ln.dat + match.rm_so, + match.rm_eo - match.rm_so, 1, stdout); + if (grep_color) + fprintf(stdout, "\33[m\33[K"); + a = match.rm_eo; + if (oflag) + putchar('\n'); + } + + /* + * Don't terminate if we reached the match limit; we may have + * other matches on this line to process. + */ + *last_out = a; + if (!oflag && matchidx != MAX_MATCHES) { + if (pc->ln.len - a > 0) { + fwrite(pc->ln.dat + a, pc->ln.len - a, 1, + stdout); + *last_out = pc->ln.len; + } + putchar('\n'); + } else if (!oflag) { + /* + * -o is terminated on every match output, so this + * branch is only designed to capture MAX_MATCHES in a + * line which may be a signal to us for a lack of + * context. The caller will know more and call us again + * to terminate if it needs to. + */ + terminated = false; + } + } else + grep_printline(&pc->ln, sep); + pc->printed++; + return (terminated); +} diff --git a/src/gzip/gzexe b/src/gzip/gzexe new file mode 100644 index 0000000..ca96dfc --- /dev/null +++ b/src/gzip/gzexe @@ -0,0 +1,178 @@ +#!/bin/sh - +# +# $NetBSD: gzexe,v 1.3 2004/05/01 08:22:41 wiz Exp $ +# $OpenBSD: gzexe,v 1.3 2003/08/05 18:22:17 deraadt Exp $ +# +#- +# Copyright (c) 2003 Otto Moerbeek +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +# The number of lines plus one in the on-the-fly decompression script +lines=19 + +# A simple string to recognize already compressed files +magic="# compressed by gzexe" + +# Write the decompression script to stdout +header () { + # first section needs variable expansion, second not + cat <<- EOF + #!/bin/sh - + $magic + lines=$lines + EOF + cat <<- 'EOF' + prog=`/usr/bin/basename "$0"` + tmp=`/usr/bin/mktemp -d /tmp/gzexeXXXXXXXXXX` || { + /bin/echo "$prog: cannot create tmp dir"; exit 1 + } + trap '/bin/rm -rf "$tmp"' 0 + if /usr/bin/tail +$lines "$0" | + /usr/bin/gzip -dc > "$tmp/$prog" 2> /dev/null; then + /bin/chmod u+x "$tmp/$prog" + "$tmp/$prog" ${1+"$@"} + ret=$? + else + /bin/echo "$prog: cannot decompress $0" + ret=1 + fi + exit $ret + EOF +} + +# Test if a file is compressed by checking the magic line +compressed () { + test "X`sed -n 2p "$1" 2> /dev/null`" = "X$magic" +} + +# Decompress a file +decompress () { + tmp=`mktemp /tmp/gzexeXXXXXXXXXX` || { + echo "$prog: cannot create tmp file" + return 1 + } + if ! cp "$1" "$tmp"; then + echo "$prog: cannot copy $1 to $tmp" + rm -f "$tmp" + return 1 + fi + if ! tail +$lines "$tmp" | gzip -vdc > "$1"; then + echo "$prog: cannot decompress $1" + cp "$tmp" "$1" + rm -f "$tmp" + return 1 + fi +} + +# Perform some sanity checks on the file +check () { + if test ! -e "$1"; then + echo "$prog: cannot compress non-existing file $1" + return 1 + fi + + if test ! -f "$1"; then + echo "$prog: cannot compress non-regular file $1" + return 1 + fi + + case `basename "$1"` in + sh | mktemp | rm | echo | tail | gzip | chmod) + echo "$prog: cannot compress $1, I depend on it" + return 1 + esac + + if test ! -x "$1"; then + echo "$prog: cannot compress $1, it is not executable" + return 1 + fi + + if test -u "$1" -o -g "$1"; then + echo "$prog: cannot compress $1, it has an s bit set" + return 1 + fi +} + +# Compress a file +compress () { + tmp=`mktemp /tmp/gzexeXXXXXXXXXX` || { + echo "$prog: cannot create tmp file" + return 1 + } + if ! cp "$1" "$tmp"; then + echo "$prog: cannot copy $1 to $tmp" + rm -f "$tmp" + return 1 + fi + if ! cp "$1" "$1"~; then + echo "$prog: cannot create backup copy $1~" + rm -f "$1"~ "$tmp" + return 1 + fi + + # Use cp to overwrite the existing file preserving mode and owner + # if possible. If the file is not writable, this will produce an + # error. + + if header "$1" > "$tmp" && gzip -vc "$1" >> "$tmp"; then + if ! cp "$tmp" "$1"; then + echo "$prog: cannot copy $tmp to $1" + rm -f "$tmp" + return 1 + fi + else + echo "$prog: cannot compress $1" + rm -f "$1"~ "$tmp" + return 1 + fi +} + +# Is the -d flag specified? +dflag= + +# Return value +rc=0 + +if test "X$1" = X-d; then + dflag=1 + shift +fi + +prog=`basename "$0"` +USAGE="usage: $prog [-d] file ..." +if test $# -eq 0; then + echo $USAGE + exit 1 +fi + +while test $# -ne 0; do + if test $dflag; then + if ! compressed "$1"; then + echo "$prog: $1 is not compressed" + rc=1; + elif ! decompress "$1"; then + rc=$? + fi + else + if compressed "$1"; then + echo "$prog: $1 is already compressed" + rc=1; + elif ! check "$1" || ! compress "$1"; then + rc=$? + fi + fi + shift +done +exit $rc diff --git a/src/gzip/gzexe.1 b/src/gzip/gzexe.1 new file mode 100644 index 0000000..7247714 --- /dev/null +++ b/src/gzip/gzexe.1 @@ -0,0 +1,71 @@ +.\" $NetBSD: gzexe.1,v 1.3 2003/12/28 12:49:41 wiz Exp $ +.\" $OpenBSD: gzexe.1,v 1.1 2003/07/31 07:32:47 otto Exp $ +.\" +.\" Copyright (c) 2003 Otto Moerbeek +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.Dd January 26, 2007 +.Dt GZEXE 1 +.Os +.Sh NAME +.Nm gzexe +.Nd create auto-decompressing executables +.Sh SYNOPSIS +.Nm gzexe +.Op Fl d +.Ar +.Sh DESCRIPTION +The +.Nm +utility uses +.Xr gzip 1 +to compress executables, producing executables that decompress on-the-fly +when executed. +This saves disk space, at the cost of slower execution times. +The original executables are saved by copying each of them to a file with +the same name with a +.Sq ~ +suffix appended. +After verifying that the compressed executables work as expected, the backup +files can be removed. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl d +Decompress executables previously compressed by +.Nm . +.El +.Pp +The +.Nm +program refuses to compress non-regular or non-executable files, +files with a setuid or setgid bit set, files that are already +compressed using +.Nm +or programs it needs to perform on-the-fly decompression: +.Xr sh 1 , +.Xr mktemp 1 , +.Xr rm 1 , +.Xr echo 1 , +.Xr tail 1 , +.Xr gzip 1 , +and +.Xr chmod 1 . +.Sh SEE ALSO +.Xr gzip 1 +.Sh CAVEATS +The +.Nm +utility replaces files by overwriting them with the generated +compressed executable. +To be able to do this, it is required that the original files are writable. diff --git a/src/gzip/gzip.1 b/src/gzip/gzip.1 new file mode 100644 index 0000000..6f1a99a --- /dev/null +++ b/src/gzip/gzip.1 @@ -0,0 +1,246 @@ +.\" $NetBSD: gzip.1,v 1.31 2018/10/26 22:10:15 christos Exp $ +.\" +.\" Copyright (c) 1997, 2003, 2004, 2008, 2009, 2015, 2017 Matthew R. Green +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. +.Dd November 2, 2022 +.Dt GZIP 1 +.Os +.Sh NAME +.Nm gzip , +.Nm gunzip , +.Nm zcat +.Nd compression/decompression tool using Lempel-Ziv coding (LZ77) +.Sh SYNOPSIS +.Nm +.Op Fl cdfhkLlNnqrtVv +.Op Fl S Ar suffix +.Ar file +.Oo +.Ar file Oo ... +.Oc +.Oc +.Nm gunzip +.Op Fl cfhkLNqrtVv +.Op Fl S Ar suffix +.Ar file +.Oo +.Ar file Oo ... +.Oc +.Oc +.Nm zcat +.Op Fl fhV +.Ar file +.Oo +.Ar file Oo ... +.Oc +.Oc +.Sh DESCRIPTION +The +.Nm +program compresses and decompresses files using Lempel-Ziv coding +(LZ77). +If no +.Ar files +are specified, +.Nm +will compress from standard input, or decompress to standard output. +When in compression mode, each +.Ar file +will be replaced with another file with the suffix, set by the +.Fl S Ar suffix +option, added, if possible. +.Pp +In decompression mode, each +.Ar file +will be checked for existence, as will the file with the suffix +added. +Each +.Ar file +argument must contain a separate complete archive; +when multiple +.Ar files +are indicated, each is decompressed in turn. +.Pp +In the case of +.Nm gzcat +the resulting data is then concatenated in the manner of +.Xr cat 1 . +.Pp +If invoked as +.Nm gunzip +then the +.Fl d +option is enabled. +If invoked as +.Nm zcat +or +.Nm gzcat +then both the +.Fl c +and +.Fl d +options are enabled. +.Pp +This version of +.Nm +is also capable of decompressing files compressed using +.Xr compress 1 , +.Xr bzip2 1 , +.Ar lzip , +.Xr zstd 1 , +or +.Xr xz 1 . +.Sh OPTIONS +The following options are available: +.Bl -tag -width XXrXXXrecursiveX +.It Fl 1 , Fl Fl fast +.It Fl 2 , 3 , 4 , 5 , 6 , 7 , 8 +.It Fl 9 , Fl Fl best +These options change the compression level used, with the +.Fl 1 +option being the fastest, with less compression, and the +.Fl 9 +option being the slowest, with optimal compression. +The default compression level is 6. +.It Fl c , Fl Fl stdout , Fl Fl to-stdout +This option specifies that output will go to the standard output +stream, leaving files intact. +.It Fl d , Fl Fl decompress , Fl Fl uncompress +This option selects decompression rather than compression. +.It Fl f , Fl Fl force +This option turns on force mode. +This allows files with multiple links, symbolic links to regular files, +overwriting of pre-existing files, reading from or writing to a terminal, +and when combined with the +.Fl c +option, allowing non-compressed data to pass through unchanged. +.It Fl h , Fl Fl help +This option prints a usage summary and exits. +.It Fl k , Fl Fl keep +This option prevents +.Nm +from deleting input files after (de)compression. +.It Fl L , -license +This option prints +.Nm +license. +.It Fl l , Fl Fl list +This option displays information about the file's compressed and +uncompressed size, ratio, uncompressed name. +With the +.Fl v +option, it also displays the compression method, CRC, date and time +embedded in the file. +.It Fl N , Fl Fl name +This option causes the stored filename in the input file to be used +as the output file. +.It Fl n , Fl Fl no-name +This option stops the filename and timestamp from being stored in +the output file. +.It Fl q , Fl Fl quiet +With this option, no warnings or errors are printed. +.It Fl r , Fl Fl recursive +This option is used to +.Nm +the files in a directory tree individually, using the +.Xr fts 3 +library. +.It Fl S Ar suffix , Fl Fl suffix Ar suffix +This option changes the default suffix from .gz to +.Ar suffix . +.It Fl t , Fl Fl test +This option will test compressed files for integrity. +.It Fl V , Fl Fl version +This option prints the version of the +.Nm +program. +.It Fl v , Fl Fl verbose +This option turns on verbose mode, which prints the compression +ratio for each file compressed. +.El +.Sh ENVIRONMENT +If the environment variable +.Ev GZIP +is set, it is parsed as a white-space separated list of options +handled before any options on the command line. +Options on the command line will override anything in +.Ev GZIP . +.Sh EXIT STATUS +The +.Nm +utility exits 0 on success, +1 on errors, +and 2 if a warning occurs. +.Sh SIGNALS +.Nm +responds to the following signals: +.Bl -tag -width indent +.It Dv SIGINFO +Report progress to standard error. +.El +.Sh SEE ALSO +.Xr bzip2 1 , +.Xr compress 1 , +.Xr xz 1 , +.Xr zstd 1 , +.Xr fts 3 , +.Xr zlib 3 +.Sh HISTORY +The +.Nm +program was originally written by Jean-loup Gailly, licensed under +the GNU Public Licence. +Matthew R. Green wrote a simple front end for +.Nx 1.3 +distribution media, based on the freely re-distributable zlib library. +It was enhanced to be mostly feature-compatible with the original +GNU +.Nm +program for +.Nx 2.0 . +.Pp +This implementation of +.Nm +was ported based on the +.Nx +.Nm +version 20181111, +and first appeared in +.Fx 7.0 . +.Sh AUTHORS +.An -nosplit +This implementation of +.Nm +was written by +.An Matthew R. Green Aq Mt mrg@eterna.com.au +with unpack support written by +.An Xin LI Aq Mt delphij@FreeBSD.org . +.Sh BUGS +According to RFC 1952, the recorded file size is stored in a 32-bit +integer, therefore, it cannot represent files larger than 4GB. +This limitation also applies to +.Fl l +option of +.Nm +utility. diff --git a/src/gzip/gzip.c b/src/gzip/gzip.c new file mode 100644 index 0000000..25d2f84 --- /dev/null +++ b/src/gzip/gzip.c @@ -0,0 +1,2305 @@ +/* $NetBSD: gzip.c,v 1.116 2018/10/27 11:39:12 skrll Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 1997, 1998, 2003, 2004, 2006, 2008, 2009, 2010, 2011, 2015, 2017 + * Matthew R. Green + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. + * + */ + +/* + * gzip.c -- GPL free gzip using zlib. + * + * RFC 1950 covers the zlib format + * RFC 1951 covers the deflate format + * RFC 1952 covers the gzip format + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Linux portability */ +#ifndef __dead2 +#define __dead2 __attribute__((__noreturn__)) +#endif +#ifndef __printflike +#define __printflike(a, b) __attribute__((__format__(__printf__, a, b))) +#endif +#ifndef __unused +#define __unused __attribute__((__unused__)) +#endif +#ifndef nitems +#define nitems(x) (sizeof((x)) / sizeof((x)[0])) +#endif +#ifndef MIN +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#endif +#ifndef SSIZE_MAX +#define SSIZE_MAX ((ssize_t)(SIZE_MAX / 2)) +#endif + +/* BSD le32dec: decode a little-endian 32-bit value from a byte buffer */ +static inline uint32_t +le32dec(const void *pp) +{ + const unsigned char *p = pp; + return ((uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24)); +} + +/* what type of file are we dealing with */ +enum filetype { + FT_GZIP, +#ifndef NO_BZIP2_SUPPORT + FT_BZIP2, +#endif +#ifndef NO_COMPRESS_SUPPORT + FT_Z, +#endif +#ifndef NO_PACK_SUPPORT + FT_PACK, +#endif +#ifndef NO_XZ_SUPPORT + FT_XZ, +#endif +#ifndef NO_LZ_SUPPORT + FT_LZ, +#endif +#ifndef NO_ZSTD_SUPPORT + FT_ZSTD, +#endif + FT_LAST, + FT_UNKNOWN +}; + +#ifndef NO_BZIP2_SUPPORT +#include + +#define BZ2_SUFFIX ".bz2" +#define BZIP2_MAGIC "BZh" +#endif + +#ifndef NO_COMPRESS_SUPPORT +#define Z_SUFFIX ".Z" +#define Z_MAGIC "\037\235" +#endif + +#ifndef NO_PACK_SUPPORT +#define PACK_MAGIC "\037\036" +#endif + +#ifndef NO_XZ_SUPPORT +#include +#define XZ_SUFFIX ".xz" +#define XZ_MAGIC "\3757zXZ" +#endif + +#ifndef NO_LZ_SUPPORT +#define LZ_SUFFIX ".lz" +#define LZ_MAGIC "LZIP" +#endif + +#ifndef NO_ZSTD_SUPPORT +#include +#define ZSTD_SUFFIX ".zst" +#define ZSTD_MAGIC "\050\265\057\375" +#endif + +#define GZ_SUFFIX ".gz" + +#define BUFLEN (64 * 1024) + +#define GZIP_MAGIC0 0x1F +#define GZIP_MAGIC1 0x8B +#define GZIP_OMAGIC1 0x9E + +#define GZIP_TIMESTAMP (off_t)4 +#define GZIP_ORIGNAME (off_t)10 + +#define HEAD_CRC 0x02 +#define EXTRA_FIELD 0x04 +#define ORIG_NAME 0x08 +#define COMMENT 0x10 + +#define OS_CODE 3 /* Unix */ + +typedef struct { + const char *zipped; + int ziplen; + const char *normal; /* for unzip - must not be longer than zipped */ +} suffixes_t; +static suffixes_t suffixes[] = { +#define SUFFIX(Z, N) {Z, sizeof Z - 1, N} + SUFFIX(GZ_SUFFIX, ""), /* Overwritten by -S .xxx */ + SUFFIX(GZ_SUFFIX, ""), + SUFFIX(".z", ""), + SUFFIX("-gz", ""), + SUFFIX("-z", ""), + SUFFIX("_z", ""), + SUFFIX(".taz", ".tar"), + SUFFIX(".tgz", ".tar"), +#ifndef NO_BZIP2_SUPPORT + SUFFIX(BZ2_SUFFIX, ""), + SUFFIX(".tbz", ".tar"), + SUFFIX(".tbz2", ".tar"), +#endif +#ifndef NO_COMPRESS_SUPPORT + SUFFIX(Z_SUFFIX, ""), +#endif +#ifndef NO_XZ_SUPPORT + SUFFIX(XZ_SUFFIX, ""), +#endif +#ifndef NO_LZ_SUPPORT + SUFFIX(LZ_SUFFIX, ""), +#endif +#ifndef NO_ZSTD_SUPPORT + SUFFIX(ZSTD_SUFFIX, ""), +#endif + SUFFIX(GZ_SUFFIX, ""), /* Overwritten by -S "" */ +#undef SUFFIX +}; +#define NUM_SUFFIXES (nitems(suffixes)) +#define SUFFIX_MAXLEN 30 + +static const char gzip_version[] = "FreeBSD gzip 20190107"; + +static const char gzip_copyright[] = \ +" Copyright (c) 1997, 1998, 2003, 2004, 2006 Matthew R. Green\n" +" All rights reserved.\n" +"\n" +" Redistribution and use in source and binary forms, with or without\n" +" modification, are permitted provided that the following conditions\n" +" are met:\n" +" 1. Redistributions of source code must retain the above copyright\n" +" notice, this list of conditions and the following disclaimer.\n" +" 2. Redistributions in binary form must reproduce the above copyright\n" +" notice, this list of conditions and the following disclaimer in the\n" +" documentation and/or other materials provided with the distribution.\n" +"\n" +" THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n" +" IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n" +" OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n" +" IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n" +" INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,\n" +" BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n" +" LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED\n" +" AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n" +" OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\n" +" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\n" +" SUCH DAMAGE."; + +static int cflag; /* stdout mode */ +static int dflag; /* decompress mode */ +static int lflag; /* list mode */ +static int numflag = 6; /* gzip -1..-9 value */ + +static const char *remove_file = NULL; /* file to be removed upon SIGINT */ + +static int fflag; /* force mode */ +static int kflag; /* don't delete input files */ +static int nflag; /* don't save name/timestamp */ +static int Nflag; /* don't restore name/timestamp */ +static int qflag; /* quiet mode */ +static int rflag; /* recursive mode */ +static int tflag; /* test */ +static int vflag; /* verbose mode */ +static sig_atomic_t print_info = 0; + +static int exit_value = 0; /* exit value */ + +static const char *infile; /* name of file coming in */ + +static void maybe_err(const char *fmt, ...) __printflike(1, 2) __dead2; +#if !defined(NO_BZIP2_SUPPORT) || !defined(NO_PACK_SUPPORT) || \ + !defined(NO_XZ_SUPPORT) || !defined(NO_ZSTD_SUPPORT) +static void maybe_errx(const char *fmt, ...) __printflike(1, 2) __dead2; +#endif +static void maybe_warn(const char *fmt, ...) __printflike(1, 2); +static void maybe_warnx(const char *fmt, ...) __printflike(1, 2); +static enum filetype file_gettype(u_char *); +static off_t gz_compress(int, int, off_t *, const char *, uint32_t); +static off_t gz_uncompress(int, int, char *, size_t, off_t *, const char *); +static off_t file_compress(char *, char *, size_t); +static off_t file_uncompress(char *, char *, size_t); +static void handle_pathname(char *); +static void handle_file(char *, struct stat *); +static void handle_stdin(void); +static void handle_stdout(void); +static void print_ratio(off_t, off_t, FILE *); +static void print_list(int fd, off_t, const char *, time_t); +static void usage(void) __dead2; +static void display_version(void) __dead2; +static void display_license(void); +static const suffixes_t *check_suffix(char *, int); +static ssize_t read_retry(int, void *, size_t); +static ssize_t write_retry(int, const void *, size_t); +static void print_list_out(off_t, off_t, const char*); + +static void infile_set(const char *newinfile, off_t total); + +static off_t infile_total; /* total expected to read/write */ +static off_t infile_current; /* current read/write */ + +static void check_siginfo(void); +static off_t cat_fd(unsigned char *, size_t, off_t *, int fd); +static void prepend_gzip(char *, int *, char ***); +static void handle_dir(char *); +static void print_verbage(const char *, const char *, off_t, off_t); +static void print_test(const char *, int); +static void copymodes(int fd, const struct stat *, const char *file); +static int check_outfile(const char *outfile); +static void setup_signals(void); +static void infile_newdata(size_t newdata); +static void infile_clear(void); + +#ifndef NO_BZIP2_SUPPORT +static off_t unbzip2(int, int, char *, size_t, off_t *); +#endif + +#ifndef NO_COMPRESS_SUPPORT +static FILE *zdopen(int); +static off_t zuncompress(FILE *, FILE *, char *, size_t, off_t *); +#endif + +#ifndef NO_PACK_SUPPORT +static off_t unpack(int, int, char *, size_t, off_t *); +#endif + +#ifndef NO_XZ_SUPPORT +static off_t unxz(int, int, char *, size_t, off_t *); +static off_t unxz_len(int); +#endif + +#ifndef NO_LZ_SUPPORT +static off_t unlz(int, int, char *, size_t, off_t *); +#endif + +#ifndef NO_ZSTD_SUPPORT +static off_t unzstd(int, int, char *, size_t, off_t *); +#endif + +static const struct option longopts[] = { + { "stdout", no_argument, 0, 'c' }, + { "to-stdout", no_argument, 0, 'c' }, + { "decompress", no_argument, 0, 'd' }, + { "uncompress", no_argument, 0, 'd' }, + { "force", no_argument, 0, 'f' }, + { "help", no_argument, 0, 'h' }, + { "keep", no_argument, 0, 'k' }, + { "list", no_argument, 0, 'l' }, + { "no-name", no_argument, 0, 'n' }, + { "name", no_argument, 0, 'N' }, + { "quiet", no_argument, 0, 'q' }, + { "recursive", no_argument, 0, 'r' }, + { "suffix", required_argument, 0, 'S' }, + { "test", no_argument, 0, 't' }, + { "verbose", no_argument, 0, 'v' }, + { "version", no_argument, 0, 'V' }, + { "fast", no_argument, 0, '1' }, + { "best", no_argument, 0, '9' }, + { "ascii", no_argument, 0, 'a' }, + { "license", no_argument, 0, 'L' }, + { NULL, no_argument, 0, 0 }, +}; + +int +gzip_main(int argc, char **argv) +{ + const char *progname; + char *gzip; + int len; + int ch; + + /* Reset all global state for multicall use */ + cflag = dflag = lflag = 0; + numflag = 6; + remove_file = NULL; + fflag = kflag = nflag = Nflag = qflag = rflag = tflag = vflag = 0; + print_info = 0; + exit_value = 0; + infile = NULL; + infile_total = infile_current = 0; + optind = 1; + + setup_signals(); + + progname = basename(argv[0]); + + if ((gzip = getenv("GZIP")) != NULL) + prepend_gzip(gzip, &argc, &argv); + + /* + * XXX + * handle being called `gunzip', `zcat' and `gzcat' + */ + if (strcmp(progname, "gunzip") == 0) + dflag = 1; + else if (strcmp(progname, "zcat") == 0 || + strcmp(progname, "gzcat") == 0) + dflag = cflag = 1; + +#define OPT_LIST "123456789acdfhklLNnqrS:tVv" + + while ((ch = getopt_long(argc, argv, OPT_LIST, longopts, NULL)) != -1) { + switch (ch) { + case '1': case '2': case '3': + case '4': case '5': case '6': + case '7': case '8': case '9': + numflag = ch - '0'; + break; + case 'c': + cflag = 1; + break; + case 'd': + dflag = 1; + break; + case 'l': + lflag = 1; + dflag = 1; + break; + case 'V': + display_version(); + /* NOTREACHED */ + case 'a': + fprintf(stderr, "%s: option --ascii ignored on this system\n", progname); + break; + case 'f': + fflag = 1; + break; + case 'k': + kflag = 1; + break; + case 'L': + display_license(); + /* NOTREACHED */ + break; + case 'N': + nflag = 0; + Nflag = 1; + break; + case 'n': + nflag = 1; + Nflag = 0; + break; + case 'q': + qflag = 1; + break; + case 'r': + rflag = 1; + break; + case 'S': + len = strlen(optarg); + if (len != 0) { + if (len > SUFFIX_MAXLEN) + errx(1, "incorrect suffix: '%s': too long", optarg); + suffixes[0].zipped = optarg; + suffixes[0].ziplen = len; + } else { + suffixes[NUM_SUFFIXES - 1].zipped = ""; + suffixes[NUM_SUFFIXES - 1].ziplen = 0; + } + break; + case 't': + cflag = 1; + tflag = 1; + dflag = 1; + break; + case 'v': + vflag = 1; + break; + default: + usage(); + /* NOTREACHED */ + } + } + argv += optind; + argc -= optind; + + if (argc == 0) { + if (dflag) /* stdin mode */ + handle_stdin(); + else /* stdout mode */ + handle_stdout(); + } else { + do { + handle_pathname(argv[0]); + } while (*++argv); + } + if (qflag == 0 && lflag && argc > 1) + print_list(-1, 0, "(totals)", 0); + return exit_value; +} + +/* maybe print a warning */ +void +maybe_warn(const char *fmt, ...) +{ + va_list ap; + + if (qflag == 0) { + va_start(ap, fmt); + vwarn(fmt, ap); + va_end(ap); + } + if (exit_value == 0) + exit_value = 1; +} + +/* ... without an errno. */ +void +maybe_warnx(const char *fmt, ...) +{ + va_list ap; + + if (qflag == 0) { + va_start(ap, fmt); + vwarnx(fmt, ap); + va_end(ap); + } + if (exit_value == 0) + exit_value = 1; +} + +/* maybe print an error */ +void +maybe_err(const char *fmt, ...) +{ + va_list ap; + + if (qflag == 0) { + va_start(ap, fmt); + vwarn(fmt, ap); + va_end(ap); + } + exit(2); +} + +#if !defined(NO_BZIP2_SUPPORT) || !defined(NO_PACK_SUPPORT) || \ + !defined(NO_XZ_SUPPORT) || !defined(NO_ZSTD_SUPPORT) +/* ... without an errno. */ +void +maybe_errx(const char *fmt, ...) +{ + va_list ap; + + if (qflag == 0) { + va_start(ap, fmt); + vwarnx(fmt, ap); + va_end(ap); + } + exit(2); +} +#endif + +/* split up $GZIP and prepend it to the argument list */ +static void +prepend_gzip(char *gzip, int *argc, char ***argv) +{ + char *s, **nargv, **ac; + int nenvarg = 0, i; + + /* scan how many arguments there are */ + for (s = gzip;;) { + while (*s == ' ' || *s == '\t') + s++; + if (*s == 0) + goto count_done; + nenvarg++; + while (*s != ' ' && *s != '\t') + if (*s++ == 0) + goto count_done; + } +count_done: + /* punt early */ + if (nenvarg == 0) + return; + + *argc += nenvarg; + ac = *argv; + + nargv = (char **)malloc((*argc + 1) * sizeof(char *)); + if (nargv == NULL) + maybe_err("malloc"); + + /* stash this away */ + *argv = nargv; + + /* copy the program name first */ + i = 0; + nargv[i++] = *(ac++); + + /* take a copy of $GZIP and add it to the array */ + s = strdup(gzip); + if (s == NULL) + maybe_err("strdup"); + for (;;) { + /* Skip whitespaces. */ + while (*s == ' ' || *s == '\t') + s++; + if (*s == 0) + goto copy_done; + nargv[i++] = s; + /* Find the end of this argument. */ + while (*s != ' ' && *s != '\t') + if (*s++ == 0) + /* Argument followed by NUL. */ + goto copy_done; + /* Terminate by overwriting ' ' or '\t' with NUL. */ + *s++ = 0; + } +copy_done: + + /* copy the original arguments and a NULL */ + while (*ac) + nargv[i++] = *(ac++); + nargv[i] = NULL; +} + +/* compress input to output. Return bytes read, -1 on error */ +static off_t +gz_compress(int in, int out, off_t *gsizep, const char *origname, uint32_t mtime) +{ + z_stream z; + char *outbufp, *inbufp; + off_t in_tot = 0, out_tot = 0; + ssize_t in_size; + int i, error; + uLong crc; + + outbufp = malloc(BUFLEN); + inbufp = malloc(BUFLEN); + if (outbufp == NULL || inbufp == NULL) { + maybe_err("malloc failed"); + goto out; + } + + memset(&z, 0, sizeof z); + z.zalloc = Z_NULL; + z.zfree = Z_NULL; + z.opaque = 0; + + if (nflag != 0) { + mtime = 0; + origname = ""; + } + + i = snprintf(outbufp, BUFLEN, "%c%c%c%c%c%c%c%c%c%c%s", + GZIP_MAGIC0, GZIP_MAGIC1, Z_DEFLATED, + *origname ? ORIG_NAME : 0, + mtime & 0xff, + (mtime >> 8) & 0xff, + (mtime >> 16) & 0xff, + (mtime >> 24) & 0xff, + numflag == 1 ? 4 : numflag == 9 ? 2 : 0, + OS_CODE, origname); + if (i >= BUFLEN) + /* this need PATH_MAX > BUFLEN ... */ + maybe_err("snprintf"); + if (*origname) + i++; + + z.next_out = (unsigned char *)outbufp + i; + z.avail_out = BUFLEN - i; + + error = deflateInit2(&z, numflag, Z_DEFLATED, + (-MAX_WBITS), 8, Z_DEFAULT_STRATEGY); + if (error != Z_OK) { + maybe_warnx("deflateInit2 failed"); + in_tot = -1; + goto out; + } + + crc = crc32(0L, Z_NULL, 0); + for (;;) { + if (z.avail_out == 0) { + if (write_retry(out, outbufp, BUFLEN) != BUFLEN) { + maybe_warn("write"); + out_tot = -1; + goto out; + } + + out_tot += BUFLEN; + z.next_out = (unsigned char *)outbufp; + z.avail_out = BUFLEN; + } + + if (z.avail_in == 0) { + in_size = read(in, inbufp, BUFLEN); + if (in_size < 0) { + maybe_warn("read"); + in_tot = -1; + goto out; + } + if (in_size == 0) + break; + infile_newdata(in_size); + + crc = crc32(crc, (const Bytef *)inbufp, (unsigned)in_size); + in_tot += in_size; + z.next_in = (unsigned char *)inbufp; + z.avail_in = in_size; + } + + error = deflate(&z, Z_NO_FLUSH); + if (error != Z_OK && error != Z_STREAM_END) { + maybe_warnx("deflate failed"); + in_tot = -1; + goto out; + } + } + + /* clean up */ + for (;;) { + size_t len; + ssize_t w; + + error = deflate(&z, Z_FINISH); + if (error != Z_OK && error != Z_STREAM_END) { + maybe_warnx("deflate failed"); + in_tot = -1; + goto out; + } + + len = (char *)z.next_out - outbufp; + + w = write_retry(out, outbufp, len); + if (w == -1 || (size_t)w != len) { + maybe_warn("write"); + out_tot = -1; + goto out; + } + out_tot += len; + z.next_out = (unsigned char *)outbufp; + z.avail_out = BUFLEN; + + if (error == Z_STREAM_END) + break; + } + + if (deflateEnd(&z) != Z_OK) { + maybe_warnx("deflateEnd failed"); + in_tot = -1; + goto out; + } + + i = snprintf(outbufp, BUFLEN, "%c%c%c%c%c%c%c%c", + (int)crc & 0xff, + (int)(crc >> 8) & 0xff, + (int)(crc >> 16) & 0xff, + (int)(crc >> 24) & 0xff, + (int)in_tot & 0xff, + (int)(in_tot >> 8) & 0xff, + (int)(in_tot >> 16) & 0xff, + (int)(in_tot >> 24) & 0xff); + if (i != 8) + maybe_err("snprintf"); + if (write_retry(out, outbufp, i) != i) { + maybe_warn("write"); + in_tot = -1; + } else + out_tot += i; + +out: + if (inbufp != NULL) + free(inbufp); + if (outbufp != NULL) + free(outbufp); + if (gsizep) + *gsizep = out_tot; + return in_tot; +} + +/* + * uncompress input to output then close the input. return the + * uncompressed size written, and put the compressed sized read + * into `*gsizep'. + */ +static off_t +gz_uncompress(int in, int out, char *pre, size_t prelen, off_t *gsizep, + const char *filename) +{ + z_stream z; + char *outbufp, *inbufp; + off_t out_tot = -1, in_tot = 0; + uint32_t out_sub_tot = 0; + enum { + GZSTATE_MAGIC0, + GZSTATE_MAGIC1, + GZSTATE_METHOD, + GZSTATE_FLAGS, + GZSTATE_SKIPPING, + GZSTATE_EXTRA, + GZSTATE_EXTRA2, + GZSTATE_EXTRA3, + GZSTATE_ORIGNAME, + GZSTATE_COMMENT, + GZSTATE_HEAD_CRC1, + GZSTATE_HEAD_CRC2, + GZSTATE_INIT, + GZSTATE_READ, + GZSTATE_CRC, + GZSTATE_LEN, + } state = GZSTATE_MAGIC0; + int flags = 0, skip_count = 0; + int error = Z_STREAM_ERROR, done_reading = 0; + uLong crc = 0; + ssize_t wr; + int needmore = 0; + +#define ADVANCE() { z.next_in++; z.avail_in--; } + + if ((outbufp = malloc(BUFLEN)) == NULL) { + maybe_err("malloc failed"); + goto out2; + } + if ((inbufp = malloc(BUFLEN)) == NULL) { + maybe_err("malloc failed"); + goto out1; + } + + memset(&z, 0, sizeof z); + z.avail_in = prelen; + z.next_in = (unsigned char *)pre; + z.avail_out = BUFLEN; + z.next_out = (unsigned char *)outbufp; + z.zalloc = NULL; + z.zfree = NULL; + z.opaque = 0; + + in_tot = prelen; + out_tot = 0; + + for (;;) { + check_siginfo(); + if ((z.avail_in == 0 || needmore) && done_reading == 0) { + ssize_t in_size; + + if (z.avail_in > 0) { + memmove(inbufp, z.next_in, z.avail_in); + } + z.next_in = (unsigned char *)inbufp; + in_size = read(in, z.next_in + z.avail_in, + BUFLEN - z.avail_in); + + if (in_size == -1) { + maybe_warn("failed to read stdin"); + goto stop_and_fail; + } else if (in_size == 0) { + done_reading = 1; + } + infile_newdata(in_size); + + z.avail_in += in_size; + needmore = 0; + + in_tot += in_size; + } + if (z.avail_in == 0) { + if (done_reading && state != GZSTATE_MAGIC0) { + maybe_warnx("%s: unexpected end of file", + filename); + goto stop_and_fail; + } + goto stop; + } + switch (state) { + case GZSTATE_MAGIC0: + if (*z.next_in != GZIP_MAGIC0) { + if (in_tot > 0) { + maybe_warnx("%s: trailing garbage " + "ignored", filename); + exit_value = 2; + goto stop; + } + maybe_warnx("input not gziped (MAGIC0)"); + goto stop_and_fail; + } + ADVANCE(); + state++; + out_sub_tot = 0; + crc = crc32(0L, Z_NULL, 0); + break; + + case GZSTATE_MAGIC1: + if (*z.next_in != GZIP_MAGIC1 && + *z.next_in != GZIP_OMAGIC1) { + maybe_warnx("input not gziped (MAGIC1)"); + goto stop_and_fail; + } + ADVANCE(); + state++; + break; + + case GZSTATE_METHOD: + if (*z.next_in != Z_DEFLATED) { + maybe_warnx("unknown compression method"); + goto stop_and_fail; + } + ADVANCE(); + state++; + break; + + case GZSTATE_FLAGS: + flags = *z.next_in; + ADVANCE(); + skip_count = 6; + state++; + break; + + case GZSTATE_SKIPPING: + if (skip_count > 0) { + skip_count--; + ADVANCE(); + } else + state++; + break; + + case GZSTATE_EXTRA: + if ((flags & EXTRA_FIELD) == 0) { + state = GZSTATE_ORIGNAME; + break; + } + skip_count = *z.next_in; + ADVANCE(); + state++; + break; + + case GZSTATE_EXTRA2: + skip_count |= ((*z.next_in) << 8); + ADVANCE(); + state++; + break; + + case GZSTATE_EXTRA3: + if (skip_count > 0) { + skip_count--; + ADVANCE(); + } else + state++; + break; + + case GZSTATE_ORIGNAME: + if ((flags & ORIG_NAME) == 0) { + state++; + break; + } + if (*z.next_in == 0) + state++; + ADVANCE(); + break; + + case GZSTATE_COMMENT: + if ((flags & COMMENT) == 0) { + state++; + break; + } + if (*z.next_in == 0) + state++; + ADVANCE(); + break; + + case GZSTATE_HEAD_CRC1: + if (flags & HEAD_CRC) + skip_count = 2; + else + skip_count = 0; + state++; + break; + + case GZSTATE_HEAD_CRC2: + if (skip_count > 0) { + skip_count--; + ADVANCE(); + } else + state++; + break; + + case GZSTATE_INIT: + if (inflateInit2(&z, -MAX_WBITS) != Z_OK) { + maybe_warnx("failed to inflateInit"); + goto stop_and_fail; + } + state++; + break; + + case GZSTATE_READ: + error = inflate(&z, Z_FINISH); + switch (error) { + /* Z_BUF_ERROR goes with Z_FINISH... */ + case Z_BUF_ERROR: + if (z.avail_out > 0 && !done_reading) + continue; + + case Z_STREAM_END: + case Z_OK: + break; + + case Z_NEED_DICT: + maybe_warnx("Z_NEED_DICT error"); + goto stop_and_fail; + case Z_DATA_ERROR: + maybe_warnx("data stream error"); + goto stop_and_fail; + case Z_STREAM_ERROR: + maybe_warnx("internal stream error"); + goto stop_and_fail; + case Z_MEM_ERROR: + maybe_warnx("memory allocation error"); + goto stop_and_fail; + + default: + maybe_warn("unknown error from inflate(): %d", + error); + } + wr = BUFLEN - z.avail_out; + + if (wr != 0) { + crc = crc32(crc, (const Bytef *)outbufp, (unsigned)wr); + if ( + /* don't write anything with -t */ + tflag == 0 && + write_retry(out, outbufp, wr) != wr) { + maybe_warn("error writing to output"); + goto stop_and_fail; + } + + out_tot += wr; + out_sub_tot += wr; + } + + if (error == Z_STREAM_END) { + inflateEnd(&z); + state++; + } + + z.next_out = (unsigned char *)outbufp; + z.avail_out = BUFLEN; + + break; + case GZSTATE_CRC: + { + uLong origcrc; + + if (z.avail_in < 4) { + if (!done_reading) { + needmore = 1; + continue; + } + maybe_warnx("truncated input"); + goto stop_and_fail; + } + origcrc = le32dec(&z.next_in[0]); + if (origcrc != crc) { + maybe_warnx("invalid compressed" + " data--crc error"); + goto stop_and_fail; + } + } + + z.avail_in -= 4; + z.next_in += 4; + + if (!z.avail_in && done_reading) { + goto stop; + } + state++; + break; + case GZSTATE_LEN: + { + uLong origlen; + + if (z.avail_in < 4) { + if (!done_reading) { + needmore = 1; + continue; + } + maybe_warnx("truncated input"); + goto stop_and_fail; + } + origlen = le32dec(&z.next_in[0]); + + if (origlen != out_sub_tot) { + maybe_warnx("invalid compressed" + " data--length error"); + goto stop_and_fail; + } + } + + z.avail_in -= 4; + z.next_in += 4; + + if (error < 0) { + maybe_warnx("decompression error"); + goto stop_and_fail; + } + state = GZSTATE_MAGIC0; + break; + } + continue; +stop_and_fail: + out_tot = -1; +stop: + break; + } + if (state > GZSTATE_INIT) + inflateEnd(&z); + + free(inbufp); +out1: + free(outbufp); +out2: + if (gsizep) + *gsizep = in_tot; + return (out_tot); +} + +/* + * set the owner, mode, flags & utimes using the given file descriptor. + * file is only used in possible warning messages. + */ +static void +copymodes(int fd, const struct stat *sbp, const char *file) +{ + struct timespec times[2]; + struct stat sb; + + /* + * If we have no info on the input, give this file some + * default values and return.. + */ + if (sbp == NULL) { + mode_t mask = umask(022); + + (void)fchmod(fd, DEFFILEMODE & ~mask); + (void)umask(mask); + return; + } + sb = *sbp; + + /* if the chown fails, remove set-id bits as-per compress(1) */ + if (fchown(fd, sb.st_uid, sb.st_gid) < 0) { + if (errno != EPERM) + maybe_warn("couldn't fchown: %s", file); + sb.st_mode &= ~(S_ISUID|S_ISGID); + } + + /* we only allow set-id and the 9 normal permission bits */ + sb.st_mode &= S_ISUID | S_ISGID | S_IRWXU | S_IRWXG | S_IRWXO; + if (fchmod(fd, sb.st_mode) < 0) + maybe_warn("couldn't fchmod: %s", file); + + times[0] = sb.st_atim; + times[1] = sb.st_mtim; + if (futimens(fd, times) < 0) + maybe_warn("couldn't futimens: %s", file); + +#ifdef UF_SETTABLE + /* only try flags if they exist already */ + if (sb.st_flags != 0 && fchflags(fd, sb.st_flags) < 0) + maybe_warn("couldn't fchflags: %s", file); +#endif +} + +/* what sort of file is this? */ +static enum filetype +file_gettype(u_char *buf) +{ + + if (buf[0] == GZIP_MAGIC0 && + (buf[1] == GZIP_MAGIC1 || buf[1] == GZIP_OMAGIC1)) + return FT_GZIP; +#ifndef NO_BZIP2_SUPPORT + else if (memcmp(buf, BZIP2_MAGIC, 3) == 0 && + buf[3] >= '0' && buf[3] <= '9') + return FT_BZIP2; +#endif +#ifndef NO_COMPRESS_SUPPORT + else if (memcmp(buf, Z_MAGIC, 2) == 0) + return FT_Z; +#endif +#ifndef NO_PACK_SUPPORT + else if (memcmp(buf, PACK_MAGIC, 2) == 0) + return FT_PACK; +#endif +#ifndef NO_XZ_SUPPORT + else if (memcmp(buf, XZ_MAGIC, 4) == 0) /* XXX: We only have 4 bytes */ + return FT_XZ; +#endif +#ifndef NO_LZ_SUPPORT + else if (memcmp(buf, LZ_MAGIC, 4) == 0) + return FT_LZ; +#endif +#ifndef NO_ZSTD_SUPPORT + else if (memcmp(buf, ZSTD_MAGIC, 4) == 0) + return FT_ZSTD; +#endif + else + return FT_UNKNOWN; +} + +/* check the outfile is OK. */ +static int +check_outfile(const char *outfile) +{ + struct stat sb; + int ok = 1; + + if (lflag == 0 && stat(outfile, &sb) == 0) { + if (fflag) + unlink(outfile); + else if (isatty(STDIN_FILENO)) { + char ans[10] = { 'n', '\0' }; /* default */ + + fprintf(stderr, "%s already exists -- do you wish to " + "overwrite (y or n)? " , outfile); + (void)fgets(ans, sizeof(ans) - 1, stdin); + if (ans[0] != 'y' && ans[0] != 'Y') { + fprintf(stderr, "\tnot overwriting\n"); + ok = 0; + } else + unlink(outfile); + } else { + maybe_warnx("%s already exists -- skipping", outfile); + ok = 0; + } + } + return ok; +} + +static void +unlink_input(const char *file, const struct stat *sb) +{ + struct stat nsb; + + if (kflag) + return; + if (stat(file, &nsb) != 0) + /* Must be gone already */ + return; + if (nsb.st_dev != sb->st_dev || nsb.st_ino != sb->st_ino) + /* Definitely a different file */ + return; + unlink(file); +} + +static void +got_sigint(int signo __unused) +{ + + if (remove_file != NULL) + unlink(remove_file); + _exit(2); +} + +static void +got_siginfo(int signo __unused) +{ + + print_info = 1; +} + +static void +setup_signals(void) +{ + +#ifdef SIGINFO + signal(SIGINFO, got_siginfo); +#endif + signal(SIGINT, got_sigint); +} + +static void +infile_newdata(size_t newdata) +{ + + infile_current += newdata; +} + +static void +infile_set(const char *newinfile, off_t total) +{ + + if (newinfile) + infile = newinfile; + infile_total = total; +} + +static void +infile_clear(void) +{ + + infile = NULL; + infile_total = infile_current = 0; +} + +static const suffixes_t * +check_suffix(char *file, int xlate) +{ + const suffixes_t *s; + int len = strlen(file); + char *sp; + + for (s = suffixes; s != suffixes + NUM_SUFFIXES; s++) { + /* if it doesn't fit in "a.suf", don't bother */ + if (s->ziplen >= len) + continue; + sp = file + len - s->ziplen; + if (strcmp(s->zipped, sp) != 0) + continue; + if (xlate) + strcpy(sp, s->normal); + return s; + } + return NULL; +} + +/* + * compress the given file: create a corresponding .gz file and remove the + * original. + */ +static off_t +file_compress(char *file, char *outfile, size_t outsize) +{ + int in; + int out; + off_t size, in_size; + struct stat isb, osb; + const suffixes_t *suff; + + in = open(file, O_RDONLY); + if (in == -1) { + maybe_warn("can't open %s", file); + return (-1); + } + + if (fstat(in, &isb) != 0) { + maybe_warn("couldn't stat: %s", file); + close(in); + return (-1); + } + + if (fstat(in, &isb) != 0) { + close(in); + maybe_warn("can't stat %s", file); + return -1; + } + infile_set(file, isb.st_size); + + if (cflag == 0) { + if (isb.st_nlink > 1 && fflag == 0) { + maybe_warnx("%s has %ju other link%s -- " + "skipping", file, + (uintmax_t)isb.st_nlink - 1, + isb.st_nlink == 1 ? "" : "s"); + close(in); + return -1; + } + + if (fflag == 0 && (suff = check_suffix(file, 0)) && + suff->zipped[0] != 0) { + maybe_warnx("%s already has %s suffix -- unchanged", + file, suff->zipped); + close(in); + return (-1); + } + + /* Add (usually) .gz to filename */ + if ((size_t)snprintf(outfile, outsize, "%s%s", + file, suffixes[0].zipped) >= outsize) + memcpy(outfile + outsize - suffixes[0].ziplen - 1, + suffixes[0].zipped, suffixes[0].ziplen + 1); + + if (check_outfile(outfile) == 0) { + close(in); + return (-1); + } + } + + if (cflag == 0) { + out = open(outfile, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (out == -1) { + maybe_warn("could not create output: %s", outfile); + fclose(stdin); + return (-1); + } + remove_file = outfile; + } else + out = STDOUT_FILENO; + + in_size = gz_compress(in, out, &size, basename(file), (uint32_t)isb.st_mtime); + + (void)close(in); + + /* + * If there was an error, in_size will be -1. + * If we compressed to stdout, just return the size. + * Otherwise stat the file and check it is the correct size. + * We only blow away the file if we can stat the output and it + * has the expected size. + */ + if (cflag != 0) + return in_size == -1 ? -1 : size; + + if (fstat(out, &osb) != 0) { + maybe_warn("couldn't stat: %s", outfile); + goto bad_outfile; + } + + if (osb.st_size != size) { + maybe_warnx("output file: %s wrong size (%ju != %ju), deleting", + outfile, (uintmax_t)osb.st_size, (uintmax_t)size); + goto bad_outfile; + } + + copymodes(out, &isb, outfile); + remove_file = NULL; + if (close(out) == -1) + maybe_warn("couldn't close output"); + + /* output is good, ok to delete input */ + unlink_input(file, &isb); + return (size); + + bad_outfile: + if (close(out) == -1) + maybe_warn("couldn't close output"); + + maybe_warnx("leaving original %s", file); + unlink(outfile); + return (size); +} + +/* uncompress the given file and remove the original */ +static off_t +file_uncompress(char *file, char *outfile, size_t outsize) +{ + struct stat isb, osb; + off_t size; + ssize_t rbytes; + unsigned char fourbytes[4]; + enum filetype method; + int fd, ofd, zfd = -1; + int error; + size_t in_size; + ssize_t rv; + time_t timestamp = 0; + char name[PATH_MAX + 1]; + + /* gather the old name info */ + + fd = open(file, O_RDONLY); + if (fd < 0) { + maybe_warn("can't open %s", file); + goto lose; + } + if (fstat(fd, &isb) != 0) { + maybe_warn("can't stat %s", file); + goto lose; + } + if (S_ISREG(isb.st_mode)) + in_size = isb.st_size; + else + in_size = 0; + infile_set(file, in_size); + + strlcpy(outfile, file, outsize); + if (check_suffix(outfile, 1) == NULL && !(cflag || lflag)) { + maybe_warnx("%s: unknown suffix -- ignored", file); + goto lose; + } + + rbytes = read(fd, fourbytes, sizeof fourbytes); + if (rbytes != sizeof fourbytes) { + /* we don't want to fail here. */ + if (fflag) + goto lose; + if (rbytes == -1) + maybe_warn("can't read %s", file); + else + goto unexpected_EOF; + goto lose; + } + infile_newdata(rbytes); + + method = file_gettype(fourbytes); + if (fflag == 0 && method == FT_UNKNOWN) { + maybe_warnx("%s: not in gzip format", file); + goto lose; + } + + + if (method == FT_GZIP && Nflag) { + unsigned char ts[4]; /* timestamp */ + + rv = pread(fd, ts, sizeof ts, GZIP_TIMESTAMP); + if (rv >= 0 && rv < (ssize_t)(sizeof ts)) + goto unexpected_EOF; + if (rv == -1) { + if (!fflag) + maybe_warn("can't read %s", file); + goto lose; + } + infile_newdata(rv); + timestamp = le32dec(&ts[0]); + + if (fourbytes[3] & ORIG_NAME) { + rbytes = pread(fd, name, sizeof(name) - 1, GZIP_ORIGNAME); + if (rbytes < 0) { + maybe_warn("can't read %s", file); + goto lose; + } + if (name[0] != '\0') { + char *dp, *nf; + + /* Make sure that name is NUL-terminated */ + name[rbytes] = '\0'; + + /* strip saved directory name */ + nf = strrchr(name, '/'); + if (nf == NULL) + nf = name; + else + nf++; + + /* preserve original directory name */ + dp = strrchr(file, '/'); + if (dp == NULL) + dp = file; + else + dp++; + snprintf(outfile, outsize, "%.*s%.*s", + (int) (dp - file), + file, (int) rbytes, nf); + } + } + } + lseek(fd, 0, SEEK_SET); + + if (cflag == 0 || lflag) { + if (isb.st_nlink > 1 && lflag == 0 && fflag == 0) { + maybe_warnx("%s has %ju other links -- skipping", + file, (uintmax_t)isb.st_nlink - 1); + goto lose; + } + if (nflag == 0 && timestamp) + isb.st_mtime = timestamp; + if (check_outfile(outfile) == 0) + goto lose; + } + + if (cflag) + zfd = STDOUT_FILENO; + else if (lflag) + zfd = -1; + else { + zfd = open(outfile, O_WRONLY|O_CREAT|O_EXCL, 0600); + if (zfd == STDOUT_FILENO) { + /* We won't close STDOUT_FILENO later... */ + zfd = dup(zfd); + close(STDOUT_FILENO); + } + if (zfd == -1) { + maybe_warn("can't open %s", outfile); + goto lose; + } + remove_file = outfile; + } + + switch (method) { +#ifndef NO_BZIP2_SUPPORT + case FT_BZIP2: + /* XXX */ + if (lflag) { + maybe_warnx("no -l with bzip2 files"); + goto lose; + } + + size = unbzip2(fd, zfd, NULL, 0, NULL); + break; +#endif + +#ifndef NO_COMPRESS_SUPPORT + case FT_Z: { + FILE *in, *out; + + /* XXX */ + if (lflag) { + maybe_warnx("no -l with Lempel-Ziv files"); + goto lose; + } + + if ((in = zdopen(fd)) == NULL) { + maybe_warn("zdopen for read: %s", file); + goto lose; + } + + out = fdopen(dup(zfd), "w"); + if (out == NULL) { + maybe_warn("fdopen for write: %s", outfile); + fclose(in); + goto lose; + } + + size = zuncompress(in, out, NULL, 0, NULL); + /* need to fclose() if ferror() is true... */ + error = ferror(in); + if (error | fclose(in)) { + if (error) + maybe_warn("failed infile"); + else + maybe_warn("failed infile fclose"); + if (cflag == 0) + unlink(outfile); + (void)fclose(out); + goto lose; + } + if (fclose(out) != 0) { + maybe_warn("failed outfile fclose"); + if (cflag == 0) + unlink(outfile); + goto lose; + } + break; + } +#endif + +#ifndef NO_PACK_SUPPORT + case FT_PACK: + if (lflag) { + maybe_warnx("no -l with packed files"); + goto lose; + } + + size = unpack(fd, zfd, NULL, 0, NULL); + break; +#endif + +#ifndef NO_XZ_SUPPORT + case FT_XZ: + if (lflag) { + size = unxz_len(fd); + if (!tflag) { + print_list_out(in_size, size, file); + close(fd); + return -1; + } + } else + size = unxz(fd, zfd, NULL, 0, NULL); + break; +#endif + +#ifndef NO_LZ_SUPPORT + case FT_LZ: + if (lflag) { + maybe_warnx("no -l with lzip files"); + goto lose; + } + size = unlz(fd, zfd, NULL, 0, NULL); + break; +#endif + +#ifndef NO_ZSTD_SUPPORT + case FT_ZSTD: + if (lflag) { + maybe_warnx("no -l with zstd files"); + goto lose; + } + size = unzstd(fd, zfd, NULL, 0, NULL); + break; +#endif + case FT_UNKNOWN: + if (lflag) { + maybe_warnx("no -l for unknown filetypes"); + goto lose; + } + size = cat_fd(NULL, 0, NULL, fd); + break; + default: + if (lflag) { + print_list(fd, in_size, outfile, isb.st_mtime); + if (!tflag) { + close(fd); + return -1; /* XXX */ + } + } + + size = gz_uncompress(fd, zfd, NULL, 0, NULL, file); + break; + } + + if (close(fd) != 0) + maybe_warn("couldn't close input"); + if (zfd != STDOUT_FILENO && close(zfd) != 0) + maybe_warn("couldn't close output"); + + if (size == -1) { + if (cflag == 0) + unlink(outfile); + maybe_warnx("%s: uncompress failed", file); + return -1; + } + + /* if testing, or we uncompressed to stdout, this is all we need */ + if (tflag) + return size; + /* if we are uncompressing to stdin, don't remove the file. */ + if (cflag) + return size; + + /* + * if we create a file... + */ + /* + * if we can't stat the file don't remove the file. + */ + + ofd = open(outfile, O_RDWR, 0); + if (ofd == -1) { + maybe_warn("couldn't open (leaving original): %s", + outfile); + return -1; + } + if (fstat(ofd, &osb) != 0) { + maybe_warn("couldn't stat (leaving original): %s", + outfile); + close(ofd); + return -1; + } + if (osb.st_size != size) { + maybe_warnx("stat gave different size: %ju != %ju (leaving original)", + (uintmax_t)size, (uintmax_t)osb.st_size); + close(ofd); + unlink(outfile); + return -1; + } + copymodes(ofd, &isb, outfile); + remove_file = NULL; + close(ofd); + unlink_input(file, &isb); + return size; + + unexpected_EOF: + maybe_warnx("%s: unexpected end of file", file); + lose: + if (fd != -1) + close(fd); + if (zfd != -1 && zfd != STDOUT_FILENO) + close(zfd); + return -1; +} + +static void +check_siginfo(void) +{ + if (print_info == 0) + return; + if (infile) { + if (infile_total) { + int pcent = (int)((100.0 * infile_current) / infile_total); + + fprintf(stderr, "%s: done %llu/%llu bytes %d%%\n", + infile, (unsigned long long)infile_current, + (unsigned long long)infile_total, pcent); + } else + fprintf(stderr, "%s: done %llu bytes\n", + infile, (unsigned long long)infile_current); + } + print_info = 0; +} + +static off_t +cat_fd(unsigned char * prepend, size_t count, off_t *gsizep, int fd) +{ + char buf[BUFLEN]; + off_t in_tot; + ssize_t w; + + in_tot = count; + w = write_retry(STDOUT_FILENO, prepend, count); + if (w == -1 || (size_t)w != count) { + maybe_warn("write to stdout"); + return -1; + } + for (;;) { + ssize_t rv; + + rv = read(fd, buf, sizeof buf); + if (rv == 0) + break; + if (rv < 0) { + maybe_warn("read from fd %d", fd); + break; + } + infile_newdata(rv); + + if (write_retry(STDOUT_FILENO, buf, rv) != rv) { + maybe_warn("write to stdout"); + break; + } + in_tot += rv; + } + + if (gsizep) + *gsizep = in_tot; + return (in_tot); +} + +static void +handle_stdin(void) +{ + struct stat isb; + unsigned char fourbytes[4]; + size_t in_size; + off_t usize, gsize; + enum filetype method; + ssize_t bytes_read; +#ifndef NO_COMPRESS_SUPPORT + FILE *in; +#endif + + if (fflag == 0 && lflag == 0 && isatty(STDIN_FILENO)) { + maybe_warnx("standard input is a terminal -- ignoring"); + goto out; + } + + if (fstat(STDIN_FILENO, &isb) < 0) { + maybe_warn("fstat"); + goto out; + } + if (S_ISREG(isb.st_mode)) + in_size = isb.st_size; + else + in_size = 0; + infile_set("(stdin)", in_size); + + if (lflag) { + print_list(STDIN_FILENO, in_size, infile, isb.st_mtime); + goto out; + } + + bytes_read = read_retry(STDIN_FILENO, fourbytes, sizeof fourbytes); + if (bytes_read == -1) { + maybe_warn("can't read stdin"); + goto out; + } else if (bytes_read != sizeof(fourbytes)) { + maybe_warnx("(stdin): unexpected end of file"); + goto out; + } + + method = file_gettype(fourbytes); + switch (method) { + default: + if (fflag == 0) { + maybe_warnx("unknown compression format"); + goto out; + } + usize = cat_fd(fourbytes, sizeof fourbytes, &gsize, STDIN_FILENO); + break; + case FT_GZIP: + usize = gz_uncompress(STDIN_FILENO, STDOUT_FILENO, + (char *)fourbytes, sizeof fourbytes, &gsize, "(stdin)"); + break; +#ifndef NO_BZIP2_SUPPORT + case FT_BZIP2: + usize = unbzip2(STDIN_FILENO, STDOUT_FILENO, + (char *)fourbytes, sizeof fourbytes, &gsize); + break; +#endif +#ifndef NO_COMPRESS_SUPPORT + case FT_Z: + if ((in = zdopen(STDIN_FILENO)) == NULL) { + maybe_warnx("zopen of stdin"); + goto out; + } + + usize = zuncompress(in, stdout, (char *)fourbytes, + sizeof fourbytes, &gsize); + fclose(in); + break; +#endif +#ifndef NO_PACK_SUPPORT + case FT_PACK: + usize = unpack(STDIN_FILENO, STDOUT_FILENO, + (char *)fourbytes, sizeof fourbytes, &gsize); + break; +#endif +#ifndef NO_XZ_SUPPORT + case FT_XZ: + usize = unxz(STDIN_FILENO, STDOUT_FILENO, + (char *)fourbytes, sizeof fourbytes, &gsize); + break; +#endif +#ifndef NO_LZ_SUPPORT + case FT_LZ: + usize = unlz(STDIN_FILENO, STDOUT_FILENO, + (char *)fourbytes, sizeof fourbytes, &gsize); + break; +#endif +#ifndef NO_ZSTD_SUPPORT + case FT_ZSTD: + usize = unzstd(STDIN_FILENO, STDOUT_FILENO, + (char *)fourbytes, sizeof fourbytes, &gsize); + break; +#endif + } + + if (vflag && !tflag && usize != -1 && gsize != -1) + print_verbage(NULL, NULL, usize, gsize); + if (vflag && tflag) + print_test("(stdin)", usize != -1); + +out: + infile_clear(); +} + +static void +handle_stdout(void) +{ + off_t gsize; + off_t usize; + struct stat sb; + time_t systime; + uint32_t mtime; + int ret; + + infile_set("(stdout)", 0); + + if (fflag == 0 && isatty(STDOUT_FILENO)) { + maybe_warnx("standard output is a terminal -- ignoring"); + return; + } + + /* If stdin is a file use its mtime, otherwise use current time */ + ret = fstat(STDIN_FILENO, &sb); + if (ret < 0) { + maybe_warn("Can't stat stdin"); + return; + } + + if (S_ISREG(sb.st_mode)) { + infile_set("(stdout)", sb.st_size); + mtime = (uint32_t)sb.st_mtime; + } else { + systime = time(NULL); + if (systime == -1) { + maybe_warn("time"); + return; + } + mtime = (uint32_t)systime; + } + + usize = + gz_compress(STDIN_FILENO, STDOUT_FILENO, &gsize, "", mtime); + if (vflag && !tflag && usize != -1 && gsize != -1) + print_verbage(NULL, NULL, usize, gsize); +} + +/* do what is asked for, for the path name */ +static void +handle_pathname(char *path) +{ + char *opath = path, *s = NULL; + ssize_t len; + int slen; + struct stat sb; + + /* check for stdout/stdin */ + if (path[0] == '-' && path[1] == '\0') { + if (dflag) + handle_stdin(); + else + handle_stdout(); + return; + } + +retry: + if (stat(path, &sb) != 0 || (fflag == 0 && cflag == 0 && + lstat(path, &sb) != 0)) { + /* lets try .gz if we're decompressing */ + if (dflag && s == NULL && errno == ENOENT) { + len = strlen(path); + slen = suffixes[0].ziplen; + s = malloc(len + slen + 1); + if (s == NULL) + maybe_err("malloc"); + memcpy(s, path, len); + memcpy(s + len, suffixes[0].zipped, slen + 1); + path = s; + goto retry; + } + maybe_warn("can't stat: %s", opath); + goto out; + } + + if (S_ISDIR(sb.st_mode)) { + if (rflag) + handle_dir(path); + else + maybe_warnx("%s is a directory", path); + goto out; + } + + if (S_ISREG(sb.st_mode)) + handle_file(path, &sb); + else + maybe_warnx("%s is not a regular file", path); + +out: + if (s) + free(s); +} + +/* compress/decompress a file */ +static void +handle_file(char *file, struct stat *sbp) +{ + off_t usize, gsize; + char outfile[PATH_MAX]; + + infile_set(file, sbp->st_size); + if (dflag) { + usize = file_uncompress(file, outfile, sizeof(outfile)); + if (vflag && tflag) + print_test(file, usize != -1); + if (usize == -1) + return; + gsize = sbp->st_size; + } else { + gsize = file_compress(file, outfile, sizeof(outfile)); + if (gsize == -1) + return; + usize = sbp->st_size; + } + infile_clear(); + + if (vflag && !tflag) + print_verbage(file, (cflag) ? NULL : outfile, usize, gsize); +} + +/* this is used with -r to recursively descend directories */ +static void +handle_dir(char *dir) +{ + char *path_argv[2]; + FTS *fts; + FTSENT *entry; + + path_argv[0] = dir; + path_argv[1] = 0; + fts = fts_open(path_argv, FTS_PHYSICAL | FTS_NOCHDIR, NULL); + if (fts == NULL) { + warn("couldn't fts_open %s", dir); + return; + } + + while (errno = 0, (entry = fts_read(fts))) { + switch(entry->fts_info) { + case FTS_D: + case FTS_DP: + continue; + + case FTS_DNR: + case FTS_ERR: + case FTS_NS: + maybe_warn("%s", entry->fts_path); + continue; + case FTS_F: + handle_file(entry->fts_path, entry->fts_statp); + } + } + if (errno != 0) + warn("error with fts_read %s", dir); + (void)fts_close(fts); +} + +/* print a ratio - size reduction as a fraction of uncompressed size */ +static void +print_ratio(off_t in, off_t out, FILE *where) +{ + int percent10; /* 10 * percent */ + off_t diff; + char buff[8]; + int len; + + diff = in - out/2; + if (in == 0 && out == 0) + percent10 = 0; + else if (diff < 0) + /* + * Output is more than double size of input! print -99.9% + * Quite possibly we've failed to get the original size. + */ + percent10 = -999; + else { + /* + * We only need 12 bits of result from the final division, + * so reduce the values until a 32bit division will suffice. + */ + while (in > 0x100000) { + diff >>= 1; + in >>= 1; + } + if (in != 0) + percent10 = ((u_int)diff * 2000) / (u_int)in - 1000; + else + percent10 = 0; + } + + len = snprintf(buff, sizeof buff, "%2.2d.", percent10); + /* Move the '.' to before the last digit */ + buff[len - 1] = buff[len - 2]; + buff[len - 2] = '.'; + fprintf(where, "%5s%%", buff); +} + +/* print compression statistics, and the new name (if there is one!) */ +static void +print_verbage(const char *file, const char *nfile, off_t usize, off_t gsize) +{ + if (file) + fprintf(stderr, "%s:%s ", file, + strlen(file) < 7 ? "\t\t" : "\t"); + print_ratio(usize, gsize, stderr); + if (nfile) + fprintf(stderr, " -- replaced with %s", nfile); + fprintf(stderr, "\n"); + fflush(stderr); +} + +/* print test results */ +static void +print_test(const char *file, int ok) +{ + + if (exit_value == 0 && ok == 0) + exit_value = 1; + fprintf(stderr, "%s:%s %s\n", file, + strlen(file) < 7 ? "\t\t" : "\t", ok ? "OK" : "NOT OK"); + fflush(stderr); +} + +/* print a file's info ala --list */ +/* eg: + compressed uncompressed ratio uncompressed_name + 354841 1679360 78.8% /usr/pkgsrc/distfiles/libglade-2.0.1.tar +*/ +static void +print_list(int fd, off_t out, const char *outfile, time_t ts) +{ + static int first = 1; + static off_t in_tot, out_tot; + uint32_t crc = 0; + off_t in = 0, rv; + + if (first) { + if (vflag) + printf("method crc date time "); + if (qflag == 0) + printf(" compressed uncompressed " + "ratio uncompressed_name\n"); + } + first = 0; + + /* print totals? */ + if (fd == -1) { + in = in_tot; + out = out_tot; + } else + { + /* read the last 4 bytes - this is the uncompressed size */ + rv = lseek(fd, (off_t)(-8), SEEK_END); + if (rv != -1) { + unsigned char buf[8]; + uint32_t usize; + + rv = read(fd, (char *)buf, sizeof(buf)); + if (rv == -1) + maybe_warn("read of uncompressed size"); + else if (rv != sizeof(buf)) + maybe_warnx("read of uncompressed size"); + + else { + usize = le32dec(&buf[4]); + in = (off_t)usize; + crc = le32dec(&buf[0]); + } + } + } + + if (vflag && fd == -1) + printf(" "); + else if (vflag) { + char *date = ctime(&ts); + + /* skip the day, 1/100th second, and year */ + date += 4; + date[12] = 0; + printf("%5s %08x %11s ", "defla"/*XXX*/, crc, date); + } + in_tot += in; + out_tot += out; + print_list_out(out, in, outfile); +} + +static void +print_list_out(off_t out, off_t in, const char *outfile) +{ + printf("%12llu %12llu ", (unsigned long long)out, (unsigned long long)in); + print_ratio(in, out, stdout); + printf(" %s\n", outfile); +} + +/* display the usage of NetBSD gzip */ +static void +usage(void) +{ + + fprintf(stderr, "%s\n", gzip_version); + fprintf(stderr, + "usage: %s [-123456789acdfhklLNnqrtVv] [-S .suffix] [ [ ...]]\n" + " -1 --fast fastest (worst) compression\n" + " -2 .. -8 set compression level\n" + " -9 --best best (slowest) compression\n" + " -c --stdout write to stdout, keep original files\n" + " --to-stdout\n" + " -d --decompress uncompress files\n" + " --uncompress\n" + " -f --force force overwriting & compress links\n" + " -h --help display this help\n" + " -k --keep don't delete input files during operation\n" + " -l --list list compressed file contents\n" + " -N --name save or restore original file name and time stamp\n" + " -n --no-name don't save original file name or time stamp\n" + " -q --quiet output no warnings\n" + " -r --recursive recursively compress files in directories\n" + " -S .suf use suffix .suf instead of .gz\n" + " --suffix .suf\n" + " -t --test test compressed file\n" + " -V --version display program version\n" + " -v --verbose print extra statistics\n", + "gzip"); + exit(0); +} + +/* display the license information of FreeBSD gzip */ +static void +display_license(void) +{ + + fprintf(stderr, "%s (based on NetBSD gzip 20150113)\n", gzip_version); + fprintf(stderr, "%s\n", gzip_copyright); + exit(0); +} + +/* display the version of NetBSD gzip */ +static void +display_version(void) +{ + + fprintf(stderr, "%s\n", gzip_version); + exit(0); +} + +#ifndef NO_BZIP2_SUPPORT +#include "unbzip2.c" +#endif +#ifndef NO_COMPRESS_SUPPORT +#include "zuncompress.c" +#endif +#ifndef NO_PACK_SUPPORT +#include "unpack.c" +#endif +#ifndef NO_XZ_SUPPORT +#include "unxz.c" +#endif +#ifndef NO_LZ_SUPPORT +#include "unlz.c" +#endif +#ifndef NO_ZSTD_SUPPORT +#include "unzstd.c" +#endif + +static ssize_t +read_retry(int fd, void *buf, size_t sz) +{ + char *cp = buf; + size_t left = MIN(sz, (size_t) SSIZE_MAX); + + while (left > 0) { + ssize_t ret; + + ret = read(fd, cp, left); + if (ret == -1) { + return ret; + } else if (ret == 0) { + break; /* EOF */ + } + cp += ret; + left -= ret; + } + + return sz - left; +} + +static ssize_t +write_retry(int fd, const void *buf, size_t sz) +{ + const char *cp = buf; + size_t left = MIN(sz, (size_t) SSIZE_MAX); + + while (left > 0) { + ssize_t ret; + + ret = write(fd, cp, left); + if (ret == -1) { + return ret; + } else if (ret == 0) { + abort(); /* Can't happen */ + } + cp += ret; + left -= ret; + } + + return sz - left; +} diff --git a/src/gzip/unlz.c b/src/gzip/unlz.c new file mode 100644 index 0000000..1dc558a --- /dev/null +++ b/src/gzip/unlz.c @@ -0,0 +1,642 @@ +/* $NetBSD: unlz.c,v 1.6 2018/11/11 01:42:36 christos Exp $ */ + +/*- + * Copyright (c) 2018 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Christos Zoulas. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. 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 FOUNDATION 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. + */ + +/* Lzd - Educational decompressor for the lzip format + Copyright (C) 2013-2018 Antonio Diaz Diaz. + + This program is free software. Redistribution and use in source and + binary forms, with or without modification, are permitted provided + that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define LZ_STATES 12 + +#define LITERAL_CONTEXT_BITS 3 +#define POS_STATE_BITS 2 +#define POS_STATES (1 << POS_STATE_BITS) +#define POS_STATE_MASK (POS_STATES - 1) + +#define STATES 4 +#define DIS_SLOT_BITS 6 + +#define DIS_MODEL_START 4 +#define DIS_MODEL_END 14 + +#define MODELED_DISTANCES (1 << (DIS_MODEL_END / 2)) +#define DIS_ALIGN_BITS 4 +#define DIS_ALIGN_SIZE (1 << DIS_ALIGN_BITS) + +#define LOW_BITS 3 +#define MID_BITS 3 +#define HIGH_BITS 8 + +#define LOW_SYMBOLS (1 << LOW_BITS) +#define MID_SYMBOLS (1 << MID_BITS) +#define HIGH_SYMBOLS (1 << HIGH_BITS) + +#define MAX_SYMBOLS (LOW_SYMBOLS + MID_SYMBOLS + HIGH_SYMBOLS) + +#define MIN_MATCH_LEN 2 + +#define BIT_MODEL_MOVE_BITS 5 +#define BIT_MODEL_TOTAL_BITS 11 +#define BIT_MODEL_TOTAL (1 << BIT_MODEL_TOTAL_BITS) +#define BIT_MODEL_INIT (BIT_MODEL_TOTAL / 2) + +static const int lz_st_next[] = { + 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 4, 5, +}; + +static bool +lz_st_is_char(int st) { + return st < 7; +} + +static int +lz_st_get_char(int st) { + return lz_st_next[st]; +} + +static int +lz_st_get_match(int st) { + return st < 7 ? 7 : 10; +} + +static int +lz_st_get_rep(int st) { + return st < 7 ? 8 : 11; +} + +static int +lz_st_get_short_rep(int st) { + return st < 7 ? 9 : 11; +} + +struct lz_len_model { + int choice1; + int choice2; + int bm_low[POS_STATES][LOW_SYMBOLS]; + int bm_mid[POS_STATES][MID_SYMBOLS]; + int bm_high[HIGH_SYMBOLS]; +}; + +static uint32_t lz_crc[256]; + +static void +lz_crc_init(void) +{ + for (unsigned i = 0; i < nitems(lz_crc); i++) { + unsigned c = i; + for (unsigned j = 0; j < 8; j++) { + if (c & 1) + c = 0xEDB88320U ^ (c >> 1); + else + c >>= 1; + } + lz_crc[i] = c; + } +} + +static void +lz_crc_update(uint32_t *crc, const uint8_t *buf, size_t len) +{ + for (size_t i = 0; i < len; i++) + *crc = lz_crc[(*crc ^ buf[i]) & 0xFF] ^ (*crc >> 8); +} + +struct lz_range_decoder { + FILE *fp; + uint32_t code; + uint32_t range; +}; + +static int +lz_rd_create(struct lz_range_decoder *rd, FILE *fp) +{ + rd->fp = fp; + rd->code = 0; + rd->range = ~0; + for (int i = 0; i < 5; i++) + rd->code = (rd->code << 8) | (uint8_t)getc(rd->fp); + return ferror(rd->fp) ? -1 : 0; +} + +static unsigned +lz_rd_decode(struct lz_range_decoder *rd, int num_bits) +{ + unsigned symbol = 0; + + for (int i = num_bits; i > 0; i--) { + rd->range >>= 1; + symbol <<= 1; + if (rd->code >= rd->range) { + rd->code -= rd->range; + symbol |= 1; + } + if (rd->range <= 0x00FFFFFFU) { + rd->range <<= 8; + rd->code = (rd->code << 8) | (uint8_t)getc(rd->fp); + } + } + + return symbol; +} + +static unsigned +lz_rd_decode_bit(struct lz_range_decoder *rd, int *bm) +{ + unsigned symbol; + const uint32_t bound = (rd->range >> BIT_MODEL_TOTAL_BITS) * *bm; + + if(rd->code < bound) { + rd->range = bound; + *bm += (BIT_MODEL_TOTAL - *bm) >> BIT_MODEL_MOVE_BITS; + symbol = 0; + } + else { + rd->range -= bound; + rd->code -= bound; + *bm -= *bm >> BIT_MODEL_MOVE_BITS; + symbol = 1; + } + + if (rd->range <= 0x00FFFFFFU) { + rd->range <<= 8; + rd->code = (rd->code << 8) | (uint8_t)getc(rd->fp); + } + return symbol; +} + +static unsigned +lz_rd_decode_tree(struct lz_range_decoder *rd, int *bm, int num_bits) +{ + unsigned symbol = 1; + + for (int i = 0; i < num_bits; i++) + symbol = (symbol << 1) | lz_rd_decode_bit(rd, &bm[symbol]); + + return symbol - (1 << num_bits); +} + +static unsigned +lz_rd_decode_tree_reversed(struct lz_range_decoder *rd, int *bm, int num_bits) +{ + unsigned symbol = lz_rd_decode_tree(rd, bm, num_bits); + unsigned reversed_symbol = 0; + + for (int i = 0; i < num_bits; i++) { + reversed_symbol = (reversed_symbol << 1) | (symbol & 1); + symbol >>= 1; + } + + return reversed_symbol; +} + +static unsigned +lz_rd_decode_matched(struct lz_range_decoder *rd, int *bm, int match_byte) +{ + unsigned symbol = 1; + + for (int i = 7; i >= 0; i--) { + const unsigned match_bit = (match_byte >> i) & 1; + const unsigned bit = lz_rd_decode_bit(rd, + &bm[symbol + (match_bit << 8) + 0x100]); + symbol = (symbol << 1) | bit; + if (match_bit != bit) { + while (symbol < 0x100) { + symbol = (symbol << 1) | + lz_rd_decode_bit(rd, &bm[symbol]); + } + break; + } + } + return symbol & 0xFF; +} + +static unsigned +lz_rd_decode_len(struct lz_range_decoder *rd, struct lz_len_model *lm, + int pos_state) +{ + if (lz_rd_decode_bit(rd, &lm->choice1) == 0) + return lz_rd_decode_tree(rd, lm->bm_low[pos_state], LOW_BITS); + + if (lz_rd_decode_bit(rd, &lm->choice2) == 0) { + return LOW_SYMBOLS + + lz_rd_decode_tree(rd, lm->bm_mid[pos_state], MID_BITS); + } + + return LOW_SYMBOLS + MID_SYMBOLS + + lz_rd_decode_tree(rd, lm->bm_high, HIGH_BITS); +} + +struct lz_decoder { + FILE *fin, *fout; + off_t pos, ppos, spos, dict_size; + bool wrapped; + uint32_t crc; + uint8_t *obuf; + struct lz_range_decoder rdec; +}; + +static int +lz_flush(struct lz_decoder *lz) +{ + off_t offs = lz->pos - lz->spos; + if (offs <= 0) + return -1; + + size_t size = (size_t)offs; + lz_crc_update(&lz->crc, lz->obuf + lz->spos, size); + if (fwrite(lz->obuf + lz->spos, 1, size, lz->fout) != size) + return -1; + + lz->wrapped = lz->pos >= lz->dict_size; + if (lz->wrapped) { + lz->ppos += lz->pos; + lz->pos = 0; + } + lz->spos = lz->pos; + return 0; +} + +static void +lz_destroy(struct lz_decoder *lz) +{ + if (lz->fin) + fclose(lz->fin); + if (lz->fout) + fclose(lz->fout); + free(lz->obuf); +} + +static int +lz_create(struct lz_decoder *lz, int fin, int fdout, int dict_size) +{ + memset(lz, 0, sizeof(*lz)); + + lz->fin = fdopen(dup(fin), "r"); + if (lz->fin == NULL) + goto out; + + lz->fout = fdopen(dup(fdout), "w"); + if (lz->fout == NULL) + goto out; + + lz->pos = lz->ppos = lz->spos = 0; + lz->crc = ~0; + lz->dict_size = dict_size; + lz->wrapped = false; + + lz->obuf = malloc(dict_size); + if (lz->obuf == NULL) + goto out; + + if (lz_rd_create(&lz->rdec, lz->fin) == -1) + goto out; + return 0; +out: + lz_destroy(lz); + return -1; +} + +static uint8_t +lz_peek(const struct lz_decoder *lz, unsigned ahead) +{ + off_t diff = lz->pos - ahead - 1; + + if (diff >= 0) + return lz->obuf[diff]; + + if (lz->wrapped) + return lz->obuf[lz->dict_size + diff]; + + return 0; +} + +static void +lz_put(struct lz_decoder *lz, uint8_t b) +{ + lz->obuf[lz->pos++] = b; + if (lz->dict_size == lz->pos) + lz_flush(lz); +} + +static off_t +lz_get_data_position(const struct lz_decoder *lz) +{ + return lz->ppos + lz->pos; +} + +static unsigned +lz_get_crc(const struct lz_decoder *lz) +{ + return lz->crc ^ 0xffffffffU; +} + +static void +lz_bm_init(int *a, size_t l) +{ + for (size_t i = 0; i < l; i++) + a[i] = BIT_MODEL_INIT; +} + +#define LZ_BM_INIT(a) lz_bm_init(a, nitems(a)) +#define LZ_BM_INIT2(a) do { \ + size_t l = nitems(a[0]); \ + for (size_t i = 0; i < nitems(a); i++) \ + lz_bm_init(a[i], l); \ +} while (/*CONSTCOND*/0) + +#define LZ_MODEL_INIT(a) do { \ + a.choice1 = BIT_MODEL_INIT; \ + a.choice2 = BIT_MODEL_INIT; \ + LZ_BM_INIT2(a.bm_low); \ + LZ_BM_INIT2(a.bm_mid); \ + LZ_BM_INIT(a.bm_high); \ +} while (/*CONSTCOND*/0) + +static bool +lz_decode_member(struct lz_decoder *lz) +{ + int bm_literal[1 << LITERAL_CONTEXT_BITS][0x300]; + int bm_match[LZ_STATES][POS_STATES]; + int bm_rep[4][LZ_STATES]; + int bm_len[LZ_STATES][POS_STATES]; + int bm_dis_slot[LZ_STATES][1 << DIS_SLOT_BITS]; + int bm_dis[MODELED_DISTANCES - DIS_MODEL_END + 1]; + int bm_align[DIS_ALIGN_SIZE]; + + LZ_BM_INIT2(bm_literal); + LZ_BM_INIT2(bm_match); + LZ_BM_INIT2(bm_rep); + LZ_BM_INIT2(bm_len); + LZ_BM_INIT2(bm_dis_slot); + LZ_BM_INIT(bm_dis); + LZ_BM_INIT(bm_align); + + struct lz_len_model match_len_model; + struct lz_len_model rep_len_model; + + LZ_MODEL_INIT(match_len_model); + LZ_MODEL_INIT(rep_len_model); + + struct lz_range_decoder *rd = &lz->rdec; + unsigned rep[4] = { 0 }; + + + int state = 0; + + while (!feof(lz->fin) && !ferror(lz->fin)) { + const int pos_state = lz_get_data_position(lz) & POS_STATE_MASK; + // bit 1 + if (lz_rd_decode_bit(rd, &bm_match[state][pos_state]) == 0) { + const uint8_t prev_byte = lz_peek(lz, 0); + const int literal_state = + prev_byte >> (8 - LITERAL_CONTEXT_BITS); + int *bm = bm_literal[literal_state]; + if (lz_st_is_char(state)) + lz_put(lz, lz_rd_decode_tree(rd, bm, 8)); + else { + int peek = lz_peek(lz, rep[0]); + lz_put(lz, lz_rd_decode_matched(rd, bm, peek)); + } + state = lz_st_get_char(state); + continue; + } + int len; + // bit 2 + if (lz_rd_decode_bit(rd, &bm_rep[0][state]) != 0) { + // bit 3 + if (lz_rd_decode_bit(rd, &bm_rep[1][state]) == 0) { + // bit 4 + if (lz_rd_decode_bit(rd, + &bm_len[state][pos_state]) == 0) + { + state = lz_st_get_short_rep(state); + lz_put(lz, lz_peek(lz, rep[0])); + continue; + } + } else { + unsigned distance; + // bit 4 + if (lz_rd_decode_bit(rd, &bm_rep[2][state]) + == 0) + distance = rep[1]; + else { + // bit 5 + if (lz_rd_decode_bit(rd, + &bm_rep[3][state]) == 0) + distance = rep[2]; + else { + distance = rep[3]; + rep[3] = rep[2]; + } + rep[2] = rep[1]; + } + rep[1] = rep[0]; + rep[0] = distance; + } + state = lz_st_get_rep(state); + len = MIN_MATCH_LEN + + lz_rd_decode_len(rd, &rep_len_model, pos_state); + } else { + rep[3] = rep[2]; rep[2] = rep[1]; rep[1] = rep[0]; + len = MIN_MATCH_LEN + + lz_rd_decode_len(rd, &match_len_model, pos_state); + const int len_state = + MIN(len - MIN_MATCH_LEN, STATES - 1); + rep[0] = lz_rd_decode_tree(rd, bm_dis_slot[len_state], + DIS_SLOT_BITS); + if (rep[0] >= DIS_MODEL_START) { + const unsigned dis_slot = rep[0]; + const int direct_bits = (dis_slot >> 1) - 1; + rep[0] = (2 | (dis_slot & 1)) << direct_bits; + if (dis_slot < DIS_MODEL_END) + rep[0] += lz_rd_decode_tree_reversed(rd, + &bm_dis[rep[0] - dis_slot], + direct_bits); + else { + rep[0] += lz_rd_decode(rd, direct_bits + - DIS_ALIGN_BITS) << DIS_ALIGN_BITS; + rep[0] += lz_rd_decode_tree_reversed(rd, + bm_align, DIS_ALIGN_BITS); + if (rep[0] == 0xFFFFFFFFU) { + lz_flush(lz); + return len == MIN_MATCH_LEN; + } + } + } + state = lz_st_get_match(state); + if (rep[0] >= lz->dict_size || + (rep[0] >= lz->pos && !lz->wrapped)) { + lz_flush(lz); + return false; + } + } + for (int i = 0; i < len; i++) + lz_put(lz, lz_peek(lz, rep[0])); + } + lz_flush(lz); + return false; +} + +/* + * 0-3 CRC32 of the uncompressed data + * 4-11 size of the uncompressed data + * 12-19 member size including header and trailer + */ +#define TRAILER_SIZE 20 + + +static off_t +lz_decode(int fin, int fdout, unsigned dict_size, off_t *insize) +{ + struct lz_decoder lz; + off_t rv = -1; + + if (lz_create(&lz, fin, fdout, dict_size) == -1) + return -1; + + if (!lz_decode_member(&lz)) + goto out; + + uint8_t trailer[TRAILER_SIZE]; + + for(size_t i = 0; i < nitems(trailer); i++) + trailer[i] = (uint8_t)getc(lz.fin); + + unsigned crc = 0; + for (int i = 3; i >= 0; --i) { + crc <<= 8; + crc += trailer[i]; + } + + int64_t data_size = 0; + for (int i = 11; i >= 4; --i) { + data_size <<= 8; + data_size += trailer[i]; + } + + if (crc != lz_get_crc(&lz) || data_size != lz_get_data_position(&lz)) + goto out; + + rv = 0; + for (int i = 19; i >= 12; --i) { + rv <<= 8; + rv += trailer[i]; + } + if (insize) + *insize = rv; +#if 0 + /* Does not work with pipes */ + rv = ftello(lz.fout); +#else + rv = data_size; +#endif +out: + lz_destroy(&lz); + return rv; +} + + +/* + * 0-3 magic + * 4 version + * 5 coded dict_size + */ +#define HDR_SIZE 6 +#define MIN_DICTIONARY_SIZE (1 << 12) +#define MAX_DICTIONARY_SIZE (1 << 29) + +static const char hdrmagic[] = { 'L', 'Z', 'I', 'P', 1 }; + +static unsigned +lz_get_dict_size(unsigned char c) +{ + unsigned dict_size = 1 << (c & 0x1f); + dict_size -= (dict_size >> 2) * ( (c >> 5) & 0x7); + if (dict_size < MIN_DICTIONARY_SIZE || dict_size > MAX_DICTIONARY_SIZE) + return 0; + return dict_size; +} + +static off_t +unlz(int fin, int fout, char *pre, size_t prelen, off_t *bytes_in) +{ + if (lz_crc[0] == 0) + lz_crc_init(); + + char header[HDR_SIZE]; + + if (pre && prelen) + memcpy(header, pre, prelen); + + ssize_t nr = read(fin, header + prelen, sizeof(header) - prelen); + switch (nr) { + case -1: + return -1; + case 0: + return prelen ? -1 : 0; + default: + if ((size_t)nr != sizeof(header) - prelen) + return -1; + break; + } + + if (memcmp(header, hdrmagic, sizeof(hdrmagic)) != 0) + return -1; + + unsigned dict_size = lz_get_dict_size(header[5]); + if (dict_size == 0) + return -1; + + return lz_decode(fin, fout, dict_size, bytes_in); +} diff --git a/src/gzip/unpack.c b/src/gzip/unpack.c new file mode 100644 index 0000000..110500a --- /dev/null +++ b/src/gzip/unpack.c @@ -0,0 +1,334 @@ +/* $NetBSD: unpack.c,v 1.3 2017/08/04 07:27:08 mrg Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2009 Xin LI + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ + +/* This file is #included by gzip.c */ + +/* + * pack(1) file format: + * + * The first 7 bytes is the header: + * 00, 01 - Signature (US, RS), we already validated it earlier. + * 02..05 - Uncompressed size + * 06 - Level for the huffman tree (<=24) + * + * pack(1) will then store symbols (leaf) nodes count in each huffman + * tree levels, each level would consume 1 byte (See [1]). + * + * After the symbol count table, there is the symbol table, storing + * symbols represented by corresponding leaf node. EOB is not being + * explicitly transmitted (not necessary anyway) in the symbol table. + * + * Compressed data goes after the symbol table. + * + * NOTES + * + * [1] If we count EOB into the symbols, that would mean that we will + * have at most 256 symbols in the huffman tree. pack(1) rejects empty + * file and files that just repeats one character, which means that we + * will have at least 2 symbols. Therefore, pack(1) would reduce the + * last level symbol count by 2 which makes it a number in + * range [0..254], so all levels' symbol count would fit into 1 byte. + */ + +#define PACK_HEADER_LENGTH 7 +#define HTREE_MAXLEVEL 24 + +/* + * unpack descriptor + * + * Represent the huffman tree in a similar way that pack(1) would + * store in a packed file. We store all symbols in a linear table, + * and store pointers to each level's first symbol. In addition to + * that, maintain two counts for each level: inner nodes count and + * leaf nodes count. + */ +typedef struct { + int symbol_size; /* Size of the symbol table */ + int treelevels; /* Levels for the huffman tree */ + + int *symbolsin; /* Table of leaf symbols count in each + * level */ + int *inodesin; /* Table of internal nodes count in + * each level */ + + char *symbol; /* The symbol table */ + char *symbol_eob; /* Pointer to the EOB symbol */ + char **tree; /* Decoding huffman tree (pointers to + * first symbol of each tree level */ + + off_t uncompressed_size; /* Uncompressed size */ + FILE *fpIn; /* Input stream */ + FILE *fpOut; /* Output stream */ +} unpack_descriptor_t; + +/* + * Release resource allocated to an unpack descriptor. + * + * Caller is responsible to make sure that all of these pointers are + * initialized (in our case, they all point to valid memory block). + * We don't zero out pointers here because nobody else would ever + * reference the memory block without scrubbing them. + */ +static void +unpack_descriptor_fini(unpack_descriptor_t *unpackd) +{ + + free(unpackd->symbolsin); + free(unpackd->inodesin); + free(unpackd->symbol); + free(unpackd->tree); + + fclose(unpackd->fpIn); + fclose(unpackd->fpOut); +} + +/* + * Recursively fill the internal node count table + */ +static void +unpackd_fill_inodesin(const unpack_descriptor_t *unpackd, int level) +{ + + /* + * The internal nodes would be 1/2 of total internal nodes and + * leaf nodes in the next level. For the last level there + * would be no internal node by definition. + */ + if (level < unpackd->treelevels) { + unpackd_fill_inodesin(unpackd, level + 1); + unpackd->inodesin[level] = (unpackd->inodesin[level + 1] + + unpackd->symbolsin[level + 1]) / 2; + } else + unpackd->inodesin[level] = 0; +} + +/* + * Update counter for accepted bytes + */ +static void +accepted_bytes(off_t *bytes_in, off_t newbytes) +{ + + if (bytes_in != NULL) + (*bytes_in) += newbytes; +} + +/* + * Read file header and construct the tree. Also, prepare the buffered I/O + * for decode routine. + * + * Return value is uncompressed size. + */ +static void +unpack_parse_header(int in, int out, char *pre, size_t prelen, off_t *bytes_in, + unpack_descriptor_t *unpackd) +{ + unsigned char hdr[PACK_HEADER_LENGTH]; /* buffer for header */ + ssize_t bytesread; /* Bytes read from the file */ + int i, j, thisbyte; + + /* Prepend the header buffer if we already read some data */ + if (prelen != 0) + memcpy(hdr, pre, prelen); + + /* Read in and fill the rest bytes of header */ + bytesread = read(in, hdr + prelen, PACK_HEADER_LENGTH - prelen); + if (bytesread < 0) + maybe_err("Error reading pack header"); + infile_newdata(bytesread); + + accepted_bytes(bytes_in, PACK_HEADER_LENGTH); + + /* Obtain uncompressed length (bytes 2,3,4,5) */ + unpackd->uncompressed_size = 0; + for (i = 2; i <= 5; i++) { + unpackd->uncompressed_size <<= 8; + unpackd->uncompressed_size |= hdr[i]; + } + + /* Get the levels of the tree */ + unpackd->treelevels = hdr[6]; + if (unpackd->treelevels > HTREE_MAXLEVEL || unpackd->treelevels < 1) + maybe_errx("Huffman tree has insane levels"); + + /* Let libc take care for buffering from now on */ + if ((unpackd->fpIn = fdopen(in, "r")) == NULL) + maybe_err("Can not fdopen() input stream"); + if ((unpackd->fpOut = fdopen(out, "w")) == NULL) + maybe_err("Can not fdopen() output stream"); + + /* Allocate for the tables of bounds and the tree itself */ + unpackd->inodesin = + calloc(unpackd->treelevels, sizeof(*(unpackd->inodesin))); + unpackd->symbolsin = + calloc(unpackd->treelevels, sizeof(*(unpackd->symbolsin))); + unpackd->tree = + calloc(unpackd->treelevels, (sizeof(*(unpackd->tree)))); + if (unpackd->inodesin == NULL || unpackd->symbolsin == NULL || + unpackd->tree == NULL) + maybe_err("calloc"); + + /* We count from 0 so adjust to match array upper bound */ + unpackd->treelevels--; + + /* Read the levels symbol count table and calculate total */ + unpackd->symbol_size = 1; /* EOB */ + for (i = 0; i <= unpackd->treelevels; i++) { + if ((thisbyte = fgetc(unpackd->fpIn)) == EOF) + maybe_err("File appears to be truncated"); + unpackd->symbolsin[i] = (unsigned char)thisbyte; + unpackd->symbol_size += unpackd->symbolsin[i]; + } + accepted_bytes(bytes_in, unpackd->treelevels); + if (unpackd->symbol_size > 256) + maybe_errx("Bad symbol table"); + infile_newdata(unpackd->treelevels); + + /* Allocate for the symbol table, point symbol_eob at the beginning */ + unpackd->symbol_eob = unpackd->symbol = calloc(1, unpackd->symbol_size); + if (unpackd->symbol == NULL) + maybe_err("calloc"); + + /* + * Read in the symbol table, which contain [2, 256] symbols. + * In order to fit the count in one byte, pack(1) would offset + * it by reducing 2 from the actual number from the last level. + * + * We adjust the last level's symbol count by 1 here, because + * the EOB symbol is not being transmitted explicitly. Another + * adjustment would be done later afterward. + */ + unpackd->symbolsin[unpackd->treelevels]++; + for (i = 0; i <= unpackd->treelevels; i++) { + unpackd->tree[i] = unpackd->symbol_eob; + for (j = 0; j < unpackd->symbolsin[i]; j++) { + if ((thisbyte = fgetc(unpackd->fpIn)) == EOF) + maybe_errx("Symbol table truncated"); + *unpackd->symbol_eob++ = (char)thisbyte; + } + infile_newdata(unpackd->symbolsin[i]); + accepted_bytes(bytes_in, unpackd->symbolsin[i]); + } + + /* Now, take account for the EOB symbol as well */ + unpackd->symbolsin[unpackd->treelevels]++; + + /* + * The symbolsin table has been constructed now. + * Calculate the internal nodes count table based on it. + */ + unpackd_fill_inodesin(unpackd, 0); +} + +/* + * Decode huffman stream, based on the huffman tree. + */ +static void +unpack_decode(const unpack_descriptor_t *unpackd, off_t *bytes_in) +{ + int thislevel, thiscode, thisbyte, inlevelindex; + int i; + off_t bytes_out = 0; + const char *thissymbol; /* The symbol pointer decoded from stream */ + + /* + * Decode huffman. Fetch every bytes from the file, get it + * into 'thiscode' bit-by-bit, then output the symbol we got + * when one has been found. + * + * Assumption: sizeof(int) > ((max tree levels + 1) / 8). + * bad things could happen if not. + */ + thislevel = 0; + thiscode = thisbyte = 0; + + while ((thisbyte = fgetc(unpackd->fpIn)) != EOF) { + accepted_bytes(bytes_in, 1); + infile_newdata(1); + check_siginfo(); + + /* + * Split one bit from thisbyte, from highest to lowest, + * feed the bit into thiscode, until we got a symbol from + * the tree. + */ + for (i = 7; i >= 0; i--) { + thiscode = (thiscode << 1) | ((thisbyte >> i) & 1); + + /* Did we got a symbol? (referencing leaf node) */ + if (thiscode >= unpackd->inodesin[thislevel]) { + inlevelindex = + thiscode - unpackd->inodesin[thislevel]; + if (inlevelindex > unpackd->symbolsin[thislevel]) + maybe_errx("File corrupt"); + + thissymbol = + &(unpackd->tree[thislevel][inlevelindex]); + if ((thissymbol == unpackd->symbol_eob) && + (bytes_out == unpackd->uncompressed_size)) + goto finished; + + fputc((*thissymbol), unpackd->fpOut); + bytes_out++; + + /* Prepare for next input */ + thislevel = 0; thiscode = 0; + } else { + thislevel++; + if (thislevel > unpackd->treelevels) + maybe_errx("File corrupt"); + } + } + } + +finished: + if (bytes_out != unpackd->uncompressed_size) + maybe_errx("Premature EOF"); +} + +/* Handler for pack(1)'ed file */ +static off_t +unpack(int in, int out, char *pre, size_t prelen, off_t *bytes_in) +{ + unpack_descriptor_t unpackd; + + in = dup(in); + if (in == -1) + maybe_err("dup"); + out = dup(out); + if (out == -1) + maybe_err("dup"); + + unpack_parse_header(in, out, pre, prelen, bytes_in, &unpackd); + unpack_decode(&unpackd, bytes_in); + unpack_descriptor_fini(&unpackd); + + /* If we reached here, the unpack was successful */ + return (unpackd.uncompressed_size); +} diff --git a/src/gzip/unxz.c b/src/gzip/unxz.c new file mode 100644 index 0000000..bff71cd --- /dev/null +++ b/src/gzip/unxz.c @@ -0,0 +1,473 @@ +/* $NetBSD: unxz.c,v 1.8 2018/10/06 16:36:45 martin Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2011 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Christos Zoulas. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. 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 FOUNDATION 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 +#include +#include +#include +#include + +static off_t +unxz(int i, int o, char *pre, size_t prelen, off_t *bytes_in) +{ + lzma_stream strm = LZMA_STREAM_INIT; + static const int flags = LZMA_TELL_UNSUPPORTED_CHECK|LZMA_CONCATENATED; + lzma_ret ret; + lzma_action action = LZMA_RUN; + off_t bytes_out, bp; + uint8_t ibuf[BUFSIZ]; + uint8_t obuf[BUFSIZ]; + + if (bytes_in == NULL) + bytes_in = &bp; + + strm.next_in = ibuf; + memcpy(ibuf, pre, prelen); + strm.avail_in = read(i, ibuf + prelen, sizeof(ibuf) - prelen); + if (strm.avail_in == (size_t)-1) + maybe_err("read failed"); + infile_newdata(strm.avail_in); + strm.avail_in += prelen; + *bytes_in = strm.avail_in; + + if ((ret = lzma_stream_decoder(&strm, UINT64_MAX, flags)) != LZMA_OK) + maybe_errx("Can't initialize decoder (%d)", ret); + + strm.next_out = NULL; + strm.avail_out = 0; + if ((ret = lzma_code(&strm, LZMA_RUN)) != LZMA_OK) + maybe_errx("Can't read headers (%d)", ret); + + bytes_out = 0; + strm.next_out = obuf; + strm.avail_out = sizeof(obuf); + + for (;;) { + check_siginfo(); + if (strm.avail_in == 0) { + strm.next_in = ibuf; + strm.avail_in = read(i, ibuf, sizeof(ibuf)); + switch (strm.avail_in) { + case (size_t)-1: + maybe_err("read failed"); + /*NOTREACHED*/ + case 0: + action = LZMA_FINISH; + break; + default: + infile_newdata(strm.avail_in); + *bytes_in += strm.avail_in; + break; + } + } + + ret = lzma_code(&strm, action); + + // Write and check write error before checking decoder error. + // This way as much data as possible gets written to output + // even if decoder detected an error. + if (strm.avail_out == 0 || ret != LZMA_OK) { + const size_t write_size = sizeof(obuf) - strm.avail_out; + + if (write(o, obuf, write_size) != (ssize_t)write_size) + maybe_err("write failed"); + + strm.next_out = obuf; + strm.avail_out = sizeof(obuf); + bytes_out += write_size; + } + + if (ret != LZMA_OK) { + if (ret == LZMA_STREAM_END) { + // Check that there's no trailing garbage. + if (strm.avail_in != 0 || read(i, ibuf, 1)) + ret = LZMA_DATA_ERROR; + else { + lzma_end(&strm); + return bytes_out; + } + } + + const char *msg; + switch (ret) { + case LZMA_MEM_ERROR: + msg = strerror(ENOMEM); + break; + + case LZMA_FORMAT_ERROR: + msg = "File format not recognized"; + break; + + case LZMA_OPTIONS_ERROR: + // FIXME: Better message? + msg = "Unsupported compression options"; + break; + + case LZMA_DATA_ERROR: + msg = "File is corrupt"; + break; + + case LZMA_BUF_ERROR: + msg = "Unexpected end of input"; + break; + + case LZMA_MEMLIMIT_ERROR: + msg = "Reached memory limit"; + break; + + default: + maybe_errx("Unknown error (%d)", ret); + break; + } + maybe_errx("%s", msg); + + } + } +} + +#include + +/* + * Copied various bits and pieces from xz support code or brute force + * replacements. + */ + +#define my_min(A,B) ((A)<(B)?(A):(B)) + +// Some systems have suboptimal BUFSIZ. Use a bit bigger value on them. +// We also need that IO_BUFFER_SIZE is a multiple of 8 (sizeof(uint64_t)) +#if BUFSIZ <= 1024 +# define IO_BUFFER_SIZE 8192 +#else +# define IO_BUFFER_SIZE (BUFSIZ & ~7U) +#endif + +/// is_sparse() accesses the buffer as uint64_t for maximum speed. +/// Use an union to make sure that the buffer is properly aligned. +typedef union { + uint8_t u8[IO_BUFFER_SIZE]; + uint32_t u32[IO_BUFFER_SIZE / sizeof(uint32_t)]; + uint64_t u64[IO_BUFFER_SIZE / sizeof(uint64_t)]; +} io_buf; + + +static bool +io_pread(int fd, io_buf *buf, size_t size, off_t pos) +{ + // Using lseek() and read() is more portable than pread() and + // for us it is as good as real pread(). + if (lseek(fd, pos, SEEK_SET) != pos) { + return true; + } + + const size_t amount = read(fd, buf, size); + if (amount == SIZE_MAX) + return true; + + if (amount != size) { + return true; + } + + return false; +} + +/* + * Most of the following is copied (mostly verbatim) from the xz + * distribution, from file src/xz/list.c + */ + +/////////////////////////////////////////////////////////////////////////////// +// +/// \file list.c +/// \brief Listing information about .xz files +// +// Author: Lasse Collin +// +// This file has been put into the public domain. +// You can do whatever you want with this file. +// +/////////////////////////////////////////////////////////////////////////////// + + +/// Information about a .xz file +typedef struct { + /// Combined Index of all Streams in the file + lzma_index *idx; + + /// Total amount of Stream Padding + uint64_t stream_padding; + + /// Highest memory usage so far + uint64_t memusage_max; + + /// True if all Blocks so far have Compressed Size and + /// Uncompressed Size fields + bool all_have_sizes; + + /// Oldest XZ Utils version that will decompress the file + uint32_t min_version; + +} xz_file_info; + +#define XZ_FILE_INFO_INIT { NULL, 0, 0, true, 50000002 } + + +/// \brief Parse the Index(es) from the given .xz file +/// +/// \param xfi Pointer to structure where the decoded information +/// is stored. +/// \param pair Input file +/// +/// \return On success, false is returned. On error, true is returned. +/// +// TODO: This function is pretty big. liblzma should have a function that +// takes a callback function to parse the Index(es) from a .xz file to make +// it easy for applications. +static bool +parse_indexes(xz_file_info *xfi, int src_fd) +{ + struct stat st; + + if (fstat(src_fd, &st) != 0) { + return true; + } + + if (st.st_size < 2 * LZMA_STREAM_HEADER_SIZE) { + return true; + } + + io_buf buf; + lzma_stream_flags header_flags; + lzma_stream_flags footer_flags; + lzma_ret ret; + + // lzma_stream for the Index decoder + lzma_stream strm = LZMA_STREAM_INIT; + + // All Indexes decoded so far + lzma_index *combined_index = NULL; + + // The Index currently being decoded + lzma_index *this_index = NULL; + + // Current position in the file. We parse the file backwards so + // initialize it to point to the end of the file. + off_t pos = st.st_size; + + // Each loop iteration decodes one Index. + do { + // Check that there is enough data left to contain at least + // the Stream Header and Stream Footer. This check cannot + // fail in the first pass of this loop. + if (pos < 2 * LZMA_STREAM_HEADER_SIZE) { + goto error; + } + + pos -= LZMA_STREAM_HEADER_SIZE; + lzma_vli stream_padding = 0; + + // Locate the Stream Footer. There may be Stream Padding which + // we must skip when reading backwards. + while (true) { + if (pos < LZMA_STREAM_HEADER_SIZE) { + goto error; + } + + if (io_pread(src_fd, &buf, + LZMA_STREAM_HEADER_SIZE, pos)) + goto error; + + // Stream Padding is always a multiple of four bytes. + int i = 2; + if (buf.u32[i] != 0) + break; + + // To avoid calling io_pread() for every four bytes + // of Stream Padding, take advantage that we read + // 12 bytes (LZMA_STREAM_HEADER_SIZE) already and + // check them too before calling io_pread() again. + do { + stream_padding += 4; + pos -= 4; + --i; + } while (i >= 0 && buf.u32[i] == 0); + } + + // Decode the Stream Footer. + ret = lzma_stream_footer_decode(&footer_flags, buf.u8); + if (ret != LZMA_OK) { + goto error; + } + + // Check that the Stream Footer doesn't specify something + // that we don't support. This can only happen if the xz + // version is older than liblzma and liblzma supports + // something new. + // + // It is enough to check Stream Footer. Stream Header must + // match when it is compared against Stream Footer with + // lzma_stream_flags_compare(). + if (footer_flags.version != 0) { + goto error; + } + + // Check that the size of the Index field looks sane. + lzma_vli index_size = footer_flags.backward_size; + if ((lzma_vli)(pos) < index_size + LZMA_STREAM_HEADER_SIZE) { + goto error; + } + + // Set pos to the beginning of the Index. + pos -= index_size; + + // Decode the Index. + ret = lzma_index_decoder(&strm, &this_index, UINT64_MAX); + if (ret != LZMA_OK) { + goto error; + } + + do { + // Don't give the decoder more input than the + // Index size. + strm.avail_in = my_min(IO_BUFFER_SIZE, index_size); + if (io_pread(src_fd, &buf, strm.avail_in, pos)) + goto error; + + pos += strm.avail_in; + index_size -= strm.avail_in; + + strm.next_in = buf.u8; + ret = lzma_code(&strm, LZMA_RUN); + + } while (ret == LZMA_OK); + + // If the decoding seems to be successful, check also that + // the Index decoder consumed as much input as indicated + // by the Backward Size field. + if (ret == LZMA_STREAM_END) + if (index_size != 0 || strm.avail_in != 0) + ret = LZMA_DATA_ERROR; + + if (ret != LZMA_STREAM_END) { + // LZMA_BUFFER_ERROR means that the Index decoder + // would have liked more input than what the Index + // size should be according to Stream Footer. + // The message for LZMA_DATA_ERROR makes more + // sense in that case. + if (ret == LZMA_BUF_ERROR) + ret = LZMA_DATA_ERROR; + + goto error; + } + + // Decode the Stream Header and check that its Stream Flags + // match the Stream Footer. + pos -= footer_flags.backward_size + LZMA_STREAM_HEADER_SIZE; + if ((lzma_vli)(pos) < lzma_index_total_size(this_index)) { + goto error; + } + + pos -= lzma_index_total_size(this_index); + if (io_pread(src_fd, &buf, LZMA_STREAM_HEADER_SIZE, pos)) + goto error; + + ret = lzma_stream_header_decode(&header_flags, buf.u8); + if (ret != LZMA_OK) { + goto error; + } + + ret = lzma_stream_flags_compare(&header_flags, &footer_flags); + if (ret != LZMA_OK) { + goto error; + } + + // Store the decoded Stream Flags into this_index. This is + // needed so that we can print which Check is used in each + // Stream. + ret = lzma_index_stream_flags(this_index, &footer_flags); + if (ret != LZMA_OK) + goto error; + + // Store also the size of the Stream Padding field. It is + // needed to show the offsets of the Streams correctly. + ret = lzma_index_stream_padding(this_index, stream_padding); + if (ret != LZMA_OK) + goto error; + + if (combined_index != NULL) { + // Append the earlier decoded Indexes + // after this_index. + ret = lzma_index_cat( + this_index, combined_index, NULL); + if (ret != LZMA_OK) { + goto error; + } + } + + combined_index = this_index; + this_index = NULL; + + xfi->stream_padding += stream_padding; + + } while (pos > 0); + + lzma_end(&strm); + + // All OK. Make combined_index available to the caller. + xfi->idx = combined_index; + return false; + +error: + // Something went wrong, free the allocated memory. + lzma_end(&strm); + lzma_index_end(combined_index, NULL); + lzma_index_end(this_index, NULL); + return true; +} + +/***************** end of copy form list.c *************************/ + +/* + * Small wrapper to extract total length of a file + */ +off_t +unxz_len(int fd) +{ + xz_file_info xfi = XZ_FILE_INFO_INIT; + if (!parse_indexes(&xfi, fd)) { + off_t res = lzma_index_uncompressed_size(xfi.idx); + lzma_index_end(xfi.idx, NULL); + return res; + } + return 0; +} + diff --git a/src/gzip/zdiff b/src/gzip/zdiff new file mode 100644 index 0000000..f0f8c2d --- /dev/null +++ b/src/gzip/zdiff @@ -0,0 +1,141 @@ +#!/bin/sh - +# +# $NetBSD: zdiff,v 1.5 2010/04/14 20:30:28 joerg Exp $ +# +# $OpenBSD: zdiff,v 1.2 2003/07/29 07:42:44 otto Exp $ +# +#- +# Copyright (c) 2003 Todd C. Miller +# Copyright (c) 2010 Joerg Sonnenberger +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# +# Sponsored in part by the Defense Advanced Research Projects +# Agency (DARPA) and Air Force Research Laboratory, Air Force +# Materiel Command, USAF, under agreement number F39502-99-1-0512. +# + +# Set $prog based on $0 +case $0 in + *cmp) prog=cmp + ;; + *) prog=diff + ;; +esac +USAGE="usage: $0 [options] file1 [file2]" + +check_suffix() { + case "$1" in + *[._-][Zz]) + eval "$2=\${1%??}" + eval "$3=\"gzip -cdqf\"" + ;; + *[._-]bz) + eval "$2=\${1%???}" + eval "$3=\"bzip2 -cdqf\"" + ;; + *[._-]gz) + eval "$2=\${1%???}" + eval "$3=\"gzip -cdqf\"" + ;; + *[._-]xz) + eval "$2=\${1%???}" + eval "$3=\"xz -cdqf\"" + ;; + *[._-]bz2) + eval "$2=\${1%????}" + eval "$3=\"bzip2 -cdqf\"" + ;; + *[._-]lzma) + eval "$2=\${1%?????}" + eval "$3=\"xz -cdqf\"" + ;; + *.t[ag]z) + eval "$2=\${1%??}ar" + eval "$3=\"gzip -cdqf\"" + ;; + *.tbz) + eval "$2=\${1%??}ar" + eval "$3=\"bzip2 -cdqf\"" + ;; + *.tbz2) + eval "$2=\${1%???}ar" + eval "$3=\"bzip2 -cdqf\"" + ;; + *.t[lx]z) + eval "$2=\${1%??}ar" + eval "$3=\"xz -cdqf\"" + ;; + *) + eval "$2=\$1" + eval "$3=\"\"" + ;; + esac +} + + +# Pull out any command line flags so we can pass them to diff/cmp +# XXX - assumes there is no optarg +flags= +while test $# -ne 0; do + case "$1" in + --) + shift + break + ;; + -) + break + ;; + -*) + flags="$flags $1" + shift + ;; + *) + break + ;; + esac +done + +if [ $# -eq 1 ]; then + # One file given, compare compressed to uncompressed + files="$1" + check_suffix "$1" files filt + if [ -z "$filt" ]; then + echo "z$prog: unknown suffix" 1>&2 + exit 1 + fi + $filt -- "$1" | $prog $flags -- - "$files" + status=$? +elif [ $# -eq 2 ]; then + # Two files given, compare the two uncompressing as needed + check_suffix "$1" files filt + check_suffix "$2" files2 filt2 + if [ -z "$filt" -a -z "$filt2" ]; then + $prog $flags -- "$1" "$2" + elif [ -z "$filt" -a -n "$filt2" -a "$1" != "-" ]; then + $filt2 -- "$2" | $prog $flags -- "$1" - + elif [ -n "$filt" -a -z "$filt2" -a "$2" != "-" ]; then + $filt -- "$1" | $prog $flags -- - "$2" + else + tmp=`mktemp -t z$prog.XXXXXXXXXX` || exit 1 + trap "rm -f $tmp" 0 1 2 3 13 15 + ${filt2:-cat} -- "$2" > $tmp || exit $? + ${filt:-cat} -- "$1" | $prog $flags -- - "$tmp" + fi + status=$? +else + echo "$USAGE" 1>&2 + exit 1 +fi + +exit $status diff --git a/src/gzip/zdiff.1 b/src/gzip/zdiff.1 new file mode 100644 index 0000000..3ce1663 --- /dev/null +++ b/src/gzip/zdiff.1 @@ -0,0 +1,140 @@ +.\" $NetBSD: zdiff.1,v 1.5 2010/04/14 19:52:05 wiz Exp $ +.\" $OpenBSD: zdiff.1,v 1.2 2003/07/13 17:39:14 millert Exp $ +.\" +.\" Copyright (c) 2003 Todd C. Miller +.\" Copyright (c) 2010 Joerg Sonnenberger +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.\" Sponsored in part by the Defense Advanced Research Projects +.\" Agency (DARPA) and Air Force Research Laboratory, Air Force +.\" Materiel Command, USAF, under agreement number F39502-99-1-0512. +.Dd May 23, 2011 +.Dt ZDIFF 1 +.Os +.Sh NAME +.Nm zcmp , +.Nm zdiff +.Nd compare compressed files +.Sh SYNOPSIS +.Nm zcmp +.Op Ar options +.Ar file +.Op Ar file2 +.Nm zdiff +.Op Ar options +.Ar file +.Op Ar file2 +.Sh DESCRIPTION +.Nm zcmp +and +.Nm zdiff +are filters that invoke +.Xr cmp 1 +or +.Xr diff 1 +respectively to compare compressed files. +Any +.Ar options +that are specified are passed to +.Xr cmp 1 +or +.Xr diff 1 . +.Pp +If only +.Ar file1 +is specified, it is compared against a file with the same name, but +with the extension removed. +When both +.Ar file1 +or +.Ar file2 +are specified, either file may be compressed. +.Pp +Extensions handled by +.Xr gzip 1 : +.Bl -bullet -compact +.It +z, Z, +.It +gz, +.It +taz, +.It +tgz. +.El +.Pp +Extensions handled by +.Xr bzip2 1 : +.Bl -bullet -compact +.It +bz, +.It +bz2, +.It +tbz, +.It +tbz2. +.El +.Pp +Extensions handled by +.Xr xz 1 : +.Bl -bullet -compact +.It +lzma, +.It +xz, +.It +tlz, +.It +txz. +.El +.Sh ENVIRONMENT +.Bl -tag -width "TMPDIR" +.It Ev TMPDIR +Directory in which to place temporary files. +If unset, +.Pa /tmp +is used. +.El +.Sh FILES +.Bl -tag -width "/tmp/zdiff.XXXXXXXXXX" -compact +.It Pa /tmp/zcmp.XXXXXXXXXX +Temporary file for +.Nm zcmp . +.It Pa /tmp/zdiff.XXXXXXXXXX +Temporary file for +.Nm zdiff . +.El +.Sh SEE ALSO +.Xr bzip2 1 , +.Xr cmp 1 , +.Xr diff 1 , +.Xr gzip 1 , +.Xr xz 1 +.Sh CAVEATS +.Nm zcmp +and +.Nm zdiff +rely solely on the file extension to determine what is, or is not, +a compressed file. +Consequently, the following are not supported as arguments: +.Bl -dash +.It +directories +.It +device special files +.It +filenames indicating the standard input +.Pq Dq \- +.El diff --git a/src/gzip/zforce b/src/gzip/zforce new file mode 100644 index 0000000..06c897d --- /dev/null +++ b/src/gzip/zforce @@ -0,0 +1,54 @@ +#!/bin/sh - +# +# $NetBSD: zforce,v 1.2 2003/12/28 12:43:43 wiz Exp $ +# $OpenBSD: zforce,v 1.2 2003/08/05 18:22:17 deraadt Exp $ +# +#- +# Copyright (c) 2003 Otto Moerbeek +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# +prog=`basename $0` +USAGE="usage: $prog file ..." +if test $# -eq 0; then + echo $USAGE + exit 1 +fi + +ret=0 + +while test $# -ne 0; do + case "$1" in + *[._-]gz) + shift + ;; + *.t[ag]z) + shift + ;; + *) + if file "$1" | + grep -q "gzip compressed data" 2> /dev/null + then + n="$1".gz + if mv "$1" "$n" 2> /dev/null; then + echo "$1" -- renamed to "$n" + else + ret=1 + echo $prog: cannot rename "$1" to "$n" + fi + fi + shift + ;; + esac +done +exit $ret diff --git a/src/gzip/zforce.1 b/src/gzip/zforce.1 new file mode 100644 index 0000000..10010d6 --- /dev/null +++ b/src/gzip/zforce.1 @@ -0,0 +1,51 @@ +.\" $NetBSD: zforce.1,v 1.2 2003/12/28 12:43:43 wiz Exp $ +.\" $OpenBSD: zforce.1,v 1.1 2003/07/29 11:50:09 otto Exp $ +.\" +.\" Copyright (c) 2003 Otto Moerbeek +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.Dd January 26, 2007 +.Dt ZFORCE 1 +.Os +.Sh NAME +.Nm zforce +.Nd force gzip files to have a .gz suffix +.Sh SYNOPSIS +.Nm zforce +.Ar +.Sh DESCRIPTION +The +.Nm +utility renames +.Xr gzip 1 +files to have a +.Sq .gz +suffix, so that +.Xr gzip 1 +will not compress them twice. +This can be useful if file names were truncated during a file transfer. +Files that have an existing +.Sq .gz , +.Sq -gz , +.Sq _gz , +.Sq .tgz +or +.Sq .taz +suffix, or that have not been compressed by +.Xr gzip 1 , +are ignored. +.Sh SEE ALSO +.Xr gzip 1 +.Sh CAVEATS +.Nm +overwrites existing files without warning. diff --git a/src/gzip/zgrep b/src/gzip/zgrep new file mode 100755 index 0000000..474f386 --- /dev/null +++ b/src/gzip/zgrep @@ -0,0 +1,230 @@ +#!/bin/sh +# +# Copyright (c) 2003 Thomas Klausner. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. +# + +set -u +grep=grep +zcat=zstdcat + +endofopts=0 +pattern_file=0 +pattern_found=0 +grep_args="" +hyphen=0 +silent=0 + +prg=${0##*/} + +# handle being called 'zegrep' or 'zfgrep' +case ${prg} in +*egrep) + grep_args="-E";; +*fgrep) + grep_args="-F";; +esac + +catargs="-f" +case ${prg} in +zstd*) + cattool="/usr/bin/zstdcat" + catargs="-fq" + ;; +bz*) + cattool="/usr/bin/bzcat" + ;; +z*) + cattool="/usr/bin/zcat" + ;; +xz*) + cattool="/usr/bin/xzcat" + ;; +lz*) + cattool="/usr/bin/lzcat" + ;; +*) + echo "Invalid command: ${prg}" >&2 + exit 1 + ;; +esac + +# skip all options and pass them on to grep taking care of options +# with arguments, and if -e was supplied + +while [ $# -gt 0 -a ${endofopts} -eq 0 ] +do + case $1 in + # from GNU grep-2.6.0 -- keep in sync! + --) + shift + endofopts=1 + ;; + --file=*) + pattern_file=1 + grep_args="${grep_args} ${1}" + shift + ;; + --regexp=*) + if [ ${pattern_found} -ne 0 ]; then + grep_args="${grep_args} -e ${pattern}" + fi + pattern="${1#--regexp=}" + pattern_found=1 + shift + ;; + -h|--no-filename) + silent=1 + shift + ;; + -V|--version) + exec ${grep} -V + ;; + --*) + grep_args="${grep_args} $1" + shift + ;; + -[EFGHILOSUVabchilnopqsuvwxyz]*) + post="${1#-?}" + pre=${1%${post}} + grep_args="${grep_args} ${pre}" + shift + # Put back partial arg + set -- "-${post}" "$@" + ;; + -[ABCDdefm]) + if [ $# -lt 2 ] + then + echo "${prg}: missing argument for $1 flag" >&2 + exit 1 + fi + case $1 in + -e) + if [ ${pattern_found} -ne 0 ]; then + grep_args="${grep_args} -e ${pattern}" + fi + pattern="$2" + pattern_found=1 + shift 2 + continue + ;; + -f) + pattern_file=1 + ;; + *) + ;; + esac + grep_args="${grep_args} $1 $2" + shift 2 + ;; + -[ABCDdefm]*) + post="${1#-e}" + case ${1} in + -e*) + if [ ${pattern_found} -ne 0 ]; then + grep_args="${grep_args} -e ${pattern}" + fi + pattern="${post}" + pattern_found=1 + shift + continue + ;; + -f*) + pattern_file=1 + ;; + *) + ;; + esac + grep_args="${grep_args} ${post}" + shift + ;; + -) + hyphen=1 + shift + ;; + -r|-R) + echo "${prg}: the ${1} flag is not currently supported" >&2 + exit 1 + ;; + -?) + grep_args="${grep_args} $1" + shift + ;; + *) + # pattern to grep for + endofopts=1 + ;; + esac +done + +# if no -e option was found, take next argument as grep-pattern +if [ ${pattern_file} -eq 0 -a ${pattern_found} -eq 0 ] +then + if [ $# -ge 1 ]; then + pattern="$1" + shift + elif [ ${hyphen} -gt 0 ]; then + pattern="-" + else + echo "${prg}: missing pattern" >&2 + exit 1 + fi + pattern_found=1 +fi + +# Clean up possible leading blank +grep_args="${grep_args# }" + +# call grep ... +if [ $# -lt 1 ] +then + # ... on stdin + if [ ${pattern_file} -eq 0 ]; then + ${cattool} ${catargs} - | ${grep} ${grep_args} -e "${pattern}" -- - + else + ${cattool} ${catargs} - | ${grep} ${grep_args} -- - + fi + ret=$? +else + # ... on all files given on the command line + if [ ${silent} -lt 1 -a $# -gt 1 ]; then + grep_args="-H ${grep_args}" + fi + # Succeed if any file matches. First assume no match. + ret=1 + for file; do + if [ ${pattern_file} -eq 0 ]; then + ${cattool} ${catargs} -- "${file}" | + ${grep} --label="${file}" ${grep_args} -e "${pattern}" -- - + else + ${cattool} ${catargs} -- "${file}" | + ${grep} --label="${file}" ${grep_args} -- - + fi + this_ret=$? + # A match (0) overrides a no-match (1). An error (>=2) overrides all. + if [ ${this_ret} -eq 0 -a ${ret} -eq 1 ] || [ ${this_ret} -ge 2 ]; then + ret=${this_ret} + fi + done +fi + +exit ${ret} diff --git a/src/gzip/zgrep.1 b/src/gzip/zgrep.1 new file mode 100644 index 0000000..7fa42c9 --- /dev/null +++ b/src/gzip/zgrep.1 @@ -0,0 +1,94 @@ +.\" Copyright (c) 2018 Baptiste Daroussin +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. +.\" +.Dd July 20, 2020 +.Dt ZGREP 1 +.Os +.Sh NAME +.Nm zgrep , +.Nm zegrep , +.Nm zfgrep , +.Nm bzgrep , +.Nm bzegrep , +.Nm bzfgrep , +.Nm lzgrep , +.Nm lzegrep , +.Nm lzfgrep , +.Nm xzgrep , +.Nm xzegrep , +.Nm xzfgrep , +.Nm zstdgrep , +.Nm zstdegrep , +.Nm zstdfgrep +.Nd grep compressed files +.Sh SYNOPSIS +.Nm +.Oo Ar flags Oc Ar files +.Nm zegrep +.Oo Ar flags Oc Ar files +.Nm zfgrep +.Oo Ar flags Oc Ar files +.Pp +.Nm bzgrep +.Oo Ar flags Oc Ar files +.Nm bzegrep +.Oo Ar flags Oc Ar files +.Nm bzfgrep +.Oo Ar flags Oc Ar files +.Pp +.Nm lzgrep +.Oo Ar flags Oc Ar files +.Nm lzegrep +.Oo Ar flags Oc Ar files +.Nm lzfgrep +.Oo Ar flags Oc Ar files +.Pp +.Nm xzgrep +.Oo Ar flags Oc Ar files +.Nm xzegrep +.Oo Ar flags Oc Ar files +.Nm xzfgrep +.Oo Ar flags Oc Ar files +.Pp +.Nm zstdgrep +.Oo Ar flags Oc Ar files +.Nm zstdegrep +.Oo Ar flags Oc Ar files +.Nm zstdfgrep +.Oo Ar flags Oc Ar files +.Sh DESCRIPTION +Allow +.Xr grep 1 +to read compressed files. +.Sh SEE ALSO +.Xr bzip2 1 , +.Xr grep 1 , +.Xr gzip 1 , +.Xr xz 1 , +.Xr zstd 1 +.Sh AUTHORS +This version of the +.Nm +utility was written by +.An Thomas Klausner Aq Mt wiz@NetBSD.org . diff --git a/src/gzip/zless b/src/gzip/zless new file mode 100644 index 0000000..dbf4bd5 --- /dev/null +++ b/src/gzip/zless @@ -0,0 +1,6 @@ +#!/bin/sh +# +# + +export LESSOPEN="||/usr/bin/lesspipe.sh %s" +exec /usr/bin/less "$@" diff --git a/src/gzip/zmore b/src/gzip/zmore new file mode 100644 index 0000000..fb195c4 --- /dev/null +++ b/src/gzip/zmore @@ -0,0 +1,81 @@ +#!/bin/sh - +# +# $NetBSD: zmore,v 1.5 2013/12/06 13:33:15 pettai Exp $ +# +# $OpenBSD: zmore,v 1.6 2008/08/20 09:22:02 mpf Exp $ +# +#- +# Copyright (c) 2003 Todd C. Miller +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# +# Sponsored in part by the Defense Advanced Research Projects +# Agency (DARPA) and Air Force Research Laboratory, Air Force +# Materiel Command, USAF, under agreement number F39502-99-1-0512. +# + +# Pull out any command line flags so we can pass them to more/less +flags= +while test $# -ne 0; do + case "$1" in + --) + shift + break + ;; + -*) + flags="$flags $1" + shift + ;; + *) + break + ;; + esac +done + +if [ `basename $0` = "zless" ] ; then + pager=${PAGER-less} +else + pager=${PAGER-more} +fi + +# No files means read from stdin +if [ $# -eq 0 ]; then + gzip -cdfq 2>&1 | $pager $flags + exit 0 +fi + +oterm=`stty -g 2>/dev/null` +while test $# -ne 0; do + gzip -cdfq "$1" 2>&1 | $pager $flags + prev="$1" + shift + if tty -s && test -n "$oterm" -a $# -gt 0; then + #echo -n "--More--(Next file: $1)" + echo -n "$prev (END) - Next: $1 " + trap "stty $oterm 2>/dev/null" 0 1 2 3 13 15 + stty cbreak -echo 2>/dev/null + REPLY=`dd bs=1 count=1 2>/dev/null` + stty $oterm 2>/dev/null + trap - 0 1 2 3 13 15 + echo + case "$REPLY" in + s) + shift + ;; + e|q) + break + ;; + esac + fi +done +exit 0 diff --git a/src/gzip/zmore.1 b/src/gzip/zmore.1 new file mode 100644 index 0000000..5e0acc9 --- /dev/null +++ b/src/gzip/zmore.1 @@ -0,0 +1,108 @@ +.\" $NetBSD: zmore.1,v 1.4 2013/11/12 21:58:37 pettai Exp $ +.\" $OpenBSD: zmore.1,v 1.10 2009/08/16 09:41:08 sobrado Exp $ +.\" +.\" Copyright (c) 2003 Todd C. Miller +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.\" Sponsored in part by the Defense Advanced Research Projects +.\" Agency (DARPA) and Air Force Research Laboratory, Air Force +.\" Materiel Command, USAF, under agreement number F39502-99-1-0512. +.Dd October 22, 2014 +.Dt ZMORE 1 +.Os +.Sh NAME +.Nm zmore , +.Nm zless +.Nd view compressed files +.Sh SYNOPSIS +.Nm zmore +.Op Ar flags +.Op Ar +.Nm zless +.Op Ar flags +.Op Ar +.Sh DESCRIPTION +.Nm +is a filter that allows the viewing of files compressed with Lempel-Ziv +encoding. +Such files generally have a +.Dq Z +or +.Dq gz +extension (both the +.Xr compress 1 +and +.Xr gzip 1 +formats are supported). +Any +.Ar flags +that are specified are passed to the user's preferred +.Ev PAGER +(which is +.Pa /usr/bin/more +by default). +.Pp +.Nm zless +is equivalent to +.Nm zmore +but uses +.Xr less 1 +as a pager instead of +.Xr more 1 . +.Pp +When multiple files are specified, +.Nm +will pause at the end of each file and present the following prompt to the user: +.Bd -literal -offset indent +prev_file (END) - Next: next_file +.Ed +.Pp +Where +.Sy prev_file +is the file that was just displayed and +.Sy next_file +is the next file to be displayed. +The following keys are recognized at the prompt: +.Bl -tag -width "e or q" -offset indent +.It Ic e No or Ic q +quit +.Nm zmore . +.It Ic s +skip the next file (or exit if the next file is the last). +.El +.Pp +If no files are specified, +.Nm +will read from the standard input. +In this mode +.Nm +will assume +.Xr gzip 1 +style compression since there is no suffix on which to make a decision. +.Sh ENVIRONMENT +.Bl -tag -width "PAGER" +.It Ev PAGER +Program used to display files. +If unset, +.Pa /usr/bin/more +is used +.Pq Nm zmore +or +.Pa /usr/bin/less +.Pq Nm zless . +.El +.Sh SEE ALSO +.Xr compress 1 , +.Xr less 1 , +.Xr more 1 diff --git a/src/gzip/znew b/src/gzip/znew new file mode 100644 index 0000000..498c1b7 --- /dev/null +++ b/src/gzip/znew @@ -0,0 +1,136 @@ +#!/bin/sh - +# +# $NetBSD: znew,v 1.3 2008/04/27 09:07:13 nakayama Exp $ +# $OpenBSD: znew,v 1.2 2003/08/05 18:22:17 deraadt Exp $ +# +#- +# Copyright (c) 2003 Otto Moerbeek +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +# Return 0 if the first arg file size is smaller than the second, 1 otherwise. +smaller () { + a=`du -k "$1" | awk '{ print $1 }'` + b=`du -k "$2" | awk '{ print $1 }'` + test $a -lt $b +} + +# Check gzip integrity if the -t flag is specified +checkfile () { + if test $tflag -eq 1; then + gzip -qt < "$1" + fi +} + +# Decompress a file and then gzip it +process () { + prefix="${1%.Z}" + filez="$prefix".Z + filegz="$prefix".gz + + if test ! -e "$filez"; then + echo "$prog: $filez does not exist" + return 1 + fi + if test ! -f "$filez"; then + echo "$prog: $filez is not a regular file" + return 1 + fi + if test -e "$filegz" -a $fflag -eq 0; then + echo "$prog: $filegz already exists" + return 1 + fi + + tmp=`mktemp /tmp/znewXXXXXXXXXX` || { + echo "$prog: cannot create tmp file" + return 1 + } + trap 'rm -f "$tmp"; exit 1' HUP INT QUIT PIPE TERM + + # Do the actual work, producing a file "$tmp" + if uncompress -f -c < "$filez" | gzip -f -c $gzipflags > "$tmp"; then + if test $kflag -eq 1 && smaller "$filez" "$tmp"; then + echo -n "$prog: $filez is smaller than $filegz" + echo "; keeping it" + rm -f "$tmp" + return 0 + fi + if ! checkfile "$tmp"; then + echo "$prog: integrity check of $tmp failed" + rm -f "$tmp" + return 1; + fi + + # Try to keep the mode of the original file + if ! cp -fp "$filez" "$filegz"; then + echo "$prog: warning: could not keep mode of $filez" + fi + if ! cp "$tmp" "$filegz" 2> /dev/null; then + echo "$prog: warning: could not keep mode of $filez" + if ! cp -f "$tmp" "$filegz" 2> /dev/null; then + echo "$prog: could not copy $tmp to $filegz" + rm -f "$filegz" "$tmp" + return 1 + fi + fi + if ! touch -fr "$filez" "$filegz"; then + echo -n "$prog: warning: could not keep timestamp of " + echo "$filez" + fi + rm -f "$filez" "$tmp" + else + echo "$prog: failed to process $filez" + rm -f "$tmp" + return 1 + fi +} + +prog=`basename "$0"` +usage="usage: $prog [-ftv9K] file ..." + +fflag=0 +tflag=0 +kflag=0 +gzipflags= + +# -P flag is recognized to maintain compatibility, but ignored. Pipe mode is +# always used +while getopts :ftv9PK i; do + case $i in + f) fflag=1;; + t) tflag=1;; + v) gzipflags="-v $gzipflags";; + 9) gzipflags="-9 $gzipflags";; + P) ;; + K) kflag=1;; + \?) echo "$usage"; exit 1;; + esac +done + +shift $((OPTIND - 1)) + +if test $# -eq 0; then + echo "$usage" + exit 1 +fi + +rc=0 + +while test $# -ne 0; do + if ! process "$1"; then + rc=$? + fi + shift +done +exit $rc diff --git a/src/gzip/znew.1 b/src/gzip/znew.1 new file mode 100644 index 0000000..0b97fe6 --- /dev/null +++ b/src/gzip/znew.1 @@ -0,0 +1,69 @@ +.\" $NetBSD: znew.1,v 1.2 2003/12/28 12:43:43 wiz Exp $ +.\" $OpenBSD: znew.1,v 1.1 2003/08/02 20:52:50 otto Exp $ +.\" +.\" Copyright (c) 2003 Otto Moerbeek +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.Dd January 26, 2007 +.Dt ZNEW 1 +.Os +.Sh NAME +.Nm znew +.Nd convert compressed files to gzipped files +.Sh SYNOPSIS +.Nm +.Op Fl ftv9K +.Ar +.Sh DESCRIPTION +The +.Nm +utility uncompresses files compressed by +.Xr compress 1 +and recompresses them with +.Xr gzip 1 . +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl f +Overwrite existing +.Sq .gz +files. +Unless this option is specified, +.Nm +refuses to overwrite existing files. +.It Fl t +Test integrity of the gzipped file before deleting the original file. +If the integrity check fails, the original +.Sq .Z +file is not removed. +.It Fl v +Print a report specifying the achieved compression ratios. +.It Fl 9 +Use the -9 mode of +.Xr gzip 1 , +achieving better compression at the cost of slower execution. +.It Fl K +Keep the original +.Sq .Z +file if it uses less disk blocks than the gzipped one. +A disk block is 1024 bytes. +.El +.Sh SEE ALSO +.Xr gzip 1 +.Sh CAVEATS +The +.Nm +utility tries to maintain the file mode of the original file. +If the original file is not writable, it is not able to do that and +.Nm +will print a warning. diff --git a/src/gzip/zuncompress.c b/src/gzip/zuncompress.c new file mode 100644 index 0000000..8536f8a --- /dev/null +++ b/src/gzip/zuncompress.c @@ -0,0 +1,412 @@ +/* $NetBSD: zuncompress.c,v 1.11 2011/08/16 13:55:02 joerg Exp $ */ + +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1985, 1986, 1992, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Diomidis Spinellis and James A. Woods, derived from original + * work by Spencer Thomas and Joseph Orost. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + * + * from: NetBSD: zopen.c,v 1.8 2003/08/07 11:13:29 agc Exp + */ + +/* This file is #included by gzip.c */ + +#ifndef EFTYPE +#define EFTYPE EINVAL +#endif + +static int zread(void *, char *, int); + +#define tab_prefixof(i) (zs->zs_codetab[i]) +#define tab_suffixof(i) ((char_type *)(zs->zs_htab))[i] +#define de_stack ((char_type *)&tab_suffixof(1 << BITS)) + +#define BITS 16 /* Default bits. */ +#define HSIZE 69001 /* 95% occupancy */ /* XXX may not need HSIZE */ +#define BIT_MASK 0x1f /* Defines for third byte of header. */ +#define BLOCK_MASK 0x80 +#define CHECK_GAP 10000 /* Ratio check interval. */ +#define BUFSIZE (64 * 1024) + +/* + * Masks 0x40 and 0x20 are free. I think 0x20 should mean that there is + * a fourth header byte (for expansion). + */ +#define INIT_BITS 9 /* Initial number of bits/code. */ + +/* + * the next two codes should not be changed lightly, as they must not + * lie within the contiguous general code space. + */ +#define FIRST 257 /* First free entry. */ +#define CLEAR 256 /* Table clear output code. */ + + +#define MAXCODE(n_bits) ((1 << (n_bits)) - 1) + +typedef long code_int; +typedef long count_int; +typedef u_char char_type; + +static char_type magic_header[] = + {'\037', '\235'}; /* 1F 9D */ + +static char_type rmask[9] = + {0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff}; + +static off_t total_compressed_bytes; +static size_t compressed_prelen; +static char *compressed_pre; + +struct s_zstate { + FILE *zs_fp; /* File stream for I/O */ + char zs_mode; /* r or w */ + enum { + S_START, S_MIDDLE, S_EOF + } zs_state; /* State of computation */ + int zs_n_bits; /* Number of bits/code. */ + int zs_maxbits; /* User settable max # bits/code. */ + code_int zs_maxcode; /* Maximum code, given n_bits. */ + code_int zs_maxmaxcode; /* Should NEVER generate this code. */ + count_int zs_htab [HSIZE]; + u_short zs_codetab [HSIZE]; + code_int zs_hsize; /* For dynamic table sizing. */ + code_int zs_free_ent; /* First unused entry. */ + /* + * Block compression parameters -- after all codes are used up, + * and compression rate changes, start over. + */ + int zs_block_compress; + int zs_clear_flg; + long zs_ratio; + count_int zs_checkpoint; + int zs_offset; + long zs_in_count; /* Length of input. */ + long zs_bytes_out; /* Length of compressed output. */ + long zs_out_count; /* # of codes output (for debugging). */ + char_type zs_buf[BITS]; + union { + struct { + long zs_fcode; + code_int zs_ent; + code_int zs_hsize_reg; + int zs_hshift; + } w; /* Write parameters */ + struct { + char_type *zs_stackp; + int zs_finchar; + code_int zs_code, zs_oldcode, zs_incode; + int zs_roffset, zs_size; + char_type zs_gbuf[BITS]; + } r; /* Read parameters */ + } u; +}; + +static code_int getcode(struct s_zstate *zs); + +static off_t +zuncompress(FILE *in, FILE *out, char *pre, size_t prelen, + off_t *compressed_bytes) +{ + off_t bin, bout = 0; + char *buf; + + buf = malloc(BUFSIZE); + if (buf == NULL) + return -1; + + /* XXX */ + compressed_prelen = prelen; + if (prelen != 0) + compressed_pre = pre; + else + compressed_pre = NULL; + + while ((bin = fread(buf, 1, BUFSIZE, in)) != 0) { + if (tflag == 0 && (off_t)fwrite(buf, 1, bin, out) != bin) { + free(buf); + return -1; + } + bout += bin; + } + + if (compressed_bytes) + *compressed_bytes = total_compressed_bytes; + + free(buf); + return bout; +} + +/* Forward declaration with fopencookie signature */ +static ssize_t zread_cookie(void *cookie, char *rbp, size_t num); +static int zclose(void *zs); + +static int +zclose(void *zs) +{ + free(zs); + /* We leave the caller to close the fd passed to zdopen() */ + return 0; +} + +FILE * +zdopen(int fd) +{ + struct s_zstate *zs; + + if ((zs = calloc(1, sizeof(struct s_zstate))) == NULL) + return (NULL); + + zs->zs_state = S_START; + + zs->zs_hsize = HSIZE; + zs->zs_free_ent = 0; + zs->zs_block_compress = BLOCK_MASK; + zs->zs_clear_flg = 0; + zs->zs_ratio = 0; + zs->zs_checkpoint = CHECK_GAP; + zs->zs_in_count = 1; + zs->zs_out_count = 0; + zs->u.r.zs_roffset = 0; + zs->u.r.zs_size = 0; + + if ((zs->zs_fp = fdopen(fd, "r")) == NULL) { + free(zs); + return NULL; + } + + cookie_io_functions_t io = { + .read = zread_cookie, + .write = NULL, + .seek = NULL, + .close = zclose, + }; + return fopencookie(zs, "r", io); +} + +/* + * Decompress read. This routine adapts to the codes in the file building + * the "string" table on-the-fly; requiring no table to be stored in the + * compressed file. The tables used herein are shared with those of the + * compress() routine. See the definitions above. + */ +static int +zread(void *cookie, char *rbp, int num) +{ + u_int count, i; + struct s_zstate *zs; + u_char *bp, header[3]; + + if (num == 0) + return (0); + + zs = cookie; + count = num; + bp = (u_char *)rbp; + switch (zs->zs_state) { + case S_START: + zs->zs_state = S_MIDDLE; + break; + case S_MIDDLE: + goto middle; + case S_EOF: + goto eof; + } + + /* Check the magic number */ + for (i = 0; i < 3 && compressed_prelen; i++, compressed_prelen--) + header[i] = *compressed_pre++; + + if (fread(header + i, 1, sizeof(header) - i, zs->zs_fp) != + sizeof(header) - i || + memcmp(header, magic_header, sizeof(magic_header)) != 0) { + errno = EFTYPE; + return (-1); + } + total_compressed_bytes = 0; + zs->zs_maxbits = header[2]; /* Set -b from file. */ + zs->zs_block_compress = zs->zs_maxbits & BLOCK_MASK; + zs->zs_maxbits &= BIT_MASK; + zs->zs_maxmaxcode = 1L << zs->zs_maxbits; + if (zs->zs_maxbits > BITS || zs->zs_maxbits < 12) { + errno = EFTYPE; + return (-1); + } + /* As above, initialize the first 256 entries in the table. */ + zs->zs_maxcode = MAXCODE(zs->zs_n_bits = INIT_BITS); + for (zs->u.r.zs_code = 255; zs->u.r.zs_code >= 0; zs->u.r.zs_code--) { + tab_prefixof(zs->u.r.zs_code) = 0; + tab_suffixof(zs->u.r.zs_code) = (char_type) zs->u.r.zs_code; + } + zs->zs_free_ent = zs->zs_block_compress ? FIRST : 256; + + zs->u.r.zs_oldcode = -1; + zs->u.r.zs_stackp = de_stack; + + while ((zs->u.r.zs_code = getcode(zs)) > -1) { + + if ((zs->u.r.zs_code == CLEAR) && zs->zs_block_compress) { + for (zs->u.r.zs_code = 255; zs->u.r.zs_code >= 0; + zs->u.r.zs_code--) + tab_prefixof(zs->u.r.zs_code) = 0; + zs->zs_clear_flg = 1; + zs->zs_free_ent = FIRST; + zs->u.r.zs_oldcode = -1; + continue; + } + zs->u.r.zs_incode = zs->u.r.zs_code; + + /* Special case for KwKwK string. */ + if (zs->u.r.zs_code >= zs->zs_free_ent) { + if (zs->u.r.zs_code > zs->zs_free_ent || + zs->u.r.zs_oldcode == -1) { + /* Bad stream. */ + errno = EFTYPE; + return (-1); + } + *zs->u.r.zs_stackp++ = zs->u.r.zs_finchar; + zs->u.r.zs_code = zs->u.r.zs_oldcode; + } + /* + * The above condition ensures that code < free_ent. + * The construction of tab_prefixof in turn guarantees that + * each iteration decreases code and therefore stack usage is + * bound by 1 << BITS - 256. + */ + + /* Generate output characters in reverse order. */ + while (zs->u.r.zs_code >= 256) { + *zs->u.r.zs_stackp++ = tab_suffixof(zs->u.r.zs_code); + zs->u.r.zs_code = tab_prefixof(zs->u.r.zs_code); + } + *zs->u.r.zs_stackp++ = zs->u.r.zs_finchar = tab_suffixof(zs->u.r.zs_code); + + /* And put them out in forward order. */ +middle: do { + if (count-- == 0) + return (num); + *bp++ = *--zs->u.r.zs_stackp; + } while (zs->u.r.zs_stackp > de_stack); + + /* Generate the new entry. */ + if ((zs->u.r.zs_code = zs->zs_free_ent) < zs->zs_maxmaxcode && + zs->u.r.zs_oldcode != -1) { + tab_prefixof(zs->u.r.zs_code) = (u_short) zs->u.r.zs_oldcode; + tab_suffixof(zs->u.r.zs_code) = zs->u.r.zs_finchar; + zs->zs_free_ent = zs->u.r.zs_code + 1; + } + + /* Remember previous code. */ + zs->u.r.zs_oldcode = zs->u.r.zs_incode; + } + zs->zs_state = S_EOF; +eof: return (num - count); +} + +static ssize_t +zread_cookie(void *cookie, char *rbp, size_t num) +{ + return zread(cookie, rbp, (int)num); +} + +/*- + * Read one code from the standard input. If EOF, return -1. + * Inputs: + * stdin + * Outputs: + * code or -1 is returned. + */ +static code_int +getcode(struct s_zstate *zs) +{ + code_int gcode; + int r_off, bits, i; + char_type *bp; + + bp = zs->u.r.zs_gbuf; + if (zs->zs_clear_flg > 0 || zs->u.r.zs_roffset >= zs->u.r.zs_size || + zs->zs_free_ent > zs->zs_maxcode) { + /* + * If the next entry will be too big for the current gcode + * size, then we must increase the size. This implies reading + * a new buffer full, too. + */ + if (zs->zs_free_ent > zs->zs_maxcode) { + zs->zs_n_bits++; + if (zs->zs_n_bits == zs->zs_maxbits) /* Won't get any bigger now. */ + zs->zs_maxcode = zs->zs_maxmaxcode; + else + zs->zs_maxcode = MAXCODE(zs->zs_n_bits); + } + if (zs->zs_clear_flg > 0) { + zs->zs_maxcode = MAXCODE(zs->zs_n_bits = INIT_BITS); + zs->zs_clear_flg = 0; + } + /* XXX */ + for (i = 0; i < zs->zs_n_bits && compressed_prelen; i++, compressed_prelen--) + zs->u.r.zs_gbuf[i] = *compressed_pre++; + zs->u.r.zs_size = fread(zs->u.r.zs_gbuf + i, 1, zs->zs_n_bits - i, zs->zs_fp); + zs->u.r.zs_size += i; + if (zs->u.r.zs_size <= 0) /* End of file. */ + return (-1); + zs->u.r.zs_roffset = 0; + + total_compressed_bytes += zs->u.r.zs_size; + + /* Round size down to integral number of codes. */ + zs->u.r.zs_size = (zs->u.r.zs_size << 3) - (zs->zs_n_bits - 1); + } + r_off = zs->u.r.zs_roffset; + bits = zs->zs_n_bits; + + /* Get to the first byte. */ + bp += (r_off >> 3); + r_off &= 7; + + /* Get first part (low order bits). */ + gcode = (*bp++ >> r_off); + bits -= (8 - r_off); + r_off = 8 - r_off; /* Now, roffset into gcode word. */ + + /* Get any 8 bit parts in the middle (<=1 for up to 16 bits). */ + if (bits >= 8) { + gcode |= *bp++ << r_off; + r_off += 8; + bits -= 8; + } + + /* High order bits. */ + gcode |= (*bp & rmask[bits]) << r_off; + zs->u.r.zs_roffset += zs->zs_n_bits; + + return (gcode); +} + diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..3cd8188 --- /dev/null +++ b/src/main.c @@ -0,0 +1,91 @@ +#include +#include +#include +#include + +int patch_main(int argc, char *argv[]); +int diff_main(int argc, char *argv[]); +int cmp_main(int argc, char *argv[]); +int diff3_main(int argc, char *argv[]); +int sdiff_main(int argc, char *argv[]); +int which_main(int argc, char *argv[]); +int gzip_main(int argc, char *argv[]); +int grep_main(int argc, char *argv[]); +int find_main(int argc, char *argv[]); +int xargs_main(int argc, char *argv[]); + +struct applet { + const char *name; + int (*main)(int, char *[]); +}; + +static const struct applet applets[] = { + { "patch", patch_main }, + { "diff", diff_main }, + { "cmp", cmp_main }, + { "diff3", diff3_main }, + { "sdiff", sdiff_main }, + { "which", which_main }, + { "gzip", gzip_main }, + { "gunzip", gzip_main }, + { "zcat", gzip_main }, + { "gzcat", gzip_main }, + { "uncompress", gzip_main }, + { "grep", grep_main }, + { "egrep", grep_main }, + { "fgrep", grep_main }, + { "rgrep", grep_main }, + { "find", find_main }, + { "xargs", xargs_main }, +}; + +static const size_t n_applets = sizeof(applets) / sizeof(applets[0]); + +static const struct applet * +find_applet(const char *name) +{ + for (size_t i = 0; i < n_applets; i++) { + if (strcmp(applets[i].name, name) == 0) + return &applets[i]; + } + return NULL; +} + +static void +usage(void) +{ + fprintf(stderr, "usage: vx [args...]\n"); + fprintf(stderr, " or invoke via symlink (e.g. patch -> vx)\n\n"); + fprintf(stderr, "available applets:\n"); + for (size_t i = 0; i < n_applets; i++) + fprintf(stderr, " %s\n", applets[i].name); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + const struct applet *ap; + char *progname; + + progname = basename(argv[0]); + + /* Direct symlink invocation: argv[0] matches an applet name */ + if (strcmp(progname, "vx") != 0) { + ap = find_applet(progname); + if (ap != NULL) + return ap->main(argc, argv); + } + + /* Subcommand invocation: vx [args...] */ + if (argc < 2) + usage(); + + ap = find_applet(argv[1]); + if (ap == NULL) { + fprintf(stderr, "vx: unknown applet '%s'\n", argv[1]); + usage(); + } + + return ap->main(argc - 1, argv + 1); +} diff --git a/src/patch/backupfile.c b/src/patch/backupfile.c new file mode 100644 index 0000000..76ccbca --- /dev/null +++ b/src/patch/backupfile.c @@ -0,0 +1,244 @@ +/*- + * Copyright (C) 1990 Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * without restriction. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * backupfile.c -- make Emacs style backup file names + * + * David MacKenzie . Some algorithms adapted from GNU Emacs. + * + * $OpenBSD: backupfile.c,v 1.20 2009/10/27 23:59:41 deraadt Exp $ + */ + +#include +#include +#include +#include +#include +#include + +#include "backupfile.h" + + +#define ISDIGIT(c) (isascii ((unsigned char)c) && isdigit ((unsigned char)c)) + +/* Which type of backup file names are generated. */ +enum backup_type backup_type = none; + +/* + * The extension added to file names to produce a simple (as opposed to + * numbered) backup file name. + */ +const char *simple_backup_suffix = "~"; + +static char *concat(const char *, const char *); +static char *make_version_name(const char *, int); +static int max_backup_version(const char *, const char *); +static int version_number(const char *, const char *, size_t); +static int argmatch(const char *, const char **); +static void invalid_arg(const char *, const char *, int); + +/* + * Return the name of the new backup file for file FILE, allocated with + * malloc. Return 0 if out of memory. FILE must not end with a '/' unless it + * is the root directory. Do not call this function if backup_type == none. + */ +char * +find_backup_file_name(const char *file) +{ + char *dir, *base_versions, *tmp_file; + int highest_backup; + + if (backup_type == simple) + return concat(file, simple_backup_suffix); + tmp_file = strdup(file); + if (tmp_file == NULL) + return NULL; + base_versions = concat(basename(tmp_file), ".~"); + free(tmp_file); + if (base_versions == NULL) + return NULL; + tmp_file = strdup(file); + if (tmp_file == NULL) { + free(base_versions); + return NULL; + } + dir = dirname(tmp_file); + if (dir == NULL) { + free(base_versions); + free(tmp_file); + return NULL; + } + highest_backup = max_backup_version(base_versions, dir); + free(base_versions); + free(tmp_file); + if (backup_type == numbered_existing && highest_backup == 0) + return concat(file, simple_backup_suffix); + return make_version_name(file, highest_backup + 1); +} + +/* + * Return the number of the highest-numbered backup file for file FILE in + * directory DIR. If there are no numbered backups of FILE in DIR, or an + * error occurs reading DIR, return 0. FILE should already have ".~" appended + * to it. + */ +static int +max_backup_version(const char *file, const char *dir) +{ + DIR *dirp; + struct dirent *dp; + int highest_version, this_version; + size_t file_name_length; + + dirp = opendir(dir); + if (dirp == NULL) + return 0; + + highest_version = 0; + file_name_length = strlen(file); + + while ((dp = readdir(dirp)) != NULL) { + if (strlen(dp->d_name) <= file_name_length) + continue; + + this_version = version_number(file, dp->d_name, file_name_length); + if (this_version > highest_version) + highest_version = this_version; + } + closedir(dirp); + return highest_version; +} + +/* + * Return a string, allocated with malloc, containing "FILE.~VERSION~". + * Return 0 if out of memory. + */ +static char * +make_version_name(const char *file, int version) +{ + char *backup_name; + + if (asprintf(&backup_name, "%s.~%d~", file, version) == -1) + return NULL; + return backup_name; +} + +/* + * If BACKUP is a numbered backup of BASE, return its version number; + * otherwise return 0. BASE_LENGTH is the length of BASE. BASE should + * already have ".~" appended to it. + */ +static int +version_number(const char *base, const char *backup, size_t base_length) +{ + int version; + const char *p; + + version = 0; + if (!strncmp(base, backup, base_length) && ISDIGIT(backup[base_length])) { + for (p = &backup[base_length]; ISDIGIT(*p); ++p) + version = version * 10 + *p - '0'; + if (p[0] != '~' || p[1]) + version = 0; + } + return version; +} + +/* + * Return the newly-allocated concatenation of STR1 and STR2. If out of + * memory, return 0. + */ +static char * +concat(const char *str1, const char *str2) +{ + char *newstr; + + if (asprintf(&newstr, "%s%s", str1, str2) == -1) + return NULL; + return newstr; +} + +/* + * If ARG is an unambiguous match for an element of the null-terminated array + * OPTLIST, return the index in OPTLIST of the matched element, else -1 if it + * does not match any element or -2 if it is ambiguous (is a prefix of more + * than one element). + */ +static int +argmatch(const char *arg, const char **optlist) +{ + int i; /* Temporary index in OPTLIST. */ + size_t arglen; /* Length of ARG. */ + int matchind = -1; /* Index of first nonexact match. */ + int ambiguous = 0; /* If nonzero, multiple nonexact match(es). */ + + arglen = strlen(arg); + + /* Test all elements for either exact match or abbreviated matches. */ + for (i = 0; optlist[i]; i++) { + if (!strncmp(optlist[i], arg, arglen)) { + if (strlen(optlist[i]) == arglen) + /* Exact match found. */ + return i; + else if (matchind == -1) + /* First nonexact match found. */ + matchind = i; + else + /* Second nonexact match found. */ + ambiguous = 1; + } + } + if (ambiguous) + return -2; + else + return matchind; +} + +/* + * Error reporting for argmatch. KIND is a description of the type of entity + * that was being matched. VALUE is the invalid value that was given. PROBLEM + * is the return value from argmatch. + */ +static void +invalid_arg(const char *kind, const char *value, int problem) +{ + fprintf(stderr, "patch: "); + if (problem == -1) + fprintf(stderr, "invalid"); + else /* Assume -2. */ + fprintf(stderr, "ambiguous"); + fprintf(stderr, " %s `%s'\n", kind, value); +} + +static const char *backup_args[] = { + "none", "never", "simple", "nil", "existing", "t", "numbered", 0 +}; + +static enum backup_type backup_types[] = { + none, simple, simple, numbered_existing, + numbered_existing, numbered, numbered +}; + +/* + * Return the type of backup indicated by VERSION. Unique abbreviations are + * accepted. + */ +enum backup_type +get_version(const char *version) +{ + int i; + + if (version == NULL || *version == '\0') + return numbered_existing; + i = argmatch(version, backup_args); + if (i >= 0) + return backup_types[i]; + invalid_arg("version control type", version, i); + exit(2); +} diff --git a/src/patch/backupfile.h b/src/patch/backupfile.h new file mode 100644 index 0000000..925212b --- /dev/null +++ b/src/patch/backupfile.h @@ -0,0 +1,38 @@ +/*- + * Copyright (C) 1990 Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * without restriction. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * + * backupfile.h -- declarations for making Emacs style backup file names + * + * $OpenBSD: backupfile.h,v 1.6 2003/07/28 18:35:36 otto Exp $ + */ + +/* When to make backup files. */ +enum backup_type { + /* Never make backups. */ + none, + + /* Make simple backups of every file. */ + simple, + + /* + * Make numbered backups of files that already have numbered backups, + * and simple backups of the others. + */ + numbered_existing, + + /* Make numbered backups of every file. */ + numbered +}; + +extern enum backup_type backup_type; +extern const char *simple_backup_suffix; + +char *find_backup_file_name(const char *file); +enum backup_type get_version(const char *version); diff --git a/src/patch/common.h b/src/patch/common.h new file mode 100644 index 0000000..0e73bd8 --- /dev/null +++ b/src/patch/common.h @@ -0,0 +1,111 @@ +/*- + * Copyright 1986, Larry Wall + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following condition is met: + * 1. Redistributions of source code must retain the above copyright notice, + * this condition and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + * + * patch - a program to apply diffs to original files + * + * -C option added in 1998, original code by Marc Espie, based on FreeBSD + * behaviour + * + * $OpenBSD: common.h,v 1.26 2006/03/11 19:41:30 otto Exp $ + */ + +#include + +#include +#include + +#define DEBUGGING + +/* constants */ + +#define MAXHUNKSIZE 200000 /* is this enough lines? */ +#define INITHUNKMAX 125 /* initial dynamic allocation size */ +#define INITLINELEN 4096 +#define BUFFERSIZE 4096 +#define LINENUM_MAX LONG_MAX + +#define ORIGEXT ".orig" +#define REJEXT ".rej" + +/* handy definitions */ + +#define strEQ(s1,s2) (strcmp(s1, s2) == 0) +#define strnNE(s1,s2,l) (strncmp(s1, s2, l) != 0) +#define strnEQ(s1,s2,l) (strncmp(s1, s2, l) == 0) + +/* typedefs */ + +typedef long LINENUM; /* must be signed */ + +/* globals */ + +extern mode_t filemode; + +extern char *buf; /* general purpose buffer */ +extern size_t buf_size; /* size of general purpose buffer */ + +extern bool using_plan_a; /* try to keep everything in memory */ +extern bool out_of_mem; /* ran out of memory in plan a */ +extern bool nonempty_patchf_seen; /* seen a non-zero-length patch file? */ + +#define MAXFILEC 2 + +extern char *filearg[MAXFILEC]; +extern bool ok_to_create_file; +extern char *outname; +extern char *origprae; + +extern char *TMPOUTNAME; +extern char *TMPINNAME; +extern char *TMPREJNAME; +extern char *TMPPATNAME; +extern bool toutkeep; +extern bool trejkeep; + +#ifdef DEBUGGING +extern int debug; +#endif + +extern bool force; +extern bool batch; +extern bool verbose; +extern bool reverse; +extern bool noreverse; +extern bool skip_rest_of_patch; +extern int strippath; +extern bool canonicalize; +/* TRUE if -C was specified on command line. */ +extern bool check_only; +extern bool warn_on_invalid_line; +extern bool last_line_missing_eol; + + +#define CONTEXT_DIFF 1 +#define NORMAL_DIFF 2 +#define ED_DIFF 3 +#define NEW_CONTEXT_DIFF 4 +#define UNI_DIFF 5 + +extern int diff_type; +extern char *revision; /* prerequisite revision, if any */ +extern LINENUM input_lines; /* how long is input file in lines */ + +extern int posix; + diff --git a/src/patch/inp.c b/src/patch/inp.c new file mode 100644 index 0000000..24fdcac --- /dev/null +++ b/src/patch/inp.c @@ -0,0 +1,437 @@ +/*- + * Copyright 1986, Larry Wall + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following condition is met: + * 1. Redistributions of source code must retain the above copyright notice, + * this condition and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + * + * patch - a program to apply diffs to original files + * + * -C option added in 1998, original code by Marc Espie, based on FreeBSD + * behaviour + * + * $OpenBSD: inp.c,v 1.44 2015/07/26 14:32:19 millert Exp $ + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "util.h" +#include "pch.h" +#include "inp.h" + + +/* Input-file-with-indexable-lines abstract type */ + +static size_t i_size; /* size of the input file */ +static char *i_womp; /* plan a buffer for entire file */ +static char **i_ptr; /* pointers to lines in i_womp */ +static char empty_line[] = { '\0' }; + +static int tifd = -1; /* plan b virtual string array */ +static char *tibuf[2]; /* plan b buffers */ +static LINENUM tiline[2] = {-1, -1}; /* 1st line in each buffer */ +static size_t lines_per_buf; /* how many lines per buffer */ +static size_t tibuflen; /* plan b buffer length */ +static size_t tireclen; /* length of records in tmp file */ + +static bool rev_in_string(const char *); +static bool reallocate_lines(size_t *); + +/* returns false if insufficient memory */ +static bool plan_a(const char *); + +static void plan_b(const char *); + +/* New patch--prepare to edit another file. */ + +void +re_input(void) +{ + if (using_plan_a) { + free(i_ptr); + i_ptr = NULL; + if (i_womp != NULL) { + munmap(i_womp, i_size); + i_womp = NULL; + } + i_size = 0; + } else { + using_plan_a = true; /* maybe the next one is smaller */ + close(tifd); + tifd = -1; + free(tibuf[0]); + free(tibuf[1]); + tibuf[0] = tibuf[1] = NULL; + tiline[0] = tiline[1] = -1; + tireclen = 0; + } +} + +/* Construct the line index, somehow or other. */ + +void +scan_input(const char *filename) +{ + if (!plan_a(filename)) + plan_b(filename); + if (verbose) { + say("Patching file %s using Plan %s...\n", filename, + (using_plan_a ? "A" : "B")); + } +} + +static bool +reallocate_lines(size_t *lines_allocated) +{ + char **p; + size_t new_size; + + new_size = *lines_allocated * 3 / 2; + p = reallocarray(i_ptr, new_size + 2, sizeof(char *)); + if (p == NULL) { /* shucks, it was a near thing */ + munmap(i_womp, i_size); + i_womp = NULL; + free(i_ptr); + i_ptr = NULL; + *lines_allocated = 0; + return false; + } + *lines_allocated = new_size; + i_ptr = p; + return true; +} + +/* Try keeping everything in memory. */ + +static bool +plan_a(const char *filename) +{ + int ifd, statfailed; + char *p, *s; + struct stat filestat; + ptrdiff_t sz; + size_t i; + size_t iline, lines_allocated; + +#ifdef DEBUGGING + if (debug & 8) + return false; +#endif + + if (filename == NULL || *filename == '\0') + return false; + + statfailed = stat(filename, &filestat); + if (statfailed && ok_to_create_file) { + if (verbose) + say("(Creating file %s...)\n", filename); + + /* + * in check_patch case, we still display `Creating file' even + * though we're not. The rule is that -C should be as similar + * to normal patch behavior as possible + */ + if (check_only) + return true; + makedirs(filename, true); + close(creat(filename, 0666)); + statfailed = stat(filename, &filestat); + } + if (statfailed) + fatal("can't find %s\n", filename); + filemode = filestat.st_mode; + if (!S_ISREG(filemode)) + fatal("%s is not a normal file--can't patch\n", filename); + if ((uint64_t)filestat.st_size > SIZE_MAX) { + say("block too large to mmap\n"); + return false; + } + i_size = (size_t)filestat.st_size; + if (out_of_mem) { + set_hunkmax(); /* make sure dynamic arrays are allocated */ + out_of_mem = false; + return false; /* force plan b because plan a bombed */ + } + if ((ifd = open(filename, O_RDONLY)) < 0) + pfatal("can't open file %s", filename); + + if (i_size) { + i_womp = mmap(NULL, i_size, PROT_READ, MAP_PRIVATE, ifd, 0); + if (i_womp == MAP_FAILED) { + perror("mmap failed"); + i_womp = NULL; + close(ifd); + return false; + } + } else { + i_womp = NULL; + } + + close(ifd); + if (i_size) + madvise(i_womp, i_size, MADV_SEQUENTIAL); + + /* estimate the number of lines */ + lines_allocated = i_size / 25; + if (lines_allocated < 100) + lines_allocated = 100; + + if (!reallocate_lines(&lines_allocated)) + return false; + + /* now scan the buffer and build pointer array */ + iline = 1; + i_ptr[iline] = i_womp; + /* + * Testing for NUL here actively breaks files that innocently use NUL + * for other reasons. mmap(2) succeeded, just scan the whole buffer. + */ + for (s = i_womp, i = 0; i < i_size; s++, i++) { + if (*s == '\n') { + if (iline == lines_allocated) { + if (!reallocate_lines(&lines_allocated)) + return false; + } + /* these are NOT NUL terminated */ + i_ptr[++iline] = s + 1; + } + } + /* if the last line contains no EOL, append one */ + if (i_size > 0 && i_womp[i_size - 1] != '\n') { + last_line_missing_eol = true; + /* fix last line */ + sz = s - i_ptr[iline]; + p = malloc(sz + 1); + if (p == NULL) { + free(i_ptr); + i_ptr = NULL; + munmap(i_womp, i_size); + i_womp = NULL; + return false; + } + + memcpy(p, i_ptr[iline], sz); + p[sz] = '\n'; + i_ptr[iline] = p; + /* count the extra line and make it point to some valid mem */ + i_ptr[++iline] = empty_line; + } else + last_line_missing_eol = false; + + input_lines = iline - 1; + + /* now check for revision, if any */ + + if (revision != NULL) { + if (i_womp == NULL || !rev_in_string(i_womp)) { + if (force) { + if (verbose) + say("Warning: this file doesn't appear " + "to be the %s version--patching anyway.\n", + revision); + } else if (batch) { + fatal("this file doesn't appear to be the " + "%s version--aborting.\n", + revision); + } else { + ask("This file doesn't appear to be the " + "%s version--patch anyway? [n] ", + revision); + if (*buf != 'y') + fatal("aborted\n"); + } + } else if (verbose) + say("Good. This file appears to be the %s version.\n", + revision); + } + return true; /* plan a will work */ +} + +/* Keep (virtually) nothing in memory. */ + +static void +plan_b(const char *filename) +{ + FILE *ifp; + size_t i, j, len, maxlen; + char *lbuf = NULL, *p; + bool found_revision = (revision == NULL); + + using_plan_a = false; + if ((ifp = fopen(filename, "r")) == NULL) + pfatal("can't open file %s", filename); + unlink(TMPINNAME); + if ((tifd = open(TMPINNAME, O_EXCL | O_CREAT | O_WRONLY, 0666)) < 0) + pfatal("can't open file %s", TMPINNAME); + len = 0; + maxlen = 1; + while ((p = fgetln(ifp, &len)) != NULL) { + if (p[len - 1] == '\n') + p[len - 1] = '\0'; + else { + /* EOF without EOL, copy and add the NUL */ + if ((lbuf = malloc(len + 1)) == NULL) + fatal("out of memory\n"); + memcpy(lbuf, p, len); + lbuf[len] = '\0'; + p = lbuf; + + last_line_missing_eol = true; + len++; + } + if (revision != NULL && !found_revision && rev_in_string(p)) + found_revision = true; + if (len > maxlen) + maxlen = len; /* find longest line */ + } + free(lbuf); + if (ferror(ifp)) + pfatal("can't read file %s", filename); + + if (revision != NULL) { + if (!found_revision) { + if (force) { + if (verbose) + say("Warning: this file doesn't appear " + "to be the %s version--patching anyway.\n", + revision); + } else if (batch) { + fatal("this file doesn't appear to be the " + "%s version--aborting.\n", + revision); + } else { + ask("This file doesn't appear to be the %s " + "version--patch anyway? [n] ", + revision); + if (*buf != 'y') + fatal("aborted\n"); + } + } else if (verbose) + say("Good. This file appears to be the %s version.\n", + revision); + } + fseek(ifp, 0L, SEEK_SET); /* rewind file */ + tireclen = maxlen; + tibuflen = maxlen > BUFFERSIZE ? maxlen : BUFFERSIZE; + lines_per_buf = tibuflen / maxlen; + tibuf[0] = malloc(tibuflen + 1); + if (tibuf[0] == NULL) + fatal("out of memory\n"); + tibuf[1] = malloc(tibuflen + 1); + if (tibuf[1] == NULL) + fatal("out of memory\n"); + for (i = 1;; i++) { + p = tibuf[0] + maxlen * (i % lines_per_buf); + if (i % lines_per_buf == 0) /* new block */ + if (write(tifd, tibuf[0], tibuflen) != + (ssize_t) tibuflen) + pfatal("can't write temp file"); + if (fgets(p, maxlen + 1, ifp) == NULL) { + input_lines = i - 1; + if (i % lines_per_buf != 0) + if (write(tifd, tibuf[0], tibuflen) != + (ssize_t) tibuflen) + pfatal("can't write temp file"); + break; + } + j = strlen(p); + /* These are '\n' terminated strings, so no need to add a NUL */ + if (j == 0 || p[j - 1] != '\n') + p[j] = '\n'; + } + fclose(ifp); + close(tifd); + if ((tifd = open(TMPINNAME, O_RDONLY)) < 0) + pfatal("can't reopen file %s", TMPINNAME); +} + +/* + * Fetch a line from the input file, \n terminated, not necessarily \0. + */ +char * +ifetch(LINENUM line, int whichbuf) +{ + if (line < 1 || line > input_lines) { + if (warn_on_invalid_line) { + say("No such line %ld in input file, ignoring\n", line); + warn_on_invalid_line = false; + } + return NULL; + } + if (using_plan_a) + return i_ptr[line]; + else { + LINENUM offline = line % lines_per_buf; + LINENUM baseline = line - offline; + + if (tiline[0] == baseline) + whichbuf = 0; + else if (tiline[1] == baseline) + whichbuf = 1; + else { + tiline[whichbuf] = baseline; + + if (lseek(tifd, (off_t) (baseline / lines_per_buf * + tibuflen), SEEK_SET) < 0) + pfatal("cannot seek in the temporary input file"); + + if (read(tifd, tibuf[whichbuf], tibuflen) != + (ssize_t) tibuflen) + pfatal("error reading tmp file %s", TMPINNAME); + } + return tibuf[whichbuf] + (tireclen * offline); + } +} + +/* + * True if the string argument contains the revision number we want. + */ +static bool +rev_in_string(const char *string) +{ + const char *s; + size_t patlen; + + if (revision == NULL) + return true; + patlen = strlen(revision); + if (strnEQ(string, revision, patlen) && isspace((unsigned char)string[patlen])) + return true; + for (s = string; *s; s++) { + if (isspace((unsigned char)*s) && strnEQ(s + 1, revision, patlen) && + isspace((unsigned char)s[patlen + 1])) { + return true; + } + } + return false; +} diff --git a/src/patch/inp.h b/src/patch/inp.h new file mode 100644 index 0000000..bfdc88a --- /dev/null +++ b/src/patch/inp.h @@ -0,0 +1,31 @@ +/*- + * Copyright 1986, Larry Wall + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following condition is met: + * 1. Redistributions of source code must retain the above copyright notice, + * this condition and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + * + * patch - a program to apply diffs to original files + * + * -C option added in 1998, original code by Marc Espie, based on FreeBSD + * behaviour + * + * $OpenBSD: inp.h,v 1.8 2003/08/15 08:00:51 otto Exp $ + */ + +void re_input(void); +void scan_input(const char *); +char *ifetch(LINENUM, int); diff --git a/src/patch/mkpath.c b/src/patch/mkpath.c new file mode 100644 index 0000000..0fbd58a --- /dev/null +++ b/src/patch/mkpath.c @@ -0,0 +1,79 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1983, 1992, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + * + * $OpenBSD: mkpath.c,v 1.2 2005/06/20 07:14:06 otto Exp $ + */ + +#include +#include +#include +#include +#include + +int mkpath(char *); + +/* Code taken directly from mkdir(1). + + * mkpath -- create directories. + * path - path + */ +int +mkpath(char *path) +{ + struct stat sb; + char *slash; + int done = 0; + + slash = path; + + while (!done) { + slash += strspn(slash, "/"); + slash += strcspn(slash, "/"); + + done = (*slash == '\0'); + *slash = '\0'; + + if (stat(path, &sb)) { + if (errno != ENOENT || (mkdir(path, 0777) && + errno != EEXIST)) { + warn("%s", path); + return (-1); + } + } else if (!S_ISDIR(sb.st_mode)) { + warnx("%s: %s", path, strerror(ENOTDIR)); + return (-1); + } + + *slash = '/'; + } + + return (0); +} + diff --git a/src/patch/patch.1 b/src/patch/patch.1 new file mode 100644 index 0000000..cfcd8a9 --- /dev/null +++ b/src/patch/patch.1 @@ -0,0 +1,691 @@ +.\"- +.\" Copyright 1986, Larry Wall +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following condition +.\" is met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this condition and the following disclaimer. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. +.\" +.\" $OpenBSD: patch.1,v 1.27 2014/04/15 06:26:54 jmc Exp $ +.Dd November 3, 2019 +.Dt PATCH 1 +.Os +.Sh NAME +.Nm patch +.Nd apply a diff file to an original +.Sh SYNOPSIS +.Nm +.Bk -words +.Op Fl bCcEeflNnRstuv +.Op Fl B Ar backup-prefix +.Op Fl D Ar symbol +.Op Fl d Ar directory +.Op Fl F Ar max-fuzz +.Op Fl i Ar patchfile +.Op Fl o Ar out-file +.Op Fl p Ar strip-count +.Op Fl r Ar rej-name +.Op Fl V Cm t | nil | never | none +.Op Fl x Ar number +.Op Fl z Ar backup-ext +.Op Fl Fl posix +.Op Ar origfile Op Ar patchfile +.Ek +.Nm +.Pf \*(Lt Ar patchfile +.Sh DESCRIPTION +.Nm +will take a patch file containing any of the four forms of difference +listing produced by the +.Xr diff 1 +program and apply those differences to an original file, +producing a patched version. +If +.Ar patchfile +is omitted, or is a hyphen, the patch will be read from the standard input. +.Pp +.Nm +will attempt to determine the type of the diff listing, unless overruled by a +.Fl c , +.Fl e , +.Fl n , +or +.Fl u +option. +Context diffs (old-style, new-style, and unified) and +normal diffs are applied directly by the +.Nm +program itself, whereas ed diffs are simply fed to the +.Xr ed 1 +editor via a pipe. +.Pp +If the +.Ar patchfile +contains more than one patch, +.Nm +will try to apply each of them as if they came from separate patch files. +This means, among other things, that it is assumed that the name of the file +to patch must be determined for each diff listing, and that the garbage before +each diff listing will be examined for interesting things such as file names +and revision level (see the section on +.Sx Filename Determination +below). +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Xo +.Fl B Ar backup-prefix , +.Fl Fl prefix Ar backup-prefix +.Xc +Causes the next argument to be interpreted as a prefix to the backup file +name. +If this argument is specified, any argument to +.Fl z +will be ignored. +.It Fl b , Fl Fl backup +Save a backup copy of the file before it is modified. +By default the original file is saved with a backup extension of +.Qq .orig +unless the file already has a numbered backup, in which case a numbered +backup is made. +This is equivalent to specifying +.Qo Fl V Cm existing Qc . +This option is currently the default, unless +.Fl -posix +is specified. +.It Fl C , Fl Fl check , Fl Fl dry-run +Checks that the patch would apply cleanly, but does not modify anything. +.It Fl c , Fl Fl context +Forces +.Nm +to interpret the patch file as a context diff. +.It Xo +.Fl D Ar symbol , +.Fl Fl ifdef Ar symbol +.Xc +Causes +.Nm +to use the +.Qq #ifdef...#endif +construct to mark changes. +The argument following will be used as the differentiating symbol. +Note that, unlike the C compiler, there must be a space between the +.Fl D +and the argument. +.It Xo +.Fl d Ar directory , +.Fl Fl directory Ar directory +.Xc +Causes +.Nm +to interpret the next argument as a directory, +and change the working directory to it before doing anything else. +.It Fl E , Fl Fl remove-empty-files +Causes +.Nm +to remove output files that are empty after the patches have been applied. +This option is useful when applying patches that create or remove files. +.It Fl e , Fl Fl ed +Forces +.Nm +to interpret the patch file as an +.Xr ed 1 +script. +.It Xo +.Fl F Ar max-fuzz , +.Fl Fl fuzz Ar max-fuzz +.Xc +Sets the maximum fuzz factor. +This option only applies to context diffs, and causes +.Nm +to ignore up to that many lines in looking for places to install a hunk. +Note that a larger fuzz factor increases the odds of a faulty patch. +The default fuzz factor is 2, and it may not be set to more than +the number of lines of context in the context diff, ordinarily 3. +.It Fl f , Fl Fl force +Forces +.Nm +to assume that the user knows exactly what he or she is doing, and to not +ask any questions. +It assumes the following: +skip patches for which a file to patch cannot be found; +patch files even though they have the wrong version for the +.Qq Prereq : +line in the patch; +and assume that patches are not reversed even if they look like they are. +This option does not suppress commentary; use +.Fl s +for that. +.It Xo +.Fl i Ar patchfile , +.Fl Fl input Ar patchfile +.Xc +Causes the next argument to be interpreted as the input file name +(i.e., a patchfile). +This option may be specified multiple times. +.It Fl l , Fl Fl ignore-whitespace +Causes the pattern matching to be done loosely, in case the tabs and +spaces have been munged in your input file. +Any sequence of whitespace in the pattern line will match any sequence +in the input file. +Normal characters must still match exactly. +Each line of the context must still match a line in the input file. +.It Fl N , Fl Fl forward +Causes +.Nm +to ignore patches that it thinks are reversed or already applied. +See also +.Fl R . +.It Fl n , Fl Fl normal +Forces +.Nm +to interpret the patch file as a normal diff. +.It Xo +.Fl o Ar out-file , +.Fl Fl output Ar out-file +.Xc +Causes the next argument to be interpreted as the output file name. +.It Xo +.Fl p Ar strip-count , +.Fl Fl strip Ar strip-count +.Xc +Sets the pathname strip count, +which controls how pathnames found in the patch file are treated, +in case you keep your files in a different directory than the person who sent +out the patch. +The strip count specifies how many slashes are to be stripped from +the front of the pathname. +(Any intervening directory names also go away.) +For example, supposing the file name in the patch file was +.Pa /u/howard/src/blurfl/blurfl.c : +.Pp +Setting +.Fl p Ns Ar 0 +gives the entire pathname unmodified. +.Pp +.Fl p Ns Ar 1 +gives +.Pp +.D1 Pa u/howard/src/blurfl/blurfl.c +.Pp +without the leading slash. +.Pp +.Fl p Ns Ar 4 +gives +.Pp +.D1 Pa blurfl/blurfl.c +.Pp +Not specifying +.Fl p +at all just gives you +.Pa blurfl.c , +unless all of the directories in the leading path +.Pq Pa u/howard/src/blurfl +exist and that path is relative, +in which case you get the entire pathname unmodified. +Whatever you end up with is looked for either in the current directory, +or the directory specified by the +.Fl d +option. +.It Fl R , Fl Fl reverse +Tells +.Nm +that this patch was created with the old and new files swapped. +(Yes, I am afraid that does happen occasionally, human nature being what it +is.) +.Nm +will attempt to swap each hunk around before applying it. +Rejects will come out in the swapped format. +The +.Fl R +option will not work with ed diff scripts because there is too little +information to reconstruct the reverse operation. +.Pp +If the first hunk of a patch fails, +.Nm +will reverse the hunk to see if it can be applied that way. +If it can, you will be asked if you want to have the +.Fl R +option set. +If it cannot, the patch will continue to be applied normally. +(Note: this method cannot detect a reversed patch if it is a normal diff +and if the first command is an append (i.e., it should have been a delete) +since appends always succeed, due to the fact that a null context will match +anywhere. +Luckily, most patches add or change lines rather than delete them, so most +reversed normal diffs will begin with a delete, which will fail, triggering +the heuristic.) +.It Xo +.Fl r Ar rej-name , +.Fl Fl reject-file Ar rej-name +.Xc +Causes the next argument to be interpreted as the reject file name. +.It Xo +.Fl s , Fl Fl quiet , +.Fl Fl silent +.Xc +Makes +.Nm +do its work silently, unless an error occurs. +.It Fl t , Fl Fl batch +Similar to +.Fl f , +in that it suppresses questions, but makes some different assumptions: +skip patches for which a file to patch cannot be found (the same as +.Fl f ) ; +skip patches for which the file has the wrong version for the +.Qq Prereq : +line in the patch; +and assume that patches are reversed if they look like they are. +.It Fl u , Fl Fl unified +Forces +.Nm +to interpret the patch file as a unified context diff (a unidiff). +.It Xo +.Fl V Cm t | nil | never | none , +.Fl Fl version-control Cm t | nil | never | none +.Xc +Causes the next argument to be interpreted as a method for creating +backup file names. +The type of backups made can also be given in the +.Ev PATCH_VERSION_CONTROL +or +.Ev VERSION_CONTROL +environment variables, which are overridden by this option. +The +.Fl B +option overrides this option, causing the prefix to always be used for +making backup file names. +The values of the +.Ev PATCH_VERSION_CONTROL +and +.Ev VERSION_CONTROL +environment variables and the argument to the +.Fl V +option are like the GNU Emacs +.Dq version-control +variable; they also recognize synonyms that are more descriptive. +The valid values are (unique abbreviations are accepted): +.Bl -tag -width Ds -offset indent +.It Cm t , numbered +Always make numbered backups. +.It Cm nil , existing +Make numbered backups of files that already have them, +simple backups of the others. +.It Cm never , simple +Always make simple backups. +.It Cm none +Do not make backups. +.El +.It Fl v , Fl Fl version +Causes +.Nm +to print out its revision header and patch level. +.It Xo +.Fl x Ar number , +.Fl Fl debug Ar number +.Xc +Sets internal debugging flags, and is of interest only to +.Nm +patchers. +.It Xo +.Fl z Ar backup-ext , +.Fl Fl suffix Ar backup-ext +.Xc +Causes the next argument to be interpreted as the backup extension, to be +used in place of +.Qq .orig . +.It Fl Fl posix +Enables strict +.St -p1003.1-2008 +conformance, specifically: +.Bl -enum +.It +Backup files are not created unless the +.Fl b +option is specified. +.It +If unspecified, the file name used is the first of the old, new and +index files that exists. +.El +.El +.Ss Patch Application +.Nm +will try to skip any leading garbage, apply the diff, +and then skip any trailing garbage. +Thus you could feed an article or message containing a +diff listing to +.Nm , +and it should work. +If the entire diff is indented by a consistent amount, +this will be taken into account. +.Pp +With context diffs, and to a lesser extent with normal diffs, +.Nm +can detect when the line numbers mentioned in the patch are incorrect, +and will attempt to find the correct place to apply each hunk of the patch. +As a first guess, it takes the line number mentioned for the hunk, plus or +minus any offset used in applying the previous hunk. +If that is not the correct place, +.Nm +will scan both forwards and backwards for a set of lines matching the context +given in the hunk. +First +.Nm +looks for a place where all lines of the context match. +If no such place is found, and it is a context diff, and the maximum fuzz factor +is set to 1 or more, then another scan takes place ignoring the first and last +line of context. +If that fails, and the maximum fuzz factor is set to 2 or more, +the first two and last two lines of context are ignored, +and another scan is made. +.Pq The default maximum fuzz factor is 2 . +.Pp +If +.Nm +cannot find a place to install that hunk of the patch, it will put the hunk +out to a reject file, which normally is the name of the output file plus +.Qq .rej . +(Note that the rejected hunk will come out in context diff form whether the +input patch was a context diff or a normal diff. +If the input was a normal diff, many of the contexts will simply be null.) +The line numbers on the hunks in the reject file may be different than +in the patch file: they reflect the approximate location patch thinks the +failed hunks belong in the new file rather than the old one. +.Pp +As each hunk is completed, you will be told whether the hunk succeeded or +failed, and which line (in the new file) +.Nm +thought the hunk should go on. +If this is different from the line number specified in the diff, +you will be told the offset. +A single large offset MAY be an indication that a hunk was installed in the +wrong place. +You will also be told if a fuzz factor was used to make the match, in which +case you should also be slightly suspicious. +.Ss Filename Determination +If no original file is specified on the command line, +.Nm +will try to figure out from the leading garbage what the name of the file +to edit is. +When checking a prospective file name, pathname components are stripped +as specified by the +.Fl p +option and the file's existence and writability are checked relative +to the current working directory (or the directory specified by the +.Fl d +option). +.Pp +If the diff is a context or unified diff, +.Nm +is able to determine the old and new file names from the diff header. +For context diffs, the +.Dq old +file is specified in the line beginning with +.Qq *** +and the +.Dq new +file is specified in the line beginning with +.Qq --- . +For a unified diff, the +.Dq old +file is specified in the line beginning with +.Qq --- +and the +.Dq new +file is specified in the line beginning with +.Qq +++ . +If there is an +.Qq Index : +line in the leading garbage (regardless of the diff type), +.Nm +will use the file name from that line as the +.Dq index +file. +.Pp +.Nm +will choose the file name by performing the following steps, with the first +match used: +.Bl -enum +.It +If +.Nm +is operating in strict +.St -p1003.1-2008 +mode, the first of the +.Dq old , +.Dq new +and +.Dq index +file names that exist is used. +Otherwise, +.Nm +will examine either the +.Dq old +and +.Dq new +file names or, for a non-context diff, the +.Dq index +file name, and choose the file name with the fewest path components, +the shortest basename, and the shortest total file name length (in that order). +.It +If no suitable file was found to patch, the patch file is a context or +unified diff, and the old file was zero length, the new file name is +created and used. +.It +If the file name still cannot be determined, +.Nm +will prompt the user for the file name to use. +.El +.Pp +Additionally, if the leading garbage contains a +.Qq Prereq:\ \& +line, +.Nm +will take the first word from the prerequisites line (normally a version +number) and check the input file to see if that word can be found. +If not, +.Nm +will ask for confirmation before proceeding. +.Pp +The upshot of all this is that you should be able to say, while in a news +interface, the following: +.Pp +.Dl | patch -d /usr/src/local/blurfl +.Pp +and patch a file in the blurfl directory directly from the article containing +the patch. +.Ss Backup Files +By default, the patched version is put in place of the original, with +the original file backed up to the same name with the extension +.Qq .orig , +or as specified by the +.Fl B , +.Fl V , +or +.Fl z +options. +The extension used for making backup files may also be specified in the +.Ev SIMPLE_BACKUP_SUFFIX +environment variable, which is overridden by the options above. +.Pp +If the backup file is a symbolic or hard link to the original file, +.Nm +creates a new backup file name by changing the first lowercase letter +in the last component of the file's name into uppercase. +If there are no more lowercase letters in the name, +it removes the first character from the name. +It repeats this process until it comes up with a +backup file that does not already exist or is not linked to the original file. +.Pp +You may also specify where you want the output to go with the +.Fl o +option; if that file already exists, it is backed up first. +.Ss Notes For Patch Senders +There are several things you should bear in mind if you are going to +be sending out patches: +.Pp +First, you can save people a lot of grief by keeping a +.Pa patchlevel.h +file which is patched to increment the patch level as the first diff in the +patch file you send out. +If you put a +.Qq Prereq : +line in with the patch, it will not let them apply +patches out of order without some warning. +.Pp +Second, make sure you have specified the file names right, either in a +context diff header, or with an +.Qq Index : +line. +If you are patching something in a subdirectory, be sure to tell the patch +user to specify a +.Fl p +option as needed. +.Pp +Third, you can create a file by sending out a diff that compares a +null file to the file you want to create. +If the file you want to create already exists in the target directory when the +diff is applied, then +.Nm +will identify the patch as potentially reversed and offer to reverse the patch. +.Pp +Fourth, take care not to send out reversed patches, since it makes people wonder +whether they already applied the patch. +.Pp +Fifth, while you may be able to get away with putting 582 diff listings into +one file, it is probably wiser to group related patches into separate files in +case something goes haywire. +.Sh ENVIRONMENT +.Bl -tag -width "PATCH_VERSION_CONTROL" -compact +.It Ev POSIXLY_CORRECT +When set, +.Nm +behaves as if the +.Fl Fl posix +option has been specified. +.It Ev SIMPLE_BACKUP_SUFFIX +Extension to use for backup file names instead of +.Qq .orig . +.It Ev TMPDIR +Directory to put temporary files in; default is +.Pa /tmp . +.It Ev PATCH_VERSION_CONTROL +Selects when numbered backup files are made. +.It Ev VERSION_CONTROL +Same as +.Ev PATCH_VERSION_CONTROL . +.El +.Sh FILES +.Bl -tag -width "$TMPDIR/patch*" -compact +.It Pa $TMPDIR/patch* +.Nm +temporary files +.It Pa /dev/tty +used to read input when +.Nm +prompts the user +.El +.Sh EXIT STATUS +The +.Nm +utility exits with one of the following values: +.Pp +.Bl -tag -width Ds -offset indent -compact +.It 0 +Successful completion. +.It 1 +One or more lines were written to a reject file. +.It \*(Gt1 +An error occurred. +.El +.Pp +When applying a set of patches in a loop it behooves you to check this +exit status so you do not apply a later patch to a partially patched file. +.Sh DIAGNOSTICS +Too many to list here, but generally indicative that +.Nm +couldn't parse your patch file. +.Pp +The message +.Qq Hmm... +indicates that there is unprocessed text in the patch file and that +.Nm +is attempting to intuit whether there is a patch in that text and, if so, +what kind of patch it is. +.Sh SEE ALSO +.Xr diff 1 +.Sh STANDARDS +The +.Nm +utility is compliant with the +.St -p1003.1-2008 +specification, +except as detailed above for the +.Fl -posix +option. +.Pp +The flags +.Op Fl BCEFfstVvxz +and +.Op Fl -posix +are extensions to that specification. +.Sh AUTHORS +.An Larry Wall +with many other contributors. +.Sh CAVEATS +.Nm +cannot tell if the line numbers are off in an ed script, and can only detect +bad line numbers in a normal diff when it finds a +.Qq change +or a +.Qq delete +command. +A context diff using fuzz factor 3 may have the same problem. +Until a suitable interactive interface is added, you should probably do +a context diff in these cases to see if the changes made sense. +Of course, compiling without errors is a pretty good indication that the patch +worked, but not always. +.Pp +.Nm +usually produces the correct results, even when it has to do a lot of +guessing. +However, the results are guaranteed to be correct only when the patch is +applied to exactly the same version of the file that the patch was +generated from. +.Sh BUGS +Could be smarter about partial matches, excessively deviant offsets and +swapped code, but that would take an extra pass. +.Pp +Check patch mode +.Pq Fl C +will fail if you try to check several patches in succession that build on +each other. +The entire +.Nm +code would have to be restructured to keep temporary files around so that it +can handle this situation. +.Pp +If code has been duplicated (for instance with #ifdef OLDCODE ... #else ... +#endif), +.Nm +is incapable of patching both versions, and, if it works at all, will likely +patch the wrong one, and tell you that it succeeded to boot. +.Pp +If you apply a patch you have already applied, +.Nm +will think it is a reversed patch, and offer to un-apply the patch. +This could be construed as a feature. diff --git a/src/patch/patch.c b/src/patch/patch.c new file mode 100644 index 0000000..f5d3592 --- /dev/null +++ b/src/patch/patch.c @@ -0,0 +1,1230 @@ +/*- + * Copyright 1986, Larry Wall + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following condition is met: + * 1. Redistributions of source code must retain the above copyright notice, + * this condition and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + * + * patch - a program to apply diffs to original files + * + * -C option added in 1998, original code by Marc Espie, based on FreeBSD + * behaviour + * + * $OpenBSD: patch.c,v 1.54 2014/12/13 10:31:07 tobias Exp $ + * + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "util.h" +#include "pch.h" +#include "inp.h" +#include "backupfile.h" +#include "pathnames.h" + +mode_t filemode = 0644; + +char *buf; /* general purpose buffer */ +size_t buf_size; /* size of the general purpose buffer */ + +bool using_plan_a = true; /* try to keep everything in memory */ +bool out_of_mem = false; /* ran out of memory in plan a */ +bool nonempty_patchf_seen = false; /* seen nonempty patch file? */ + +#define MAXFILEC 2 + +char *filearg[MAXFILEC]; +bool ok_to_create_file = false; +char *outname = NULL; +char *origprae = NULL; +char *TMPOUTNAME; +char *TMPINNAME; +char *TMPREJNAME; +char *TMPPATNAME; +bool toutkeep = false; +bool trejkeep = false; +bool warn_on_invalid_line; +bool last_line_missing_eol; + +#ifdef DEBUGGING +int debug = 0; +#endif + +bool force = false; +bool batch = false; +bool verbose = true; +bool reverse = false; +bool noreverse = false; +bool skip_rest_of_patch = false; +int strippath = 957; +bool canonicalize = false; +bool check_only = false; +int diff_type = 0; +char *revision = NULL; /* prerequisite revision, if any */ +LINENUM input_lines = 0; /* how long is input file in lines */ +int posix = 0; /* strict POSIX mode? */ + +static void reinitialize_almost_everything(void); +static void get_some_switches(void); +static LINENUM locate_hunk(LINENUM); +static void abort_context_hunk(void); +static void rej_line(int, LINENUM); +static void abort_hunk(void); +static void apply_hunk(LINENUM); +static void init_output(const char *); +static void init_reject(const char *); +static void copy_till(LINENUM, bool); +static bool spew_output(void); +static void dump_line(LINENUM, bool); +static bool patch_match(LINENUM, LINENUM, LINENUM); +static bool similar(const char *, const char *, int); +static void usage(void); +static bool handle_creation(bool, bool *); + +/* true if -E was specified on command line. */ +static bool remove_empty_files = false; + +/* true if -R was specified on command line. */ +static bool reverse_flag_specified = false; + +static bool Vflag = false; + +/* buffer holding the name of the rejected patch file. */ +static char rejname[PATH_MAX]; + +/* how many input lines have been irretractibly output */ +static LINENUM last_frozen_line = 0; + +static int Argc; /* guess */ +static char **Argv; +static int Argc_last; /* for restarting plan_b */ +static char **Argv_last; + +static FILE *ofp = NULL; /* output file pointer */ +static FILE *rejfp = NULL; /* reject file pointer */ + +static int filec = 0; /* how many file arguments? */ +static LINENUM last_offset = 0; +static LINENUM maxfuzz = 2; + +/* patch using ifdef, ifndef, etc. */ +static bool do_defines = false; +/* #ifdef xyzzy */ +static char if_defined[128]; +/* #ifndef xyzzy */ +static char not_defined[128]; +/* #else */ +static const char else_defined[] = "#else\n"; +/* #endif xyzzy */ +static char end_defined[128]; + + +/* Apply a set of diffs as appropriate. */ + +int +patch_main(int argc, char *argv[]) +{ + struct stat statbuf; + int error = 0, hunk, failed, i, fd; + bool out_creating, out_existed, patch_seen, remove_file; + bool reverse_seen; + LINENUM where = 0, newwhere, fuzz, mymaxfuzz; + const char *tmpdir; + char *v; + + setvbuf(stdout, NULL, _IOLBF, 0); + setvbuf(stderr, NULL, _IOLBF, 0); + for (i = 0; i < MAXFILEC; i++) + filearg[i] = NULL; + + buf_size = INITLINELEN; + buf = malloc((unsigned)(buf_size)); + if (buf == NULL) + fatal("out of memory\n"); + + /* Cons up the names of the temporary files. */ + if ((tmpdir = getenv("TMPDIR")) == NULL || *tmpdir == '\0') + tmpdir = _PATH_TMP; + for (i = strlen(tmpdir) - 1; i > 0 && tmpdir[i] == '/'; i--) + ; + i++; + if (asprintf(&TMPOUTNAME, "%.*s/patchoXXXXXXXXXX", i, tmpdir) == -1) + fatal("cannot allocate memory"); + if ((fd = mkstemp(TMPOUTNAME)) < 0) + pfatal("can't create %s", TMPOUTNAME); + close(fd); + + if (asprintf(&TMPINNAME, "%.*s/patchiXXXXXXXXXX", i, tmpdir) == -1) + fatal("cannot allocate memory"); + if ((fd = mkstemp(TMPINNAME)) < 0) + pfatal("can't create %s", TMPINNAME); + close(fd); + + if (asprintf(&TMPREJNAME, "%.*s/patchrXXXXXXXXXX", i, tmpdir) == -1) + fatal("cannot allocate memory"); + if ((fd = mkstemp(TMPREJNAME)) < 0) + pfatal("can't create %s", TMPREJNAME); + close(fd); + + if (asprintf(&TMPPATNAME, "%.*s/patchpXXXXXXXXXX", i, tmpdir) == -1) + fatal("cannot allocate memory"); + if ((fd = mkstemp(TMPPATNAME)) < 0) + pfatal("can't create %s", TMPPATNAME); + close(fd); + + v = getenv("SIMPLE_BACKUP_SUFFIX"); + if (v) + simple_backup_suffix = v; + else + simple_backup_suffix = ORIGEXT; + + /* parse switches */ + Argc = argc; + Argv = argv; + get_some_switches(); + + if (!Vflag) { + if ((v = getenv("PATCH_VERSION_CONTROL")) == NULL) + v = getenv("VERSION_CONTROL"); + if (v != NULL || !posix) + backup_type = get_version(v); /* OK to pass NULL. */ + } + + /* make sure we clean up /tmp in case of disaster */ + set_signals(0); + + patch_seen = false; + for (open_patch_file(filearg[1]); there_is_another_patch(); + reinitialize_almost_everything()) { + /* for each patch in patch file */ + + if (source_file != NULL && (diff_type == CONTEXT_DIFF || + diff_type == NEW_CONTEXT_DIFF || + diff_type == UNI_DIFF)) + out_creating = strcmp(source_file, _PATH_DEVNULL) == 0; + else + out_creating = false; + patch_seen = true; + + warn_on_invalid_line = true; + + if (outname == NULL) + outname = xstrdup(filearg[0]); + + /* + * At this point, we know if we're supposed to be creating the + * file and we know if we should be trying to handle a conflict + * between the patch and the file already existing. We defer + * handling it until hunk processing because we want to swap + * the hunk if they opt to reverse it, but we want to make sure + * we *can* swap the hunk without running into memory issues + * before we offer it. We also want to be verbose if flags or + * user decision cause us to skip -- this is explained a little + * more later. + */ + out_existed = stat(outname, &statbuf) == 0; + + /* for ed script just up and do it and exit */ + if (diff_type == ED_DIFF) { + do_ed_script(); + continue; + } + /* initialize the patched file */ + if (!skip_rest_of_patch) + init_output(TMPOUTNAME); + + /* initialize reject file */ + init_reject(TMPREJNAME); + + /* find out where all the lines are */ + if (!skip_rest_of_patch) + scan_input(filearg[0]); + + /* + * from here on, open no standard i/o files, because + * malloc might misfire and we can't catch it easily + */ + + /* apply each hunk of patch */ + hunk = 0; + failed = 0; + reverse_seen = false; + out_of_mem = false; + remove_file = false; + while (another_hunk()) { + assert(!out_creating || hunk == 0); + hunk++; + fuzz = 0; + + /* + * There are only three cases in handle_creation() that + * results in us skipping hunk location, in order: + * + * 1.) Potentially reversed but -f/--force'd, + * 2.) Potentially reversed but -N/--forward'd + * 3.) Reversed and the user's opted to not apply it. + * + * In all three cases, we still want to inform the user + * that we're ignoring it in the standard way, which is + * also tied to this hunk processing loop. + */ + if (out_creating) + reverse_seen = handle_creation(out_existed, + &remove_file); + + mymaxfuzz = pch_context(); + if (maxfuzz < mymaxfuzz) + mymaxfuzz = maxfuzz; + if (!skip_rest_of_patch) { + do { + where = locate_hunk(fuzz); + if (hunk == 1 && where == 0 && !force && !reverse_seen) { + /* dwim for reversed patch? */ + if (!pch_swap()) { + if (fuzz == 0) + say("Not enough memory to try swapped hunk! Assuming unswapped.\n"); + continue; + } + reverse = !reverse; + /* try again */ + where = locate_hunk(fuzz); + if (where == 0) { + /* didn't find it swapped */ + if (!pch_swap()) + /* put it back to normal */ + fatal("lost hunk on alloc error!\n"); + reverse = !reverse; + } else if (noreverse) { + if (!pch_swap()) + /* put it back to normal */ + fatal("lost hunk on alloc error!\n"); + reverse = !reverse; + say("Ignoring previously applied (or reversed) patch.\n"); + skip_rest_of_patch = true; + } else if (batch) { + if (verbose) + say("%seversed (or previously applied) patch detected! %s -R.", + reverse ? "R" : "Unr", + reverse ? "Assuming" : "Ignoring"); + } else { + ask("%seversed (or previously applied) patch detected! %s -R? [y] ", + reverse ? "R" : "Unr", + reverse ? "Assume" : "Ignore"); + if (*buf == 'n') { + ask("Apply anyway? [n] "); + if (*buf != 'y') + skip_rest_of_patch = true; + else + reverse_seen = true; + where = 0; + reverse = !reverse; + if (!pch_swap()) + /* put it back to normal */ + fatal("lost hunk on alloc error!\n"); + } + } + } + } while (!skip_rest_of_patch && where == 0 && + ++fuzz <= mymaxfuzz); + + if (skip_rest_of_patch) { /* just got decided */ + if (ferror(ofp) || fclose(ofp)) { + say("Error writing %s\n", + TMPOUTNAME); + error = 1; + } + ofp = NULL; + } + } + newwhere = pch_newfirst() + last_offset; + if (skip_rest_of_patch) { + abort_hunk(); + failed++; + if (verbose) + say("Hunk #%d ignored at %ld.\n", + hunk, newwhere); + } else if (where == 0) { + abort_hunk(); + failed++; + if (verbose) + say("Hunk #%d failed at %ld.\n", + hunk, newwhere); + } else { + apply_hunk(where); + if (verbose) { + say("Hunk #%d succeeded at %ld", + hunk, newwhere); + if (fuzz != 0) + say(" with fuzz %ld", fuzz); + if (last_offset) + say(" (offset %ld line%s)", + last_offset, + last_offset == 1L ? "" : "s"); + say(".\n"); + } + } + } + + if (out_of_mem && using_plan_a) { + Argc = Argc_last; + Argv = Argv_last; + say("\n\nRan out of memory using Plan A--trying again...\n\n"); + if (ofp) + fclose(ofp); + ofp = NULL; + if (rejfp) + fclose(rejfp); + rejfp = NULL; + continue; + } + if (hunk == 0) + fatal("Internal error: hunk should not be 0\n"); + + /* finish spewing out the new file */ + if (!skip_rest_of_patch && !spew_output()) { + say("Can't write %s\n", TMPOUTNAME); + error = 1; + } + + /* and put the output where desired */ + ignore_signals(); + if (!skip_rest_of_patch) { + char *realout = outname; + + if (!check_only) { + if (move_file(TMPOUTNAME, outname) < 0) { + toutkeep = true; + realout = TMPOUTNAME; + chmod(TMPOUTNAME, filemode); + } else + chmod(outname, filemode); + + /* + * remove_file is a per-patch flag indicating + * whether it's OK to remove the empty file. + * This is specifically set when we're reversing + * the creation of a file and it ends up empty. + * This is an exception to the global policy + * (remove_empty_files) because the user would + * likely not expect the reverse of file + * creation to leave an empty file laying + * around. + */ + if ((remove_empty_files || remove_file) && + stat(realout, &statbuf) == 0 && + statbuf.st_size == 0) { + if (verbose) + say("Removing %s (empty after patching).\n", + realout); + unlink(realout); + } + } + } + if (ferror(rejfp) || fclose(rejfp)) { + say("Error writing %s\n", rejname); + error = 1; + } + rejfp = NULL; + if (failed) { + error = 1; + if (*rejname == '\0') { + if (strlcpy(rejname, outname, + sizeof(rejname)) >= sizeof(rejname)) + fatal("filename %s is too long\n", outname); + if (strlcat(rejname, REJEXT, + sizeof(rejname)) >= sizeof(rejname)) + fatal("filename %s is too long\n", outname); + } + if (!check_only) + say("%d out of %d hunks %s--saving rejects to %s\n", + failed, hunk, skip_rest_of_patch ? "ignored" : "failed", rejname); + else if (filearg[0] != NULL) + say("%d out of %d hunks %s while patching %s\n", + failed, hunk, skip_rest_of_patch ? "ignored" : "failed", filearg[0]); + else + /* File prompt ignored, just note # hunks. */ + say("%d out of %d hunks %s\n", + failed, hunk, skip_rest_of_patch ? "ignored" : "failed"); + if (!check_only && move_file(TMPREJNAME, rejname) < 0) + trejkeep = true; + } + set_signals(1); + } + + if (!patch_seen && nonempty_patchf_seen) + error = 2; + + my_exit(error); + /* NOTREACHED */ +} + +/* Prepare to find the next patch to do in the patch file. */ + +static void +reinitialize_almost_everything(void) +{ + re_patch(); + re_input(); + + input_lines = 0; + last_frozen_line = 0; + + filec = 0; + if (!out_of_mem) { + free(filearg[0]); + filearg[0] = NULL; + } + + free(source_file); + source_file = NULL; + + free(outname); + outname = NULL; + + last_offset = 0; + diff_type = 0; + + free(revision); + revision = NULL; + + reverse = reverse_flag_specified; + skip_rest_of_patch = false; + + get_some_switches(); +} + +/* Process switches and filenames. */ + +static void +get_some_switches(void) +{ + const char *options = "b::B:cCd:D:eEfF:i:lnNo:p:r:RstuvV:x:z:"; + static struct option longopts[] = { + {"backup", no_argument, 0, 'b'}, + {"batch", no_argument, 0, 't'}, + {"check", no_argument, 0, 'C'}, + {"context", no_argument, 0, 'c'}, + {"debug", required_argument, 0, 'x'}, + {"directory", required_argument, 0, 'd'}, + {"dry-run", no_argument, 0, 'C'}, + {"ed", no_argument, 0, 'e'}, + {"force", no_argument, 0, 'f'}, + {"forward", no_argument, 0, 'N'}, + {"fuzz", required_argument, 0, 'F'}, + {"ifdef", required_argument, 0, 'D'}, + {"input", required_argument, 0, 'i'}, + {"ignore-whitespace", no_argument, 0, 'l'}, + {"normal", no_argument, 0, 'n'}, + {"output", required_argument, 0, 'o'}, + {"prefix", required_argument, 0, 'B'}, + {"quiet", no_argument, 0, 's'}, + {"reject-file", required_argument, 0, 'r'}, + {"remove-empty-files", no_argument, 0, 'E'}, + {"reverse", no_argument, 0, 'R'}, + {"silent", no_argument, 0, 's'}, + {"strip", required_argument, 0, 'p'}, + {"suffix", required_argument, 0, 'z'}, + {"unified", no_argument, 0, 'u'}, + {"version", no_argument, 0, 'v'}, + {"version-control", required_argument, 0, 'V'}, + {"posix", no_argument, &posix, 1}, + {NULL, 0, 0, 0} + }; + int ch; + + rejname[0] = '\0'; + Argc_last = Argc; + Argv_last = Argv; + if (!Argc) + return; +#ifdef __OpenBSD__ + optreset = optind = 1; +#else + optind = 0; /* glibc: setting optind to 0 fully resets getopt */ +#endif + while ((ch = getopt_long(Argc, Argv, options, longopts, NULL)) != -1) { + switch (ch) { + case 'b': + if (backup_type == none) + backup_type = numbered_existing; + if (optarg == NULL) + break; + if (verbose) + say("Warning, the ``-b suffix'' option has been" + " obsoleted by the -z option.\n"); + /* FALLTHROUGH */ + case 'z': + /* must directly follow 'b' case for backwards compat */ + simple_backup_suffix = xstrdup(optarg); + break; + case 'B': + origprae = xstrdup(optarg); + break; + case 'c': + diff_type = CONTEXT_DIFF; + break; + case 'C': + check_only = true; + break; + case 'd': + if (chdir(optarg) < 0) + pfatal("can't cd to %s", optarg); + break; + case 'D': + do_defines = true; + if (!isalpha((unsigned char)*optarg) && *optarg != '_') + fatal("argument to -D is not an identifier\n"); + snprintf(if_defined, sizeof if_defined, + "#ifdef %s\n", optarg); + snprintf(not_defined, sizeof not_defined, + "#ifndef %s\n", optarg); + snprintf(end_defined, sizeof end_defined, + "#endif /* %s */\n", optarg); + break; + case 'e': + diff_type = ED_DIFF; + break; + case 'E': + remove_empty_files = true; + break; + case 'f': + force = true; + break; + case 'F': + maxfuzz = atoi(optarg); + break; + case 'i': + if (++filec == MAXFILEC) + fatal("too many file arguments\n"); + filearg[filec] = xstrdup(optarg); + break; + case 'l': + canonicalize = true; + break; + case 'n': + diff_type = NORMAL_DIFF; + break; + case 'N': + noreverse = true; + break; + case 'o': + outname = xstrdup(optarg); + break; + case 'p': + strippath = atoi(optarg); + break; + case 'r': + if (strlcpy(rejname, optarg, + sizeof(rejname)) >= sizeof(rejname)) + fatal("argument for -r is too long\n"); + break; + case 'R': + reverse = true; + reverse_flag_specified = true; + break; + case 's': + verbose = false; + break; + case 't': + batch = true; + break; + case 'u': + diff_type = UNI_DIFF; + break; + case 'v': + version(); + break; + case 'V': + backup_type = get_version(optarg); + Vflag = true; + break; +#ifdef DEBUGGING + case 'x': + debug = atoi(optarg); + break; +#endif + default: + if (ch != '\0') + usage(); + break; + } + } + Argc -= optind; + Argv += optind; + + if (Argc > 0) { + filearg[0] = xstrdup(*Argv++); + Argc--; + while (Argc > 0) { + if (++filec == MAXFILEC) + fatal("too many file arguments\n"); + filearg[filec] = xstrdup(*Argv++); + Argc--; + } + } + + if (getenv("POSIXLY_CORRECT") != NULL) + posix = 1; +} + +static void +usage(void) +{ + fprintf(stderr, +"usage: patch [-bCcEeflNnRstuv] [-B backup-prefix] [-D symbol] [-d directory]\n" +" [-F max-fuzz] [-i patchfile] [-o out-file] [-p strip-count]\n" +" [-r rej-name] [-V t | nil | never | none] [-x number]\n" +" [-z backup-ext] [--posix] [origfile [patchfile]]\n" +" patch = first_guess) /* do not try lines < 0 */ + max_neg_offset = first_guess - 1; + if (first_guess <= input_lines && patch_match(first_guess, 0, fuzz)) + return first_guess; + for (offset = 1; ; offset++) { + bool check_after = (offset <= max_pos_offset); + bool check_before = (offset <= max_neg_offset); + + if (check_after && patch_match(first_guess, offset, fuzz)) { +#ifdef DEBUGGING + if (debug & 1) + say("Offset changing from %ld to %ld\n", + last_offset, offset); +#endif + last_offset = offset; + return first_guess + offset; + } else if (check_before && patch_match(first_guess, -offset, fuzz)) { +#ifdef DEBUGGING + if (debug & 1) + say("Offset changing from %ld to %ld\n", + last_offset, -offset); +#endif + last_offset = -offset; + return first_guess - offset; + } else if (!check_before && !check_after) + return 0; + } +} + +/* We did not find the pattern, dump out the hunk so they can handle it. */ + +static void +abort_context_hunk(void) +{ + LINENUM i; + const LINENUM pat_end = pch_end(); + /* + * add in last_offset to guess the same as the previous successful + * hunk + */ + const LINENUM oldfirst = pch_first() + last_offset; + const LINENUM newfirst = pch_newfirst() + last_offset; + const LINENUM oldlast = oldfirst + pch_ptrn_lines() - 1; + const LINENUM newlast = newfirst + pch_repl_lines() - 1; + const char *stars = (diff_type >= NEW_CONTEXT_DIFF ? " ****" : ""); + const char *minuses = (diff_type >= NEW_CONTEXT_DIFF ? " ----" : " -----"); + + fprintf(rejfp, "***************\n"); + for (i = 0; i <= pat_end; i++) { + switch (pch_char(i)) { + case '*': + if (oldlast < oldfirst) + fprintf(rejfp, "*** 0%s\n", stars); + else if (oldlast == oldfirst) + fprintf(rejfp, "*** %ld%s\n", oldfirst, stars); + else + fprintf(rejfp, "*** %ld,%ld%s\n", oldfirst, + oldlast, stars); + break; + case '=': + if (newlast < newfirst) + fprintf(rejfp, "--- 0%s\n", minuses); + else if (newlast == newfirst) + fprintf(rejfp, "--- %ld%s\n", newfirst, minuses); + else + fprintf(rejfp, "--- %ld,%ld%s\n", newfirst, + newlast, minuses); + break; + case '\n': + fprintf(rejfp, "%s", pfetch(i)); + break; + case ' ': + case '-': + case '+': + case '!': + fprintf(rejfp, "%c %s", pch_char(i), pfetch(i)); + break; + default: + fatal("fatal internal error in abort_context_hunk\n"); + } + } +} + +static void +rej_line(int ch, LINENUM i) +{ + size_t len; + const char *line = pfetch(i); + + len = strlen(line); + + fprintf(rejfp, "%c%s", ch, line); + if (len == 0 || line[len - 1] != '\n') { + if (len >= USHRT_MAX) + fprintf(rejfp, "\n\\ Line too long\n"); + else + fprintf(rejfp, "\n\\ No newline at end of line\n"); + } +} + +static void +abort_hunk(void) +{ + LINENUM i, j, split; + int ch1, ch2; + const LINENUM pat_end = pch_end(); + const LINENUM oldfirst = pch_first() + last_offset; + const LINENUM newfirst = pch_newfirst() + last_offset; + + if (diff_type != UNI_DIFF) { + abort_context_hunk(); + return; + } + split = -1; + for (i = 0; i <= pat_end; i++) { + if (pch_char(i) == '=') { + split = i; + break; + } + } + if (split == -1) { + fprintf(rejfp, "malformed hunk: no split found\n"); + return; + } + i = 0; + j = split + 1; + fprintf(rejfp, "@@ -%ld,%ld +%ld,%ld @@\n", + pch_ptrn_lines() ? oldfirst : 0, + pch_ptrn_lines(), newfirst, pch_repl_lines()); + while (i < split || j <= pat_end) { + ch1 = i < split ? pch_char(i) : -1; + ch2 = j <= pat_end ? pch_char(j) : -1; + if (ch1 == '-') { + rej_line('-', i); + i++; + } else if (ch1 == ' ' && ch2 == ' ') { + rej_line(' ', i); + i++; + j++; + } else if (ch1 == '!' && ch2 == '!') { + while (i < split && ch1 == '!') { + rej_line('-', i); + i++; + ch1 = i < split ? pch_char(i) : -1; + } + while (j <= pat_end && ch2 == '!') { + rej_line('+', j); + j++; + ch2 = j <= pat_end ? pch_char(j) : -1; + } + } else if (ch1 == '*') { + i++; + } else if (ch2 == '+' || ch2 == ' ') { + rej_line(ch2, j); + j++; + } else { + fprintf(rejfp, "internal error on (%ld %ld %ld)\n", + i, split, j); + rej_line(ch1, i); + rej_line(ch2, j); + return; + } + } +} + +/* We found where to apply it (we hope), so do it. */ + +static void +apply_hunk(LINENUM where) +{ + LINENUM old = 1; + const LINENUM lastline = pch_ptrn_lines(); + LINENUM new = lastline + 1; +#define OUTSIDE 0 +#define IN_IFNDEF 1 +#define IN_IFDEF 2 +#define IN_ELSE 3 + int def_state = OUTSIDE; + const LINENUM pat_end = pch_end(); + + where--; + while (pch_char(new) == '=' || pch_char(new) == '\n') + new++; + + while (old <= lastline) { + if (pch_char(old) == '-') { + copy_till(where + old - 1, false); + if (do_defines) { + if (def_state == OUTSIDE) { + fputs(not_defined, ofp); + def_state = IN_IFNDEF; + } else if (def_state == IN_IFDEF) { + fputs(else_defined, ofp); + def_state = IN_ELSE; + } + fputs(pfetch(old), ofp); + } + last_frozen_line++; + old++; + } else if (new > pat_end) { + break; + } else if (pch_char(new) == '+') { + copy_till(where + old - 1, false); + if (do_defines) { + if (def_state == IN_IFNDEF) { + fputs(else_defined, ofp); + def_state = IN_ELSE; + } else if (def_state == OUTSIDE) { + fputs(if_defined, ofp); + def_state = IN_IFDEF; + } + } + fputs(pfetch(new), ofp); + new++; + } else if (pch_char(new) != pch_char(old)) { + say("Out-of-sync patch, lines %ld,%ld--mangled text or line numbers, maybe?\n", + pch_hunk_beg() + old, + pch_hunk_beg() + new); +#ifdef DEBUGGING + say("oldchar = '%c', newchar = '%c'\n", + pch_char(old), pch_char(new)); +#endif + my_exit(2); + } else if (pch_char(new) == '!') { + copy_till(where + old - 1, false); + if (do_defines) { + fputs(not_defined, ofp); + def_state = IN_IFNDEF; + } + while (pch_char(old) == '!') { + if (do_defines) { + fputs(pfetch(old), ofp); + } + last_frozen_line++; + old++; + } + if (do_defines) { + fputs(else_defined, ofp); + def_state = IN_ELSE; + } + while (pch_char(new) == '!') { + fputs(pfetch(new), ofp); + new++; + } + } else { + if (pch_char(new) != ' ') + fatal("Internal error: expected ' '\n"); + old++; + new++; + if (do_defines && def_state != OUTSIDE) { + fputs(end_defined, ofp); + def_state = OUTSIDE; + } + } + } + if (new <= pat_end && pch_char(new) == '+') { + copy_till(where + old - 1, false); + if (do_defines) { + if (def_state == OUTSIDE) { + fputs(if_defined, ofp); + def_state = IN_IFDEF; + } else if (def_state == IN_IFNDEF) { + fputs(else_defined, ofp); + def_state = IN_ELSE; + } + } + while (new <= pat_end && pch_char(new) == '+') { + fputs(pfetch(new), ofp); + new++; + } + } + if (do_defines && def_state != OUTSIDE) { + fputs(end_defined, ofp); + } +} + +/* + * Open the new file. + */ +static void +init_output(const char *name) +{ + ofp = fopen(name, "w"); + if (ofp == NULL) + pfatal("can't create %s", name); +} + +/* + * Open a file to put hunks we can't locate. + */ +static void +init_reject(const char *name) +{ + rejfp = fopen(name, "w"); + if (rejfp == NULL) + pfatal("can't create %s", name); +} + +/* + * Copy input file to output, up to wherever hunk is to be applied. + * If endoffile is true, treat the last line specially since it may + * lack a newline. + */ +static void +copy_till(LINENUM lastline, bool endoffile) +{ + if (last_frozen_line > lastline) + fatal("misordered hunks! output would be garbled\n"); + while (last_frozen_line < lastline) { + if (++last_frozen_line == lastline && endoffile) + dump_line(last_frozen_line, !last_line_missing_eol); + else + dump_line(last_frozen_line, true); + } +} + +/* + * Finish copying the input file to the output file. + */ +static bool +spew_output(void) +{ + int rv; + +#ifdef DEBUGGING + if (debug & 256) + say("il=%ld lfl=%ld\n", input_lines, last_frozen_line); +#endif + if (input_lines) + copy_till(input_lines, true); /* dump remainder of file */ + rv = ferror(ofp) == 0 && fclose(ofp) == 0; + ofp = NULL; + return rv; +} + +/* + * Copy one line from input to output. + */ +static void +dump_line(LINENUM line, bool write_newline) +{ + char *s; + + s = ifetch(line, 0); + if (s == NULL) + return; + /* Note: string is not NUL terminated. */ + for (; *s != '\n'; s++) + putc(*s, ofp); + if (write_newline) + putc('\n', ofp); +} + +/* + * Does the patch pattern match at line base+offset? + */ +static bool +patch_match(LINENUM base, LINENUM offset, LINENUM fuzz) +{ + LINENUM pline = 1 + fuzz; + LINENUM iline; + LINENUM pat_lines = pch_ptrn_lines() - fuzz; + const char *ilineptr; + const char *plineptr; + size_t plinelen; + + /* Patch does not match if we don't have any more context to use */ + if (pline > pat_lines) + return false; + for (iline = base + offset + fuzz; pline <= pat_lines; pline++, iline++) { + ilineptr = ifetch(iline, offset >= 0); + if (ilineptr == NULL) + return false; + plineptr = pfetch(pline); + plinelen = pch_line_len(pline); + if (canonicalize) { + if (!similar(ilineptr, plineptr, plinelen)) + return false; + } else if (strnNE(ilineptr, plineptr, plinelen)) + return false; + if (iline == input_lines) { + /* + * We are looking at the last line of the file. + * If the file has no eol, the patch line should + * not have one either and vice-versa. Note that + * plinelen > 0. + */ + if (last_line_missing_eol) { + if (plineptr[plinelen - 1] == '\n') + return false; + } else { + if (plineptr[plinelen - 1] != '\n') + return false; + } + } + } + return true; +} + +/* + * Do two lines match with canonicalized white space? + */ +static bool +similar(const char *a, const char *b, int len) +{ + while (len) { + if (isspace((unsigned char)*b)) { /* whitespace (or \n) to match? */ + if (!isspace((unsigned char)*a)) /* no corresponding whitespace? */ + return false; + while (len && isspace((unsigned char)*b) && *b != '\n') + b++, len--; /* skip pattern whitespace */ + while (isspace((unsigned char)*a) && *a != '\n') + a++; /* skip target whitespace */ + if (*a == '\n' || *b == '\n') + return (*a == *b); /* should end in sync */ + } else if (*a++ != *b++) /* match non-whitespace chars */ + return false; + else + len--; /* probably not necessary */ + } + return true; /* actually, this is not reached */ + /* since there is always a \n */ +} + +static bool +handle_creation(bool out_existed, bool *remove) +{ + bool reverse_seen; + + reverse_seen = false; + if (reverse && out_existed) { + /* + * If the patch creates the file and we're reversing the patch, + * then we need to indicate to the patch processor that it's OK + * to remove this file. + */ + *remove = true; + } else if (!reverse && out_existed) { + /* + * Otherwise, we need to blow the horn because the patch appears + * to be reversed/already applied. For non-batch jobs, we'll + * prompt to figure out what we should be trying to do to raise + * awareness of the issue. batch (-t) processing suppresses the + * questions and just assumes that we're reversed if it looks + * like we are, which is always the case if we've reached this + * branch. + */ + if (force) { + skip_rest_of_patch = true; + return (false); + } + if (noreverse) { + /* If -N is supplied, however, we bail out/ignore. */ + say("Ignoring previously applied (or reversed) patch.\n"); + skip_rest_of_patch = true; + return (false); + } + + /* Unreversed... suspicious if the file existed. */ + if (!pch_swap()) + fatal("lost hunk on alloc error!\n"); + + reverse = !reverse; + + if (batch) { + if (verbose) + say("Patch creates file that already exists, %s %seversed", + reverse ? "Assuming" : "Ignoring", + reverse ? "R" : "Unr"); + } else { + ask("Patch creates file that already exists! %s -R? [y] ", + reverse ? "Assume" : "Ignore"); + + if (*buf == 'n') { + ask("Apply anyway? [n]"); + if (*buf != 'y') + /* Don't apply; error out. */ + skip_rest_of_patch = true; + else + /* Attempt to apply. */ + reverse_seen = true; + reverse = !reverse; + if (!pch_swap()) + fatal("lost hunk on alloc error!\n"); + } else { + /* + * They've opted to assume -R; effectively the + * same as the first branch in this function, + * but the decision is here rather than in a + * prior patch/hunk as in that branch. + */ + *remove = true; + } + } + } + + /* + * The return value indicates if we offered a chance to reverse but the + * user declined. This keeps the main patch processor in the loop since + * we've taken this out of the normal flow of hunk processing to + * simplify logic a little bit. + */ + return (reverse_seen); +} diff --git a/src/patch/pathnames.h b/src/patch/pathnames.h new file mode 100644 index 0000000..4d4672f --- /dev/null +++ b/src/patch/pathnames.h @@ -0,0 +1,11 @@ +/*- + * Placed in the public domain by Todd C. Miller + * on July 29, 2003. + * + * $OpenBSD: pathnames.h,v 1.1 2003/07/29 20:10:17 millert Exp $ + */ + + +#include + +#define _PATH_RED "/bin/red" diff --git a/src/patch/pch.c b/src/patch/pch.c new file mode 100644 index 0000000..fb53ff8 --- /dev/null +++ b/src/patch/pch.c @@ -0,0 +1,1659 @@ +/*- + * Copyright 1986, Larry Wall + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following condition is met: + * 1. Redistributions of source code must retain the above copyright notice, + * this condition and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + * + * patch - a program to apply diffs to original files + * + * -C option added in 1998, original code by Marc Espie, based on FreeBSD + * behaviour + * + * $OpenBSD: pch.c,v 1.43 2014/11/18 17:03:35 tobias Exp $ + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "util.h" +#include "pch.h" +#include "pathnames.h" + +/* Patch (diff listing) abstract type. */ + +static off_t p_filesize; /* size of the patch file */ +static LINENUM p_first; /* 1st line number */ +static LINENUM p_newfirst; /* 1st line number of replacement */ +static LINENUM p_ptrn_lines; /* # lines in pattern */ +static LINENUM p_repl_lines; /* # lines in replacement text */ +static LINENUM p_end = -1; /* last line in hunk */ +static LINENUM p_max; /* max allowed value of p_end */ +static LINENUM p_context = 3; /* # of context lines */ +static LINENUM p_input_line = 0; /* current line # from patch file */ +static char **p_line = NULL;/* the text of the hunk */ +static size_t *p_len = NULL; /* length of each line */ +static char *p_char = NULL; /* +, -, and ! */ +static int hunkmax = INITHUNKMAX; /* size of above arrays to begin with */ +static int p_indent; /* indent to patch */ +static off_t p_base; /* where to intuit this time */ +static LINENUM p_bline; /* line # of p_base */ +static off_t p_start; /* where intuit found a patch */ +static LINENUM p_sline; /* and the line number for it */ +static LINENUM p_hunk_beg; /* line number of current hunk */ +static LINENUM p_efake = -1; /* end of faked up lines--don't free */ +static LINENUM p_bfake = -1; /* beg of faked up lines */ +static FILE *pfp = NULL; /* patch file pointer */ +static char *bestguess = NULL; /* guess at correct filename */ + +char *source_file; + +static void grow_hunkmax(void); +static int intuit_diff_type(void); +static void next_intuit_at(off_t, LINENUM); +static void skip_to(off_t, LINENUM); +static size_t pgets(bool _do_indent); +static char *best_name(const struct file_name *, bool); +static char *posix_name(const struct file_name *, bool); +static size_t num_components(const char *); +static LINENUM strtolinenum(char *, char **); + +/* + * Prepare to look for the next patch in the patch file. + */ +void +re_patch(void) +{ + p_first = 0; + p_newfirst = 0; + p_ptrn_lines = 0; + p_repl_lines = 0; + p_end = (LINENUM) - 1; + p_max = 0; + p_indent = 0; +} + +/* + * Open the patch file at the beginning of time. + */ +void +open_patch_file(const char *filename) +{ + struct stat filestat; + int nr, nw; + + if (filename == NULL || *filename == '\0' || strEQ(filename, "-")) { + pfp = fopen(TMPPATNAME, "w"); + if (pfp == NULL) + pfatal("can't create %s", TMPPATNAME); + while ((nr = fread(buf, 1, buf_size, stdin)) > 0) { + nw = fwrite(buf, 1, nr, pfp); + if (nr != nw) + pfatal("write error to %s", TMPPATNAME); + } + if (ferror(pfp) || fclose(pfp)) + pfatal("can't write %s", TMPPATNAME); + filename = TMPPATNAME; + } + pfp = fopen(filename, "r"); + if (pfp == NULL) + pfatal("patch file %s not found", filename); + if (fstat(fileno(pfp), &filestat)) + pfatal("can't stat %s", filename); + p_filesize = filestat.st_size; + next_intuit_at(0, 1L); /* start at the beginning */ + set_hunkmax(); +} + +/* + * Make sure our dynamically realloced tables are malloced to begin with. + */ +void +set_hunkmax(void) +{ + if (p_line == NULL) + p_line = malloc(hunkmax * sizeof(char *)); + if (p_len == NULL) + p_len = malloc(hunkmax * sizeof(size_t)); + if (p_char == NULL) + p_char = malloc(hunkmax * sizeof(char)); +} + +/* + * Enlarge the arrays containing the current hunk of patch. + */ +static void +grow_hunkmax(void) +{ + int new_hunkmax = hunkmax * 2; + + if (p_line == NULL || p_len == NULL || p_char == NULL) + fatal("Internal memory allocation error\n"); + + p_line = reallocf(p_line, new_hunkmax * sizeof(char *)); + p_len = reallocf(p_len, new_hunkmax * sizeof(size_t)); + p_char = reallocf(p_char, new_hunkmax * sizeof(char)); + + if (p_line != NULL && p_len != NULL && p_char != NULL) { + hunkmax = new_hunkmax; + return; + } + + if (!using_plan_a) + fatal("out of memory\n"); + out_of_mem = true; /* whatever is null will be allocated again */ + /* from within plan_a(), of all places */ +} + +/* True if the remainder of the patch file contains a diff of some sort. */ + +bool +there_is_another_patch(void) +{ + bool exists = false; + + if (p_base != 0 && p_base >= p_filesize) { + if (verbose) + say("done\n"); + return false; + } + if (p_filesize == 0) + return false; + nonempty_patchf_seen = true; + if (verbose) + say("Hmm..."); + diff_type = intuit_diff_type(); + if (!diff_type) { + if (p_base != 0) { + if (verbose) + say(" Ignoring the trailing garbage.\ndone\n"); + } else + say(" I can't seem to find a patch in there anywhere.\n"); + return false; + } + if (verbose) + say(" %sooks like %s to me...\n", + (p_base == 0 ? "L" : "The next patch l"), + diff_type == UNI_DIFF ? "a unified diff" : + diff_type == CONTEXT_DIFF ? "a context diff" : + diff_type == NEW_CONTEXT_DIFF ? "a new-style context diff" : + diff_type == NORMAL_DIFF ? "a normal diff" : + "an ed script"); + if (p_indent && verbose) + say("(Patch is indented %d space%s.)\n", p_indent, + p_indent == 1 ? "" : "s"); + skip_to(p_start, p_sline); + while (filearg[0] == NULL) { + if (force || batch) { + say("No file to patch. Skipping...\n"); + filearg[0] = xstrdup(bestguess); + skip_rest_of_patch = true; + return true; + } + ask("File to patch: "); + if (*buf != '\n') { + free(bestguess); + bestguess = xstrdup(buf); + filearg[0] = fetchname(buf, &exists, 0); + } + /* + * fetchname can now return buf = NULL, exists = true, to + * indicate to the caller that /dev/null was specified. Retain + * previous behavior for now until this can be better evaluted. + */ + if (filearg[0] == NULL || !exists) { + int def_skip = *bestguess == '\0'; + ask("No file found--skip this patch? [%c] ", + def_skip ? 'y' : 'n'); + if (*buf == 'n' || (!def_skip && *buf != 'y')) + continue; + if (verbose) + say("Skipping patch...\n"); + free(filearg[0]); + filearg[0] = fetchname(bestguess, &exists, 0); + skip_rest_of_patch = true; + return true; + } + } + return true; +} + +static void +p4_fetchname(struct file_name *name, char *str) +{ + char *t, *h; + + /* Skip leading whitespace. */ + while (isspace((unsigned char)*str)) + str++; + + /* Remove the file revision number. */ + for (t = str, h = NULL; *t != '\0' && !isspace((unsigned char)*t); t++) + if (*t == '#') + h = t; + if (h != NULL) + *h = '\0'; + + name->path = fetchname(str, &name->exists, strippath); +} + +/* Determine what kind of diff is in the remaining part of the patch file. */ + +static int +intuit_diff_type(void) +{ + off_t this_line = 0, previous_line; + off_t first_command_line = -1; + LINENUM fcl_line = -1; + bool last_line_was_command = false, this_is_a_command = false; + bool stars_last_line = false, stars_this_line = false; + char *s, *t; + int indent, retval; + struct file_name names[MAX_FILE]; + int piece_of_git = 0; + + memset(names, 0, sizeof(names)); + ok_to_create_file = false; + fseeko(pfp, p_base, SEEK_SET); + p_input_line = p_bline - 1; + for (;;) { + previous_line = this_line; + last_line_was_command = this_is_a_command; + stars_last_line = stars_this_line; + this_line = ftello(pfp); + indent = 0; + p_input_line++; + if (pgets(false) == 0) { + if (first_command_line >= 0) { + /* nothing but deletes!? */ + p_start = first_command_line; + p_sline = fcl_line; + retval = ED_DIFF; + goto scan_exit; + } else { + p_start = this_line; + p_sline = p_input_line; + retval = 0; + goto scan_exit; + } + } + for (s = buf; *s == ' ' || *s == '\t' || *s == 'X'; s++) { + if (*s == '\t') + indent += 8 - (indent % 8); + else + indent++; + } + for (t = s; isdigit((unsigned char)*t) || *t == ','; t++) + ; + this_is_a_command = (isdigit((unsigned char)*s) && + (*t == 'd' || *t == 'c' || *t == 'a')); + if (first_command_line < 0 && this_is_a_command) { + first_command_line = this_line; + fcl_line = p_input_line; + p_indent = indent; /* assume this for now */ + } + if (!stars_last_line && strnEQ(s, "*** ", 4)) + names[OLD_FILE].path = fetchname(s + 4, + &names[OLD_FILE].exists, strippath); + else if (strnEQ(s, "--- ", 4)) { + size_t off = 4; + if (piece_of_git && strippath == 957 && + strnEQ(s, "--- a/", 6)) + off = 6; + names[NEW_FILE].path = fetchname(s + off, + &names[NEW_FILE].exists, strippath); + } else if (strnEQ(s, "+++ ", 4)) { + /* pretend it is the old name */ + size_t off = 4; + if (piece_of_git && strippath == 957 && + strnEQ(s, "+++ b/", 6)) + off = 6; + names[OLD_FILE].path = fetchname(s + off, + &names[OLD_FILE].exists, strippath); + } else if (strnEQ(s, "Index:", 6)) + names[INDEX_FILE].path = fetchname(s + 6, + &names[INDEX_FILE].exists, strippath); + else if (strnEQ(s, "Prereq:", 7)) { + for (t = s + 7; isspace((unsigned char)*t); t++) + ; + revision = xstrdup(t); + for (t = revision; + *t && !isspace((unsigned char)*t); t++) + ; + *t = '\0'; + if (*revision == '\0') { + free(revision); + revision = NULL; + } + } else if (strnEQ(s, "diff --git a/", 13)) { + /* Git-style diffs. */ + piece_of_git = 1; + } else if (strnEQ(s, "==== ", 5)) { + /* Perforce-style diffs. */ + if ((t = strstr(s + 5, " - ")) != NULL) + p4_fetchname(&names[NEW_FILE], t + 3); + p4_fetchname(&names[OLD_FILE], s + 5); + } + if ((!diff_type || diff_type == ED_DIFF) && + first_command_line >= 0 && + strEQ(s, ".\n")) { + p_indent = indent; + p_start = first_command_line; + p_sline = fcl_line; + retval = ED_DIFF; + goto scan_exit; + } + if ((!diff_type || diff_type == UNI_DIFF) && strnEQ(s, "@@ -", 4)) { + if (strnEQ(s + 4, "0,0", 3)) + ok_to_create_file = true; + p_indent = indent; + p_start = this_line; + p_sline = p_input_line; + retval = UNI_DIFF; + goto scan_exit; + } + stars_this_line = strnEQ(s, "********", 8); + if ((!diff_type || diff_type == CONTEXT_DIFF) && stars_last_line && + strnEQ(s, "*** ", 4)) { + if (strtolinenum(s + 4, &s) == 0) + ok_to_create_file = true; + /* + * If this is a new context diff the character just + * at the end of the line is a '*'. + */ + while (*s && *s != '\n') + s++; + p_indent = indent; + p_start = previous_line; + p_sline = p_input_line - 1; + retval = (*(s - 1) == '*' ? NEW_CONTEXT_DIFF : CONTEXT_DIFF); + goto scan_exit; + } + if ((!diff_type || diff_type == NORMAL_DIFF) && + last_line_was_command && + (strnEQ(s, "< ", 2) || strnEQ(s, "> ", 2))) { + p_start = previous_line; + p_sline = p_input_line - 1; + p_indent = indent; + retval = NORMAL_DIFF; + goto scan_exit; + } + } +scan_exit: + if (retval == UNI_DIFF) { + /* unswap old and new */ + struct file_name tmp = names[OLD_FILE]; + names[OLD_FILE] = names[NEW_FILE]; + names[NEW_FILE] = tmp; + } + + /* Invalidated */ + free(source_file); + source_file = NULL; + + if (retval != 0) { + /* + * If we've successfully determined a diff type, stored in + * retval, path == NULL means _PATH_DEVNULL if exists is set. + * Explicitly specify it here to make it easier to detect later + * on that we're actually creating a file and not that we've + * just goofed something up. + */ + if (names[OLD_FILE].path != NULL) + source_file = xstrdup(names[OLD_FILE].path); + else if (names[OLD_FILE].exists) + source_file = xstrdup(_PATH_DEVNULL); + } + if (filearg[0] == NULL) { + if (posix) + filearg[0] = posix_name(names, ok_to_create_file); + else { + /* Ignore the Index: name for context diffs, like GNU */ + if (names[OLD_FILE].path != NULL || + names[NEW_FILE].path != NULL) { + free(names[INDEX_FILE].path); + names[INDEX_FILE].path = NULL; + } + filearg[0] = best_name(names, ok_to_create_file); + } + } + + free(bestguess); + bestguess = NULL; + if (filearg[0] != NULL) + bestguess = xstrdup(filearg[0]); + else if (!ok_to_create_file) { + /* + * We don't want to create a new file but we need a + * filename to set bestguess. Avoid setting filearg[0] + * so the file is not created automatically. + */ + if (posix) + bestguess = posix_name(names, true); + else + bestguess = best_name(names, true); + } + free(names[OLD_FILE].path); + free(names[NEW_FILE].path); + free(names[INDEX_FILE].path); + return retval; +} + +/* + * Remember where this patch ends so we know where to start up again. + */ +static void +next_intuit_at(off_t file_pos, LINENUM file_line) +{ + p_base = file_pos; + p_bline = file_line; +} + +/* + * Basically a verbose fseeko() to the actual diff listing. + */ +static void +skip_to(off_t file_pos, LINENUM file_line) +{ + size_t len; + + if (p_base > file_pos) + fatal("Internal error: seek %lld>%lld\n", + (long long)p_base, (long long)file_pos); + if (verbose && p_base < file_pos) { + fseeko(pfp, p_base, SEEK_SET); + say("The text leading up to this was:\n--------------------------\n"); + while (ftello(pfp) < file_pos) { + len = pgets(false); + if (len == 0) + fatal("Unexpected end of file\n"); + say("|%s", buf); + } + say("--------------------------\n"); + } else + fseeko(pfp, file_pos, SEEK_SET); + p_input_line = file_line - 1; +} + +/* Make this a function for better debugging. */ +static void +malformed(void) +{ + fatal("malformed patch at line %ld: %s", p_input_line, buf); + /* about as informative as "Syntax error" in C */ +} + +/* + * True if the line has been discarded (i.e. it is a line saying + * "\ No newline at end of file".) + */ +static bool +remove_special_line(void) +{ + int c; + + c = fgetc(pfp); + if (c == '\\') { + do { + c = fgetc(pfp); + } while (c != EOF && c != '\n'); + + return true; + } + if (c != EOF) + fseeko(pfp, -1, SEEK_CUR); + + return false; +} + +/* + * True if there is more of the current diff listing to process. + */ +bool +another_hunk(void) +{ + off_t line_beginning; /* file pos of the current line */ + LINENUM repl_beginning; /* index of --- line */ + LINENUM fillcnt; /* #lines of missing ptrn or repl */ + LINENUM fillsrc; /* index of first line to copy */ + LINENUM filldst; /* index of first missing line */ + bool ptrn_spaces_eaten; /* ptrn was slightly malformed */ + bool repl_could_be_missing; /* no + or ! lines in this hunk */ + bool repl_missing; /* we are now backtracking */ + off_t repl_backtrack_position; /* file pos of first repl line */ + LINENUM repl_patch_line; /* input line number for same */ + LINENUM ptrn_copiable; /* # of copiable lines in ptrn */ + char *s; + size_t len; + int context = 0; + + while (p_end >= 0) { + if (p_end == p_efake) + p_end = p_bfake; /* don't free twice */ + else + free(p_line[p_end]); + p_end--; + } + p_efake = -1; + + p_max = hunkmax; /* gets reduced when --- found */ + if (diff_type == CONTEXT_DIFF || diff_type == NEW_CONTEXT_DIFF) { + line_beginning = ftello(pfp); + repl_beginning = 0; + fillcnt = 0; + fillsrc = 0; + filldst = 0; + ptrn_spaces_eaten = false; + repl_could_be_missing = true; + repl_missing = false; + repl_backtrack_position = 0; + repl_patch_line = 0; + ptrn_copiable = 0; + + len = pgets(true); + p_input_line++; + if (len == 0 || strnNE(buf, "********", 8)) { + next_intuit_at(line_beginning, p_input_line); + return false; + } + p_context = 100; + p_hunk_beg = p_input_line + 1; + while (p_end < p_max) { + line_beginning = ftello(pfp); + len = pgets(true); + p_input_line++; + if (len == 0) { + if (repl_beginning && repl_could_be_missing) { + repl_missing = true; + goto hunk_done; + } + fatal("unexpected end of file in patch\n"); + } + p_end++; + if (p_end >= hunkmax) + fatal("Internal error: hunk larger than hunk " + "buffer size"); + p_char[p_end] = *buf; + p_line[p_end] = NULL; + switch (*buf) { + case '*': + if (strnEQ(buf, "********", 8)) { + if (repl_beginning && repl_could_be_missing) { + repl_missing = true; + goto hunk_done; + } else + fatal("unexpected end of hunk " + "at line %ld\n", + p_input_line); + } + if (p_end != 0) { + if (repl_beginning && repl_could_be_missing) { + repl_missing = true; + goto hunk_done; + } + fatal("unexpected *** at line %ld: %s", + p_input_line, buf); + } + context = 0; + p_line[p_end] = savestr(buf); + if (out_of_mem) { + p_end--; + return false; + } + for (s = buf; + *s && !isdigit((unsigned char)*s); s++) + ; + if (!*s) + malformed(); + if (strnEQ(s, "0,0", 3)) + memmove(s, s + 2, strlen(s + 2) + 1); + p_first = strtolinenum(s, &s); + if (*s == ',') { + for (; + *s && !isdigit((unsigned char)*s); s++) + ; + if (!*s) + malformed(); + p_ptrn_lines = strtolinenum(s, &s) - p_first + 1; + if (p_ptrn_lines < 0) + malformed(); + } else if (p_first) + p_ptrn_lines = 1; + else { + p_ptrn_lines = 0; + p_first = 1; + } + if (p_first >= LINENUM_MAX - p_ptrn_lines || + p_ptrn_lines >= LINENUM_MAX - 6) + malformed(); + + /* we need this much at least */ + p_max = p_ptrn_lines + 6; + while (p_max >= hunkmax) + grow_hunkmax(); + p_max = hunkmax; + break; + case '-': + if (buf[1] == '-') { + if (repl_beginning || + (p_end != p_ptrn_lines + 1 + + (p_char[p_end - 1] == '\n'))) { + if (p_end == 1) { + /* + * `old' lines were omitted; + * set up to fill them in + * from 'new' context lines. + */ + p_end = p_ptrn_lines + 1; + fillsrc = p_end + 1; + filldst = 1; + fillcnt = p_ptrn_lines; + } else { + if (repl_beginning) { + if (repl_could_be_missing) { + repl_missing = true; + goto hunk_done; + } + fatal("duplicate \"---\" at line %ld--check line numbers at line %ld\n", + p_input_line, p_hunk_beg + repl_beginning); + } else { + fatal("%s \"---\" at line %ld--check line numbers at line %ld\n", + (p_end <= p_ptrn_lines + ? "Premature" + : "Overdue"), + p_input_line, p_hunk_beg); + } + } + } + repl_beginning = p_end; + repl_backtrack_position = ftello(pfp); + repl_patch_line = p_input_line; + p_line[p_end] = savestr(buf); + if (out_of_mem) { + p_end--; + return false; + } + p_char[p_end] = '='; + for (s = buf; *s && !isdigit((unsigned char)*s); s++) + ; + if (!*s) + malformed(); + p_newfirst = strtolinenum(s, &s); + if (*s == ',') { + for (; *s && !isdigit((unsigned char)*s); s++) + ; + if (!*s) + malformed(); + p_repl_lines = strtolinenum(s, &s) - + p_newfirst + 1; + if (p_repl_lines < 0) + malformed(); + } else if (p_newfirst) + p_repl_lines = 1; + else { + p_repl_lines = 0; + p_newfirst = 1; + } + if (p_newfirst >= LINENUM_MAX - p_repl_lines || + p_repl_lines >= LINENUM_MAX - p_end) + malformed(); + p_max = p_repl_lines + p_end; + if (p_max > MAXHUNKSIZE) + fatal("hunk too large (%ld lines) at line %ld: %s", + p_max, p_input_line, buf); + while (p_max >= hunkmax) + grow_hunkmax(); + if (p_repl_lines != ptrn_copiable && + (p_context != 0 || p_repl_lines != 1)) + repl_could_be_missing = false; + break; + } + goto change_line; + case '+': + case '!': + repl_could_be_missing = false; + change_line: + if (buf[1] == '\n' && canonicalize) + strlcpy(buf + 1, " \n", buf_size - 1); + if (!isspace((unsigned char)buf[1]) && + buf[1] != '>' && buf[1] != '<' && + repl_beginning && repl_could_be_missing) { + repl_missing = true; + goto hunk_done; + } + if (context >= 0) { + if (context < p_context) + p_context = context; + context = -1000; + } + p_line[p_end] = savestr(buf + 2); + if (out_of_mem) { + p_end--; + return false; + } + if (p_end == p_ptrn_lines) { + if (remove_special_line()) { + int l; + + l = strlen(p_line[p_end]) - 1; + (p_line[p_end])[l] = 0; + } + } + break; + case '\t': + case '\n': /* assume the 2 spaces got eaten */ + if (repl_beginning && repl_could_be_missing && + (!ptrn_spaces_eaten || + diff_type == NEW_CONTEXT_DIFF)) { + repl_missing = true; + goto hunk_done; + } + p_line[p_end] = savestr(buf); + if (out_of_mem) { + p_end--; + return false; + } + if (p_end != p_ptrn_lines + 1) { + ptrn_spaces_eaten |= (repl_beginning != 0); + context++; + if (!repl_beginning) + ptrn_copiable++; + p_char[p_end] = ' '; + } + break; + case ' ': + if (!isspace((unsigned char)buf[1]) && + repl_beginning && repl_could_be_missing) { + repl_missing = true; + goto hunk_done; + } + context++; + if (!repl_beginning) + ptrn_copiable++; + p_line[p_end] = savestr(buf + 2); + if (out_of_mem) { + p_end--; + return false; + } + break; + default: + if (repl_beginning && repl_could_be_missing) { + repl_missing = true; + goto hunk_done; + } + malformed(); + } + /* set up p_len for strncmp() so we don't have to */ + /* assume null termination */ + if (p_line[p_end]) + p_len[p_end] = strlen(p_line[p_end]); + else + p_len[p_end] = 0; + } + +hunk_done: + if (p_end >= 0 && !repl_beginning) + fatal("no --- found in patch at line %ld\n", pch_hunk_beg()); + + if (repl_missing) { + + /* reset state back to just after --- */ + p_input_line = repl_patch_line; + for (p_end--; p_end > repl_beginning; p_end--) + free(p_line[p_end]); + fseeko(pfp, repl_backtrack_position, SEEK_SET); + + /* redundant 'new' context lines were omitted - set */ + /* up to fill them in from the old file context */ + if (!p_context && p_repl_lines == 1) { + p_repl_lines = 0; + p_max--; + } + fillsrc = 1; + filldst = repl_beginning + 1; + fillcnt = p_repl_lines; + p_end = p_max; + } else if (!p_context && fillcnt == 1) { + /* the first hunk was a null hunk with no context */ + /* and we were expecting one line -- fix it up. */ + while (filldst < p_end) { + p_line[filldst] = p_line[filldst + 1]; + p_char[filldst] = p_char[filldst + 1]; + p_len[filldst] = p_len[filldst + 1]; + filldst++; + } +#if 0 + repl_beginning--; /* this doesn't need to be fixed */ +#endif + p_end--; + p_first++; /* do append rather than insert */ + fillcnt = 0; + p_ptrn_lines = 0; + } + if (diff_type == CONTEXT_DIFF && + (fillcnt || (p_first > 1 && ptrn_copiable > 2 * p_context))) { + if (verbose) + say("%s\n%s\n%s\n", + "(Fascinating--this is really a new-style context diff but without", + "the telltale extra asterisks on the *** line that usually indicate", + "the new style...)"); + diff_type = NEW_CONTEXT_DIFF; + } + /* if there were omitted context lines, fill them in now */ + if (fillcnt) { + p_bfake = filldst; /* remember where not to free() */ + p_efake = filldst + fillcnt - 1; + while (fillcnt-- > 0) { + while (fillsrc <= p_end && p_char[fillsrc] != ' ') + fillsrc++; + if (fillsrc > p_end) + fatal("replacement text or line numbers mangled in hunk at line %ld\n", + p_hunk_beg); + p_line[filldst] = p_line[fillsrc]; + p_char[filldst] = p_char[fillsrc]; + p_len[filldst] = p_len[fillsrc]; + fillsrc++; + filldst++; + } + while (fillsrc <= p_end && fillsrc != repl_beginning && + p_char[fillsrc] != ' ') + fillsrc++; +#ifdef DEBUGGING + if (debug & 64) + printf("fillsrc %ld, filldst %ld, rb %ld, e+1 %ld\n", + fillsrc, filldst, repl_beginning, p_end + 1); +#endif + if (fillsrc != p_end + 1 && fillsrc != repl_beginning) + malformed(); + if (filldst != p_end + 1 && filldst != repl_beginning) + malformed(); + } + if (p_line[p_end] != NULL) { + if (remove_special_line()) { + p_len[p_end] -= 1; + (p_line[p_end])[p_len[p_end]] = 0; + } + } + } else if (diff_type == UNI_DIFF) { + LINENUM fillold; /* index of old lines */ + LINENUM fillnew; /* index of new lines */ + char ch; + + line_beginning = ftello(pfp); /* file pos of the current line */ + len = pgets(true); + p_input_line++; + if (len == 0 || strnNE(buf, "@@ -", 4)) { + next_intuit_at(line_beginning, p_input_line); + return false; + } + s = buf + 4; + if (!*s) + malformed(); + p_first = strtolinenum(s, &s); + if (*s == ',') { + p_ptrn_lines = strtolinenum(s + 1, &s); + } else + p_ptrn_lines = 1; + if (*s == ' ') + s++; + if (*s != '+' || !*++s) + malformed(); + p_newfirst = strtolinenum(s, &s); + if (*s == ',') { + p_repl_lines = strtolinenum(s + 1, &s); + } else + p_repl_lines = 1; + if (*s == ' ') + s++; + if (*s != '@') + malformed(); + if (p_first >= LINENUM_MAX - p_ptrn_lines || + p_newfirst > LINENUM_MAX - p_repl_lines || + p_ptrn_lines >= LINENUM_MAX - p_repl_lines - 1) + malformed(); + if (!p_ptrn_lines) + p_first++; /* do append rather than insert */ + p_max = p_ptrn_lines + p_repl_lines + 1; + while (p_max >= hunkmax) + grow_hunkmax(); + fillold = 1; + fillnew = fillold + p_ptrn_lines; + p_end = fillnew + p_repl_lines; + snprintf(buf, buf_size, "*** %ld,%ld ****\n", p_first, + p_first + p_ptrn_lines - 1); + p_line[0] = savestr(buf); + if (out_of_mem) { + p_end = -1; + return false; + } + p_char[0] = '*'; + snprintf(buf, buf_size, "--- %ld,%ld ----\n", p_newfirst, + p_newfirst + p_repl_lines - 1); + p_line[fillnew] = savestr(buf); + if (out_of_mem) { + p_end = 0; + return false; + } + p_char[fillnew++] = '='; + p_context = 100; + context = 0; + p_hunk_beg = p_input_line + 1; + while (fillold <= p_ptrn_lines || fillnew <= p_end) { + line_beginning = ftello(pfp); + len = pgets(true); + p_input_line++; + if (len == 0) { + if (p_max - fillnew < 3) { + /* assume blank lines got chopped */ + strlcpy(buf, " \n", buf_size); + } else { + fatal("unexpected end of file in patch\n"); + } + } + if (*buf == '\t' || *buf == '\n') { + ch = ' '; /* assume the space got eaten */ + s = savestr(buf); + } else { + ch = *buf; + s = savestr(buf + 1); + } + if (out_of_mem) { + while (--fillnew > p_ptrn_lines) + free(p_line[fillnew]); + p_end = fillold - 1; + return false; + } + switch (ch) { + case '-': + if (fillold > p_ptrn_lines) { + free(s); + p_end = fillnew - 1; + malformed(); + } + p_char[fillold] = ch; + p_line[fillold] = s; + p_len[fillold++] = strlen(s); + if (fillold > p_ptrn_lines) { + if (remove_special_line()) { + p_len[fillold - 1] -= 1; + s[p_len[fillold - 1]] = 0; + } + } + break; + case '=': + ch = ' '; + /* FALL THROUGH */ + case ' ': + if (fillold > p_ptrn_lines) { + free(s); + while (--fillnew > p_ptrn_lines) + free(p_line[fillnew]); + p_end = fillold - 1; + malformed(); + } + context++; + p_char[fillold] = ch; + p_line[fillold] = s; + p_len[fillold++] = strlen(s); + s = savestr(s); + if (out_of_mem) { + while (--fillnew > p_ptrn_lines) + free(p_line[fillnew]); + p_end = fillold - 1; + return false; + } + if (fillold > p_ptrn_lines) { + if (remove_special_line()) { + p_len[fillold - 1] -= 1; + s[p_len[fillold - 1]] = 0; + } + } + /* FALL THROUGH */ + case '+': + if (fillnew > p_end) { + free(s); + while (--fillnew > p_ptrn_lines) + free(p_line[fillnew]); + p_end = fillold - 1; + malformed(); + } + p_char[fillnew] = ch; + p_line[fillnew] = s; + p_len[fillnew++] = strlen(s); + if (fillold > p_ptrn_lines) { + if (remove_special_line()) { + p_len[fillnew - 1] -= 1; + s[p_len[fillnew - 1]] = 0; + } + } + break; + default: + p_end = fillnew; + malformed(); + } + if (ch != ' ' && context > 0) { + if (context < p_context) + p_context = context; + context = -1000; + } + } /* while */ + } else { /* normal diff--fake it up */ + char hunk_type; + int i; + LINENUM min, max; + + line_beginning = ftello(pfp); + p_context = 0; + len = pgets(true); + p_input_line++; + if (len == 0 || !isdigit((unsigned char)*buf)) { + next_intuit_at(line_beginning, p_input_line); + return false; + } + p_first = strtolinenum(buf, &s); + if (*s == ',') { + p_ptrn_lines = strtolinenum(s + 1, &s) - p_first + 1; + if (p_ptrn_lines < 0) + malformed(); + } else + p_ptrn_lines = (*s != 'a'); + hunk_type = *s; + if (hunk_type == 'a') + p_first++; /* do append rather than insert */ + min = strtolinenum(s + 1, &s); + if (*s == ',') + max = strtolinenum(s + 1, &s); + else + max = min; + if (min < 0 || min > max || max - min == LINENUM_MAX) + malformed(); + if (hunk_type == 'd') + min++; + p_newfirst = min; + p_repl_lines = max - min + 1; + if (p_newfirst > LINENUM_MAX - p_repl_lines || + p_ptrn_lines >= LINENUM_MAX - p_repl_lines - 1) + malformed(); + p_end = p_ptrn_lines + p_repl_lines + 1; + if (p_end > MAXHUNKSIZE) + fatal("hunk too large (%ld lines) at line %ld: %s", + p_end, p_input_line, buf); + while (p_end >= hunkmax) + grow_hunkmax(); + snprintf(buf, buf_size, "*** %ld,%ld\n", p_first, + p_first + p_ptrn_lines - 1); + p_line[0] = savestr(buf); + if (out_of_mem) { + p_end = -1; + return false; + } + p_char[0] = '*'; + for (i = 1; i <= p_ptrn_lines; i++) { + len = pgets(true); + p_input_line++; + if (len == 0) + fatal("unexpected end of file in patch at line %ld\n", + p_input_line); + if (*buf != '<') + fatal("< expected at line %ld of patch\n", + p_input_line); + p_line[i] = savestr(buf + 2); + if (out_of_mem) { + p_end = i - 1; + return false; + } + p_len[i] = strlen(p_line[i]); + p_char[i] = '-'; + } + + if (remove_special_line()) { + p_len[i - 1] -= 1; + (p_line[i - 1])[p_len[i - 1]] = 0; + } + if (hunk_type == 'c') { + len = pgets(true); + p_input_line++; + if (len == 0) + fatal("unexpected end of file in patch at line %ld\n", + p_input_line); + if (*buf != '-') + fatal("--- expected at line %ld of patch\n", + p_input_line); + } + snprintf(buf, buf_size, "--- %ld,%ld\n", min, max); + p_line[i] = savestr(buf); + if (out_of_mem) { + p_end = i - 1; + return false; + } + p_char[i] = '='; + for (i++; i <= p_end; i++) { + len = pgets(true); + p_input_line++; + if (len == 0) + fatal("unexpected end of file in patch at line %ld\n", + p_input_line); + if (*buf != '>') + fatal("> expected at line %ld of patch\n", + p_input_line); + /* Don't overrun if we don't have enough line */ + if (len > 2) + p_line[i] = savestr(buf + 2); + else + p_line[i] = savestr(""); + + if (out_of_mem) { + p_end = i - 1; + return false; + } + p_len[i] = strlen(p_line[i]); + p_char[i] = '+'; + } + + if (remove_special_line()) { + p_len[i - 1] -= 1; + (p_line[i - 1])[p_len[i - 1]] = 0; + } + } + if (reverse) /* backwards patch? */ + if (!pch_swap()) + say("Not enough memory to swap next hunk!\n"); +#ifdef DEBUGGING + if (debug & 2) { + LINENUM i; + char special; + + for (i = 0; i <= p_end; i++) { + if (i == p_ptrn_lines) + special = '^'; + else + special = ' '; + fprintf(stderr, "%3ld %c %c %s", i, p_char[i], + special, p_line[i]); + fflush(stderr); + } + } +#endif + if (p_end + 1 < hunkmax)/* paranoia reigns supreme... */ + p_char[p_end + 1] = '^'; /* add a stopper for apply_hunk */ + return true; +} + +/* + * Input a line from the patch file. + * Worry about indentation if do_indent is true. + * The line is read directly into the buf global variable which + * is resized if necessary in order to hold the complete line. + * Returns the number of characters read including the terminating + * '\n', if any. + */ +size_t +pgets(bool do_indent) +{ + char *line; + size_t len = 0; + int indent = 0, skipped = 0; + + line = fgetln(pfp, &len); + if (line != NULL) { + if (len + 1 > buf_size) { + while (len + 1 > buf_size) + buf_size *= 2; + free(buf); + buf = malloc(buf_size); + if (buf == NULL) + fatal("out of memory\n"); + } + if (do_indent == 1 && p_indent) { + for (; + indent < p_indent && (*line == ' ' || *line == '\t' || *line == 'X'); + line++, skipped++) { + if (*line == '\t') + indent += 8 - (indent %7); + else + indent++; + } + } + memcpy(buf, line, len - skipped); + buf[len - skipped] = '\0'; + } + return len; +} + + +/* + * Reverse the old and new portions of the current hunk. + */ +bool +pch_swap(void) +{ + char **tp_line; /* the text of the hunk */ + size_t *tp_len; /* length of each line */ + char *tp_char; /* +, -, and ! */ + LINENUM i; + LINENUM n; + bool blankline = false; + char *s; + + i = p_first; + p_first = p_newfirst; + p_newfirst = i; + + /* make a scratch copy */ + + tp_line = p_line; + tp_len = p_len; + tp_char = p_char; + p_line = NULL; /* force set_hunkmax to allocate again */ + p_len = NULL; + p_char = NULL; + set_hunkmax(); + if (p_line == NULL || p_len == NULL || p_char == NULL) { + + free(p_line); + p_line = tp_line; + free(p_len); + p_len = tp_len; + free(p_char); + p_char = tp_char; + return false; /* not enough memory to swap hunk! */ + } + /* now turn the new into the old */ + + i = p_ptrn_lines + 1; + if (tp_char[i] == '\n') { /* account for possible blank line */ + blankline = true; + i++; + } + if (p_efake >= 0) { /* fix non-freeable ptr range */ + if (p_efake <= i) + n = p_end - i + 1; + else + n = -i; + p_efake += n; + p_bfake += n; + } + for (n = 0; i <= p_end; i++, n++) { + p_line[n] = tp_line[i]; + p_char[n] = tp_char[i]; + if (p_char[n] == '+') + p_char[n] = '-'; + p_len[n] = tp_len[i]; + } + if (blankline) { + i = p_ptrn_lines + 1; + p_line[n] = tp_line[i]; + p_char[n] = tp_char[i]; + p_len[n] = tp_len[i]; + n++; + } + if (p_char[0] != '=') + fatal("Malformed patch at line %ld: expected '=' found '%c'\n", + p_input_line, p_char[0]); + p_char[0] = '*'; + for (s = p_line[0]; *s; s++) + if (*s == '-') + *s = '*'; + + /* now turn the old into the new */ + + if (p_char[0] != '*') + fatal("Malformed patch at line %ld: expected '*' found '%c'\n", + p_input_line, p_char[0]); + tp_char[0] = '='; + for (s = tp_line[0]; *s; s++) + if (*s == '*') + *s = '-'; + for (i = 0; n <= p_end; i++, n++) { + p_line[n] = tp_line[i]; + p_char[n] = tp_char[i]; + if (p_char[n] == '-') + p_char[n] = '+'; + p_len[n] = tp_len[i]; + } + + if (i != p_ptrn_lines + 1) + fatal("Malformed patch at line %ld: expected %ld lines, " + "got %ld\n", + p_input_line, p_ptrn_lines + 1, i); + + i = p_ptrn_lines; + p_ptrn_lines = p_repl_lines; + p_repl_lines = i; + + free(tp_line); + free(tp_len); + free(tp_char); + + return true; +} + +/* + * Return the specified line position in the old file of the old context. + */ +LINENUM +pch_first(void) +{ + return p_first; +} + +/* + * Return the number of lines of old context. + */ +LINENUM +pch_ptrn_lines(void) +{ + return p_ptrn_lines; +} + +/* + * Return the probable line position in the new file of the first line. + */ +LINENUM +pch_newfirst(void) +{ + return p_newfirst; +} + +/* + * Return the number of lines in the replacement text including context. + */ +LINENUM +pch_repl_lines(void) +{ + return p_repl_lines; +} + +/* + * Return the number of lines in the whole hunk. + */ +LINENUM +pch_end(void) +{ + return p_end; +} + +/* + * Return the number of context lines before the first changed line. + */ +LINENUM +pch_context(void) +{ + return p_context; +} + +/* + * Return the length of a particular patch line. + */ +size_t +pch_line_len(LINENUM line) +{ + return p_len[line]; +} + +/* + * Return the control character (+, -, *, !, etc) for a patch line. + */ +char +pch_char(LINENUM line) +{ + return p_char[line]; +} + +/* + * Return a pointer to a particular patch line. + */ +char * +pfetch(LINENUM line) +{ + return p_line[line]; +} + +/* + * Return where in the patch file this hunk began, for error messages. + */ +LINENUM +pch_hunk_beg(void) +{ + return p_hunk_beg; +} + +/* + * Apply an ed script by feeding ed itself. + */ +void +do_ed_script(void) +{ + char *t; + off_t beginning_of_this_line; + FILE *pipefp = NULL; + int continuation; + + if (!skip_rest_of_patch) { + if (copy_file(filearg[0], TMPOUTNAME) < 0) { + unlink(TMPOUTNAME); + fatal("can't create temp file %s", TMPOUTNAME); + } + snprintf(buf, buf_size, "%s%s%s", _PATH_RED, + verbose ? " " : " -s ", TMPOUTNAME); + pipefp = popen(buf, "w"); + } + for (;;) { + beginning_of_this_line = ftello(pfp); + if (pgets(true) == 0) { + next_intuit_at(beginning_of_this_line, p_input_line); + break; + } + p_input_line++; + for (t = buf; isdigit((unsigned char)*t) || *t == ','; t++) + ; + /* POSIX defines allowed commands as {a,c,d,i,s} */ + if (isdigit((unsigned char)*buf) && + (*t == 'a' || *t == 'c' || *t == 'd' || *t == 'i' || *t == 's')) { + if (pipefp != NULL) + fputs(buf, pipefp); + if (*t == 's') { + for (;;) { + continuation = 0; + t = strchr(buf, '\0') - 1; + while (--t >= buf && *t == '\\') + continuation = !continuation; + if (!continuation || + pgets(true) == 0) + break; + if (pipefp != NULL) + fputs(buf, pipefp); + } + } else if (*t != 'd') { + while (pgets(true)) { + p_input_line++; + if (pipefp != NULL) + fputs(buf, pipefp); + if (strEQ(buf, ".\n")) + break; + } + } + } else { + next_intuit_at(beginning_of_this_line, p_input_line); + break; + } + } + if (pipefp == NULL) + return; + fprintf(pipefp, "w\n"); + fprintf(pipefp, "q\n"); + fflush(pipefp); + pclose(pipefp); + ignore_signals(); + if (!check_only) { + if (move_file(TMPOUTNAME, outname) < 0) { + toutkeep = true; + chmod(TMPOUTNAME, filemode); + } else + chmod(outname, filemode); + } + set_signals(1); +} + +/* + * Choose the name of the file to be patched based on POSIX rules. + * NOTE: the POSIX rules are amazingly stupid and we only follow them + * if the user specified --posix or set POSIXLY_CORRECT. + */ +static char * +posix_name(const struct file_name *names, bool assume_exists) +{ + char *path = NULL; + int i; + + /* + * POSIX states that the filename will be chosen from one + * of the old, new and index names (in that order) if + * the file exists relative to CWD after -p stripping. + */ + for (i = 0; i < MAX_FILE; i++) { + if (names[i].path != NULL && names[i].exists) { + path = names[i].path; + break; + } + } + if (path == NULL && !assume_exists) { + /* + * No files found, check to see if the diff could be + * creating a new file. + */ + if (path == NULL && ok_to_create_file && + names[NEW_FILE].path != NULL) + path = names[NEW_FILE].path; + } + + return path ? xstrdup(path) : NULL; +} + +static char * +compare_names(const struct file_name *names, bool assume_exists) +{ + size_t min_components, min_baselen, min_len, tmp; + char *best = NULL; + char *path; + int i; + + /* + * The "best" name is the one with the fewest number of path + * components, the shortest basename length, and the shortest + * overall length (in that order). We only use the Index: file + * if neither of the old or new files could be intuited from + * the diff header. + */ + min_components = min_baselen = min_len = SIZE_MAX; + for (i = INDEX_FILE; i >= OLD_FILE; i--) { + path = names[i].path; + if (path == NULL || (!names[i].exists && !assume_exists)) + continue; + if ((tmp = num_components(path)) > min_components) + continue; + if (tmp < min_components) { + min_components = tmp; + best = path; + } + if ((tmp = strlen(basename(path))) > min_baselen) + continue; + if (tmp < min_baselen) { + min_baselen = tmp; + best = path; + } + if ((tmp = strlen(path)) > min_len) + continue; + min_len = tmp; + best = path; + } + return best; +} + +/* + * Choose the name of the file to be patched based the "best" one + * available. + */ +static char * +best_name(const struct file_name *names, bool assume_exists) +{ + char *best; + + best = compare_names(names, assume_exists); + + /* No match? Check to see if the diff could be creating a new file. */ + if (best == NULL && ok_to_create_file) + best = names[NEW_FILE].path; + + return best ? xstrdup(best) : NULL; +} + +static size_t +num_components(const char *path) +{ + size_t n; + const char *cp; + + for (n = 0, cp = path; (cp = strchr(cp, '/')) != NULL; n++) { + cp++; + while (*cp == '/') + cp++; /* skip consecutive slashes */ + } + return n; +} + +/* + * Convert number at NPTR into LINENUM and save address of first + * character that is not a digit in ENDPTR. If conversion is not + * possible, call fatal. + */ +static LINENUM +strtolinenum(char *nptr, char **endptr) +{ + LINENUM rv; + char c; + char *p; + const char *errstr; + + for (p = nptr; isdigit((unsigned char)*p); p++) + ; + + if (p == nptr) + malformed(); + + c = *p; + *p = '\0'; + + rv = strtonum(nptr, 0, LINENUM_MAX, &errstr); + if (errstr != NULL) + fatal("invalid line number at line %ld: `%s' is %s\n", + p_input_line, nptr, errstr); + + *p = c; + *endptr = p; + + return rv; +} diff --git a/src/patch/pch.h b/src/patch/pch.h new file mode 100644 index 0000000..b6c6363 --- /dev/null +++ b/src/patch/pch.h @@ -0,0 +1,57 @@ +/*- + * Copyright 1986, Larry Wall + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following condition is met: + * 1. Redistributions of source code must retain the above copyright notice, + * this condition and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + * + * patch - a program to apply diffs to original files + * + * -C option added in 1998, original code by Marc Espie, based on FreeBSD + * behaviour + * + * $OpenBSD: pch.h,v 1.9 2003/10/31 20:20:45 millert Exp $ + */ + +#define OLD_FILE 0 +#define NEW_FILE 1 +#define INDEX_FILE 2 +#define MAX_FILE 3 + +struct file_name { + char *path; + bool exists; +}; + +extern char *source_file; + +void re_patch(void); +void open_patch_file(const char *); +void set_hunkmax(void); +bool there_is_another_patch(void); +bool another_hunk(void); +bool pch_swap(void); +char *pfetch(LINENUM); +size_t pch_line_len(LINENUM); +LINENUM pch_first(void); +LINENUM pch_ptrn_lines(void); +LINENUM pch_newfirst(void); +LINENUM pch_repl_lines(void); +LINENUM pch_end(void); +LINENUM pch_context(void); +LINENUM pch_hunk_beg(void); +char pch_char(LINENUM); +void do_ed_script(void); diff --git a/src/patch/tests/PR74127-cline.diff b/src/patch/tests/PR74127-cline.diff new file mode 100644 index 0000000..551d86d --- /dev/null +++ b/src/patch/tests/PR74127-cline.diff @@ -0,0 +1,4 @@ +file.c +@@ -3,1 +3,1 @@ +- set Log(compressProg) /usr/local/bin/gzip ++ set Log(compressProg) /usr/bin/gzip diff --git a/src/patch/tests/PR74127-good.diff b/src/patch/tests/PR74127-good.diff new file mode 100644 index 0000000..fac1726 --- /dev/null +++ b/src/patch/tests/PR74127-good.diff @@ -0,0 +1,4 @@ +file.c +@@ -3,1 +3,1 @@ +- set Log(compressProg) gzip ++ set Log(compressProg) /usr/local/bin/gzip diff --git a/src/patch/tests/PR74127-repro.diff b/src/patch/tests/PR74127-repro.diff new file mode 100644 index 0000000..0ad3640 --- /dev/null +++ b/src/patch/tests/PR74127-repro.diff @@ -0,0 +1,4 @@ +file.c +@@ -5,1 +5,1 @@ +- set Log(compressProg) /usr/local/bin/gzip ++ set Log(compressProg) /usr/bin/gzip diff --git a/src/patch/tests/PR74127.in b/src/patch/tests/PR74127.in new file mode 100644 index 0000000..ed295c4 --- /dev/null +++ b/src/patch/tests/PR74127.in @@ -0,0 +1,14 @@ +# This program is used to compress log files +if {![info exists Log(compressProg)]} { + set Log(compressProg) gzip +} + +# Flush interval +if {![info exists Log(flushInterval)]} { + set Log(flushInterval) [expr {60 * 1000}] +} + +# This is used to turn on an alternate debug log file +if {![info exist Log(debug_log)]} { + set Log(debug_log) 0 +} diff --git a/src/patch/tests/unified_patch_test.sh b/src/patch/tests/unified_patch_test.sh new file mode 100755 index 0000000..7d4b741 --- /dev/null +++ b/src/patch/tests/unified_patch_test.sh @@ -0,0 +1,170 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2019 Kyle Evans +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. +# + +atf_test_case basic +basic_body() +{ + printf "a\nb\nc\nd\ne\nf\ng\nh\ni\n" > foo_full + printf "a\nb\nc\n" > foo_start + printf "g\nh\ni\n" > foo_end + printf "d\ne\nf\n" > foo_middle + + diff -u foo_start foo_full > foo_start2full.diff + diff -u foo_end foo_full > foo_end2full.diff + diff -u foo_middle foo_full > foo_mid2full.diff + + # Check lengths... each should have all 9 lines + 3 line header + atf_check -o inline:"12" -x \ + "cat foo_start2full.diff | wc -l | tr -d '[:space:]'" + atf_check -o inline:"12" -x \ + "cat foo_end2full.diff | wc -l | tr -d '[:space:]'" + atf_check -o inline:"12" -x \ + "cat foo_mid2full.diff | wc -l | tr -d '[:space:]'" + + # Apply the patch! Should succeed + atf_check -o ignore patch foo_start foo_start2full.diff \ + -o foo_start2full + atf_check -o ignore patch foo_end foo_end2full.diff \ + -o foo_end2full + atf_check -o ignore patch foo_middle foo_mid2full.diff \ + -o foo_mid2full + + # And these should all produce equivalent to the original full + atf_check -o ignore diff foo_start2full foo_full + atf_check -o ignore diff foo_end2full foo_full + atf_check -o ignore diff foo_mid2full foo_full +} + +atf_test_case limited_ctx +limited_ctx_head() +{ + atf_set "descr" "Verify correct behavior with limited context (PR 74127)" +} +limited_ctx_body() +{ + + # First; PR74127-repro.diff should not have applied, but it instead + # assumed a match and added the modified line at the offset specified... + atf_check -s not-exit:0 -o ignore -e ignore patch -o _.out \ + "$(atf_get_srcdir)/PR74127.in" \ + "$(atf_get_srcdir)/PR74127-repro.diff" + + # Let's extend that and make sure a similarly ill-contexted diff does + # not apply even with the correct line number + atf_check -s not-exit:0 -o ignore -e ignore patch -o _.out \ + "$(atf_get_srcdir)/PR74127.in" \ + "$(atf_get_srcdir)/PR74127-line.diff" + + # Correct line number and correct old line should always work + atf_check -o ignore -e ignore patch -o _.out \ + "$(atf_get_srcdir)/PR74127.in" \ + "$(atf_get_srcdir)/PR74127-good.diff" +} + +atf_test_case file_creation +file_creation_body() +{ + + echo "x" > foo + diff -u /dev/null foo > foo.diff + rm foo + + atf_check -x "patch -s < foo.diff" + atf_check -o ignore stat foo +} + +# This test is motivated by long-standing bugs that occasionally slip by in +# commits. If a file is created by a diff, patch(1) will happily duplicate the +# contents as many times as you apply the diff. It should instead detect that +# a source of /dev/null creates the file, so it shouldn't exist. Furthermore, +# the reverse of creation is deletion -- hence the next test, which ensures that +# the file is removed if it's empty once the patch is reversed. The size checks +# are scattered throughout to make sure that we didn't get some kind of false +# error, and the first size check is merely a sanity check that should be +# trivially true as this is executed in a sandbox. +atf_test_case file_nodupe +file_nodupe_body() +{ + + echo "x" > foo + diff -u /dev/null foo > foo.diff + + atf_check -o inline:"2\n" stat -f "%z" foo + atf_check -s not-exit:0 -o ignore -x "patch -Ns < foo.diff" + atf_check -o inline:"2\n" stat -f "%z" foo + atf_check -s not-exit:0 -o ignore -x "patch -fs < foo.diff" + atf_check -o inline:"2\n" stat -f "%z" foo +} + +atf_test_case file_removal +file_removal_body() +{ + + echo "x" > foo + diff -u /dev/null foo > foo.diff + + # Check that the file is removed completely if it was sourced from + # /dev/null + atf_check -x "patch -Rs < foo.diff" + atf_check -s not-exit:0 -e ignore stat foo + + # But if it had been modified, we'll only remove the portion that the + # patch would have created. This makes us compatible with GNU patch's + # behavior, at least. Whether that is the sane action or not is a + # question for further study, and then this comment may be removed. + printf "x\ny\n" > foo + atf_check -x "patch -Rs < foo.diff" + atf_check -o inline:"y\n" cat foo +} + +atf_test_case plinelen +plinelen_body() +{ + hello="$(jot -b hello -s, 20000 | tee foo.txt)" + cp foo.txt bar.txt + echo "world" >>bar.txt + cat >foo.diff < + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "util.h" +#include "backupfile.h" +#include "pathnames.h" + +/* Rename a file, copying it if necessary. */ + +int +move_file(const char *from, const char *to) +{ + int fromfd; + ssize_t i; + + /* to stdout? */ + + if (strEQ(to, "-")) { +#ifdef DEBUGGING + if (debug & 4) + say("Moving %s to stdout.\n", from); +#endif + fromfd = open(from, O_RDONLY); + if (fromfd < 0) + pfatal("internal error, can't reopen %s", from); + while ((i = read(fromfd, buf, buf_size)) > 0) + if (write(STDOUT_FILENO, buf, i) != i) + pfatal("write failed"); + close(fromfd); + return 0; + } + if (backup_file(to) < 0) { + say("Can't backup %s, output is in %s: %s\n", to, from, + strerror(errno)); + return -1; + } +#ifdef DEBUGGING + if (debug & 4) + say("Moving %s to %s.\n", from, to); +#endif + if (rename(from, to) < 0) { + if (errno != EXDEV || copy_file(from, to) < 0) { + say("Can't create %s, output is in %s: %s\n", + to, from, strerror(errno)); + return -1; + } + } + return 0; +} + +/* Backup the original file. */ + +int +backup_file(const char *orig) +{ + struct stat filestat; + char bakname[PATH_MAX], *s, *simplename; + dev_t orig_device; + ino_t orig_inode; + + if (backup_type == none || stat(orig, &filestat) != 0) + return 0; /* nothing to do */ + /* + * If the user used zero prefixes or suffixes, then + * he doesn't want backups. Yet we have to remove + * orig to break possible hardlinks. + */ + if ((origprae && *origprae == 0) || *simple_backup_suffix == 0) { + unlink(orig); + return 0; + } + orig_device = filestat.st_dev; + orig_inode = filestat.st_ino; + + if (origprae) { + if (strlcpy(bakname, origprae, sizeof(bakname)) >= sizeof(bakname) || + strlcat(bakname, orig, sizeof(bakname)) >= sizeof(bakname)) + fatal("filename %s too long for buffer\n", origprae); + } else { + if ((s = find_backup_file_name(orig)) == NULL) + fatal("out of memory\n"); + if (strlcpy(bakname, s, sizeof(bakname)) >= sizeof(bakname)) + fatal("filename %s too long for buffer\n", s); + free(s); + } + + if ((simplename = strrchr(bakname, '/')) != NULL) + simplename = simplename + 1; + else + simplename = bakname; + + /* + * Find a backup name that is not the same file. Change the + * first lowercase char into uppercase; if that isn't + * sufficient, chop off the first char and try again. + */ + while (stat(bakname, &filestat) == 0 && + orig_device == filestat.st_dev && orig_inode == filestat.st_ino) { + /* Skip initial non-lowercase chars. */ + for (s = simplename; *s && !islower((unsigned char)*s); s++) + ; + if (*s) + *s = toupper((unsigned char)*s); + else + memmove(simplename, simplename + 1, + strlen(simplename + 1) + 1); + } +#ifdef DEBUGGING + if (debug & 4) + say("Moving %s to %s.\n", orig, bakname); +#endif + if (rename(orig, bakname) < 0) { + if (errno != EXDEV || copy_file(orig, bakname) < 0) + return -1; + } + return 0; +} + +/* + * Copy a file. + */ +int +copy_file(const char *from, const char *to) +{ + int tofd, fromfd; + ssize_t i; + + tofd = open(to, O_CREAT|O_TRUNC|O_WRONLY, 0666); + if (tofd < 0) + return -1; + fromfd = open(from, O_RDONLY, 0); + if (fromfd < 0) + pfatal("internal error, can't reopen %s", from); + while ((i = read(fromfd, buf, buf_size)) > 0) + if (write(tofd, buf, i) != i) + pfatal("write to %s failed", to); + close(fromfd); + close(tofd); + return 0; +} + +/* + * Allocate a unique area for a string. + */ +char * +savestr(const char *s) +{ + char *rv; + + if (!s) + s = "Oops"; + rv = strdup(s); + if (rv == NULL) { + if (using_plan_a) + out_of_mem = true; + else + fatal("out of memory\n"); + } + return rv; +} + +/* + * Vanilla terminal output (buffered). + */ +void +say(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stdout, fmt, ap); + va_end(ap); + fflush(stdout); +} + +/* + * Terminal output, pun intended. + */ +void +fatal(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + fprintf(stderr, "patch: **** "); + vfprintf(stderr, fmt, ap); + va_end(ap); + my_exit(2); +} + +/* + * Say something from patch, something from the system, then silence . . . + */ +void +pfatal(const char *fmt, ...) +{ + va_list ap; + int errnum = errno; + + fprintf(stderr, "patch: **** "); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, ": %s\n", strerror(errnum)); + my_exit(2); +} + +/* + * Get a response from the user via /dev/tty + */ +void +ask(const char *fmt, ...) +{ + va_list ap; + ssize_t nr = 0; + static int ttyfd = -1; + + va_start(ap, fmt); + vfprintf(stdout, fmt, ap); + va_end(ap); + fflush(stdout); + if (ttyfd < 0) + ttyfd = open(_PATH_TTY, O_RDONLY); + if (ttyfd >= 0) { + if ((nr = read(ttyfd, buf, buf_size)) > 0 && + buf[nr - 1] == '\n') + buf[nr - 1] = '\0'; + } + if (ttyfd < 0 || nr <= 0) { + /* no tty or error reading, pretend user entered 'return' */ + putchar('\n'); + buf[0] = '\0'; + } +} + +/* + * How to handle certain events when not in a critical region. + */ +void +set_signals(int reset) +{ + static sig_t hupval, intval; + + if (!reset) { + hupval = signal(SIGHUP, SIG_IGN); + if (hupval != SIG_IGN) + hupval = my_exit; + intval = signal(SIGINT, SIG_IGN); + if (intval != SIG_IGN) + intval = my_exit; + } + signal(SIGHUP, hupval); + signal(SIGINT, intval); +} + +/* + * How to handle certain events when in a critical region. + */ +void +ignore_signals(void) +{ + signal(SIGHUP, SIG_IGN); + signal(SIGINT, SIG_IGN); +} + +/* + * Make sure we'll have the directories to create a file. If `striplast' is + * true, ignore the last element of `filename'. + */ + +void +makedirs(const char *filename, bool striplast) +{ + char *tmpbuf; + + if ((tmpbuf = strdup(filename)) == NULL) + fatal("out of memory\n"); + + if (striplast) { + char *s = strrchr(tmpbuf, '/'); + if (s == NULL) { + free(tmpbuf); + return; /* nothing to be done */ + } + *s = '\0'; + } + if (mkpath(tmpbuf) != 0) + pfatal("creation of %s failed", tmpbuf); + free(tmpbuf); +} + +/* + * Make filenames more reasonable. + */ +char * +fetchname(const char *at, bool *exists, int strip_leading) +{ + char *fullname, *name, *t; + int sleading, tab; + struct stat filestat; + + if (at == NULL || *at == '\0') + return NULL; + while (isspace((unsigned char)*at)) + at++; +#ifdef DEBUGGING + if (debug & 128) + say("fetchname %s %d\n", at, strip_leading); +#endif + /* So files can be created by diffing against /dev/null. */ + if (strnEQ(at, _PATH_DEVNULL, sizeof(_PATH_DEVNULL) - 1)) { + *exists = true; + return NULL; + } + name = fullname = t = savestr(at); + + tab = strchr(t, '\t') != NULL; + /* Strip off up to `strip_leading' path components and NUL terminate. */ + for (sleading = strip_leading; *t != '\0' && ((tab && *t != '\t') || + !isspace((unsigned char)*t)); t++) { + if (t[0] == '/' && t[1] != '/' && t[1] != '\0') + if (--sleading >= 0) + name = t + 1; + } + *t = '\0'; + + /* + * If no -p option was given (957 is the default value!), we were + * given a relative pathname, and the leading directories that we + * just stripped off all exist, put them back on. + */ + if (strip_leading == 957 && name != fullname && *fullname != '/') { + name[-1] = '\0'; + if (stat(fullname, &filestat) == 0 && S_ISDIR(filestat.st_mode)) { + name[-1] = '/'; + name = fullname; + } + } + name = savestr(name); + free(fullname); + + *exists = stat(name, &filestat) == 0; + return name; +} + +void +version(void) +{ + printf("patch 2.0-12u11 FreeBSD\n"); + my_exit(EXIT_SUCCESS); +} + +/* + * Exit with cleanup. + */ +void +my_exit(int status) +{ + unlink(TMPINNAME); + if (!toutkeep) + unlink(TMPOUTNAME); + if (!trejkeep) + unlink(TMPREJNAME); + unlink(TMPPATNAME); + exit(status); +} diff --git a/src/patch/util.h b/src/patch/util.h new file mode 100644 index 0000000..a968a64 --- /dev/null +++ b/src/patch/util.h @@ -0,0 +1,50 @@ +/*- + * Copyright 1986, Larry Wall + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following condition is met: + * 1. Redistributions of source code must retain the above copyright notice, + * this condition and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + * + * patch - a program to apply diffs to original files + * + * -C option added in 1998, original code by Marc Espie, based on FreeBSD + * behaviour + * + * $OpenBSD: util.h,v 1.16 2014/12/13 10:31:07 tobias Exp $ + */ + +char *fetchname(const char *, bool *, int); +int backup_file(const char *); +int move_file(const char *, const char *); +int copy_file(const char *, const char *); +void say(const char *, ...) + __attribute__((__format__(__printf__, 1, 2))); +void fatal(const char *, ...) + __attribute__((__format__(__printf__, 1, 2))); +void pfatal(const char *, ...) + __attribute__((__format__(__printf__, 1, 2))); +void ask(const char *, ...) + __attribute__((__format__(__printf__, 1, 2))); +char *savestr(const char *); +char *xstrdup(const char *); +void set_signals(int); +void ignore_signals(void); +void makedirs(const char *, bool); +void version(void); +void my_exit(int) __attribute__((noreturn)); + +/* in mkpath.c */ +extern int mkpath(char *); diff --git a/src/sdiff/edit.c b/src/sdiff/edit.c new file mode 100644 index 0000000..249cb09 --- /dev/null +++ b/src/sdiff/edit.c @@ -0,0 +1,208 @@ +/* $OpenBSD: edit.c,v 1.19 2009/06/07 13:29:50 ray Exp $ */ + +/* + * Written by Raymond Lai . + * Public domain. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "extern.h" + +static void +cleanup(const char *filename) +{ + + if (unlink(filename)) + err(2, "could not delete: %s", filename); + exit(2); +} + +/* + * Execute an editor on the specified pathname, which is interpreted + * from the shell. This means flags may be included. + * + * Returns -1 on error, or the exit value on success. + */ +static int +editit(const char *pathname) +{ + sig_t sighup, sigint, sigquit, sigchld; + pid_t pid; + int saved_errno, st, ret = -1; + const char *ed; + + ed = getenv("VISUAL"); + if (ed == NULL) + ed = getenv("EDITOR"); + if (ed == NULL) + ed = _PATH_VI; + + sighup = signal(SIGHUP, SIG_IGN); + sigint = signal(SIGINT, SIG_IGN); + sigquit = signal(SIGQUIT, SIG_IGN); + sigchld = signal(SIGCHLD, SIG_DFL); + if ((pid = fork()) == -1) + goto fail; + if (pid == 0) { + execlp(ed, ed, pathname, (char *)NULL); + _exit(127); + } + while (waitpid(pid, &st, 0) == -1) + if (errno != EINTR) + goto fail; + if (!WIFEXITED(st)) + errno = EINTR; + else + ret = WEXITSTATUS(st); + + fail: + saved_errno = errno; + (void)signal(SIGHUP, sighup); + (void)signal(SIGINT, sigint); + (void)signal(SIGQUIT, sigquit); + (void)signal(SIGCHLD, sigchld); + errno = saved_errno; + return (ret); +} + +/* + * Parse edit command. Returns 0 on success, -1 on error. + */ +int +eparse(const char *cmd, const char *left, const char *right) +{ + FILE *file; + size_t nread; + int fd; + char *filename; + char buf[BUFSIZ], *text; + + /* Skip whitespace. */ + while (isspace(*cmd)) + ++cmd; + + text = NULL; + switch (*cmd) { + case '\0': + /* Edit empty file. */ + break; + + case 'b': + /* Both strings. */ + if (left == NULL) + goto RIGHT; + if (right == NULL) + goto LEFT; + + /* Neither column is blank, so print both. */ + if (asprintf(&text, "%s\n%s\n", left, right) == -1) + err(2, "could not allocate memory"); + break; + + case 'l': +LEFT: + /* Skip if there is no left column. */ + if (left == NULL) + break; + + if (asprintf(&text, "%s\n", left) == -1) + err(2, "could not allocate memory"); + + break; + + case 'r': +RIGHT: + /* Skip if there is no right column. */ + if (right == NULL) + break; + + if (asprintf(&text, "%s\n", right) == -1) + err(2, "could not allocate memory"); + + break; + + default: + return (-1); + } + + /* Create temp file. */ + if (asprintf(&filename, "%s/sdiff.XXXXXXXXXX", tmpdir) == -1) + err(2, "asprintf"); + if ((fd = mkstemp(filename)) == -1) + err(2, "mkstemp"); + if (text != NULL) { + size_t len; + ssize_t nwritten; + + len = strlen(text); + if ((nwritten = write(fd, text, len)) == -1 || + (size_t)nwritten != len) { + warn("error writing to temp file"); + cleanup(filename); + } + } + close(fd); + + /* text is no longer used. */ + free(text); + + /* Edit temp file. */ + if (editit(filename) == -1) { + warn("error editing %s", filename); + cleanup(filename); + } + + /* Open temporary file. */ + if (!(file = fopen(filename, "r"))) { + warn("could not open edited file: %s", filename); + cleanup(filename); + } + + /* Copy temporary file contents to output file. */ + for (nread = sizeof(buf); nread == sizeof(buf);) { + size_t nwritten; + + nread = fread(buf, sizeof(*buf), sizeof(buf), file); + /* Test for error or end of file. */ + if (nread != sizeof(buf) && + (ferror(file) || !feof(file))) { + warnx("error reading edited file: %s", filename); + cleanup(filename); + } + + /* + * If we have nothing to read, break out of loop + * instead of writing nothing. + */ + if (!nread) + break; + + /* Write data we just read. */ + nwritten = fwrite(buf, sizeof(*buf), nread, outfp); + if (nwritten != nread) { + warnx("error writing to output file"); + cleanup(filename); + } + } + + /* We've reached the end of the temporary file, so remove it. */ + if (unlink(filename)) + warn("could not delete: %s", filename); + fclose(file); + + free(filename); + + return (0); +} diff --git a/src/sdiff/extern.h b/src/sdiff/extern.h new file mode 100644 index 0000000..c9ebaef --- /dev/null +++ b/src/sdiff/extern.h @@ -0,0 +1,11 @@ +/* $OpenBSD: extern.h,v 1.5 2009/06/07 13:29:50 ray Exp $ */ + +/* + * Written by Raymond Lai . + * Public domain. + */ + +extern FILE *outfp; /* file to save changes to */ +extern const char *tmpdir; + +int eparse(const char *, const char *, const char *); diff --git a/src/sdiff/sdiff.1 b/src/sdiff/sdiff.1 new file mode 100644 index 0000000..ca6594c --- /dev/null +++ b/src/sdiff/sdiff.1 @@ -0,0 +1,169 @@ +.\" $OpenBSD: sdiff.1,v 1.15 2007/06/29 14:48:07 jmc Exp $ +.\" +.\" Written by Raymond Lai . +.\" Public domain. +.\" +.Dd February 16, 2024 +.Dt SDIFF 1 +.Os +.Sh NAME +.Nm sdiff +.Nd side-by-side diff +.Sh SYNOPSIS +.Nm +.Op Fl abdilstHW +.Op Fl I Ar regexp +.Op Fl o Ar outfile +.Op Fl w Ar width +.Ar file1 +.Ar file2 +.Sh DESCRIPTION +.Nm +displays two files side by side, +with any differences between the two highlighted as follows: +new lines are marked with +.Sq \*(Gt ; +deleted lines are marked with +.Sq \*(Lt ; +and changed lines are marked with +.Sq \*(Ba . +.Pp +.Nm +can also be used to interactively merge two files, +prompting at each set of differences. +See the +.Fl o +option for an explanation. +.Pp +The options are: +.Bl -tag -width Ds +.It Fl l -left-column +Only print the left column for identical lines. +.It Fl o -output Ar outfile +Interactively merge +.Ar file1 +and +.Ar file2 +into +.Ar outfile . +In this mode, the user is prompted for each set of differences. +See +.Ev EDITOR +and +.Ev VISUAL , +below, +for details of which editor, if any, is invoked. +.Pp +The commands are as follows: +.Bl -tag -width Ds +.It Cm l | 1 +Choose left set of diffs. +.It Cm r | 2 +Choose right set of diffs. +.It Cm s +Silent mode \(en identical lines are not printed. +.It Cm v +Verbose mode \(en identical lines are printed. +.It Cm e +Start editing an empty file, which will be merged into +.Ar outfile +upon exiting the editor. +.It Cm e Cm l +Start editing file with left set of diffs. +.It Cm e Cm r +Start editing file with right set of diffs. +.It Cm e Cm b +Start editing file with both sets of diffs. +.It Cm q +Quit +.Nm . +.El +.It Fl s -suppress-common-lines +Skip identical lines. +.It Fl w -width Ar width +Print a maximum of +.Ar width +characters on each line. +The default is 130 characters. +.El +.Pp +Options passed to +.Xr diff 1 +are: +.Bl -tag -width Ds +.It Fl a -text +Treat +.Ar file1 +and +.Ar file2 +as text files. +.It Fl b -ignore-space-change +Ignore trailing blank spaces. +.It Fl d -minimal +Minimize diff size. +.It Fl I -ignore-matching-lines Ar regexp +Ignore line changes matching +.Ar regexp . +All lines in the change must match +.Ar regexp +for the change to be ignored. +.It Fl i -ignore-case +Do a case-insensitive comparison. +.It Fl t -expand-tabs +Expand tabs to spaces. +.It Fl W -ignore-all-space +Ignore all spaces. +.It Fl B -ignore-blank-lines +Ignore blank lines. +.It Fl E -ignore-tab-expansion +Treat tabs and eight spaces as the same. +.It Fl H -speed-large-files +Assume scattered small changes in a large file. +.It Fl -ignore-file-name-case +Ignore the case of file names. +.It Fl -no-ignore-file-name-case +Do not ignore file name case. +.It Fl -strip-trailing-cr +Skip identical lines. +.It Fl -tabsize Ar NUM +Change the size of tabs (default is 8.) +.El +.Sh ENVIRONMENT +.Bl -tag -width Ds +.It Ev EDITOR , VISUAL +Specifies an editor to use with the +.Fl o +option. +If both +.Ev EDITOR +and +.Ev VISUAL +are set, +.Ev VISUAL +takes precedence. +If neither +.Ev EDITOR +nor +.Ev VISUAL +are set, +the default is +.Xr vi 1 . +.It Ev TMPDIR +Specifies a directory for temporary files to be created. +The default is +.Pa /tmp . +.El +.Sh SEE ALSO +.Xr cmp 1 , +.Xr diff 1 , +.Xr diff3 1 , +.Xr vi 1 , +.Xr re_format 7 +.Sh AUTHORS +.Nm +was written from scratch for the public domain by +.An Ray Lai Aq ray@cyth.net . +.Sh CAVEATS +Tabs are treated as anywhere from one to eight characters wide, +depending on the current column. +Terminals that treat tabs as eight characters wide will look best. diff --git a/src/sdiff/sdiff.c b/src/sdiff/sdiff.c new file mode 100644 index 0000000..89a53b0 --- /dev/null +++ b/src/sdiff/sdiff.c @@ -0,0 +1,1178 @@ +/* $OpenBSD: sdiff.c,v 1.36 2015/12/29 19:04:46 gsoares Exp $ */ + +/* + * Written by Raymond Lai . + * Public domain. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "extern.h" + +static char diff_path[] = "/usr/bin/diff"; + +#define WIDTH 126 +/* + * Each column must be at least one character wide, plus three + * characters between the columns (space, [<|>], space). + */ +#define WIDTH_MIN 5 + +/* 3 kilobytes of chars */ +#define MAX_CHECK 768 + +/* A single diff line. */ +struct diffline { + STAILQ_ENTRY(diffline) diffentries; + char *left; + char div; + char *right; +}; + +static void astrcat(char **, const char *); +static void enqueue(char *, char, char *); +static char *mktmpcpy(const char *); +static int istextfile(FILE *); +static int bindiff(FILE *, char *, FILE *, char *); +static void freediff(struct diffline *); +static void int_usage(void); +static int parsecmd(FILE *, FILE *, FILE *); +static void printa(FILE *, size_t); +static void printc(FILE *, size_t, FILE *, size_t); +static void printcol(const char *, size_t *, const size_t); +static void printd(FILE *, size_t); +static void println(const char *, const char, const char *); +static void processq(void); +static void prompt(const char *, const char *); +static void usage(void) __dead2; +static char *xfgets(FILE *); + +static STAILQ_HEAD(, diffline) diffhead = STAILQ_HEAD_INITIALIZER(diffhead); +static size_t line_width; /* width of a line (two columns and divider) */ +static size_t width; /* width of each column */ +static size_t file1ln, file2ln; /* line number of file1 and file2 */ +static bool Iflag; /* ignore sets matching regexp */ +static bool lflag; /* print only left column for identical lines */ +static bool sflag; /* skip identical lines */ +static bool tflag; /* expand tabs */ +static int tabsize = 8; /* tab size */ +FILE *outfp; /* file to save changes to */ +const char *tmpdir; /* TMPDIR or /tmp */ + +enum { + HELP_OPT = CHAR_MAX + 1, + NORMAL_OPT, + FCASE_SENSITIVE_OPT, + FCASE_IGNORE_OPT, + STRIPCR_OPT, + TSIZE_OPT, + DIFFPROG_OPT, +}; + +static struct option longopts[] = { + /* options only processed in sdiff */ + { "suppress-common-lines", no_argument, NULL, 's' }, + { "width", required_argument, NULL, 'w' }, + + { "output", required_argument, NULL, 'o' }, + { "diff-program", required_argument, NULL, DIFFPROG_OPT }, + + /* Options processed by diff. */ + { "ignore-file-name-case", no_argument, NULL, FCASE_IGNORE_OPT }, + { "no-ignore-file-name-case", no_argument, NULL, FCASE_SENSITIVE_OPT }, + { "strip-trailing-cr", no_argument, NULL, STRIPCR_OPT }, + { "tabsize", required_argument, NULL, TSIZE_OPT }, + { "help", no_argument, NULL, HELP_OPT }, + { "text", no_argument, NULL, 'a' }, + { "ignore-blank-lines", no_argument, NULL, 'B' }, + { "ignore-space-change", no_argument, NULL, 'b' }, + { "minimal", no_argument, NULL, 'd' }, + { "ignore-tab-expansion", no_argument, NULL, 'E' }, + { "ignore-matching-lines", required_argument, NULL, 'I' }, + { "ignore-case", no_argument, NULL, 'i' }, + { "left-column", no_argument, NULL, 'l' }, + { "expand-tabs", no_argument, NULL, 't' }, + { "speed-large-files", no_argument, NULL, 'H' }, + { "ignore-all-space", no_argument, NULL, 'W' }, + + { NULL, 0, NULL, '\0'} +}; + +static const char *help_msg[] = { + "usage: sdiff [-abdilstW] [-I regexp] [-o outfile] [-w width] file1 file2\n", + "-l, --left-column: only print the left column for identical lines.", + "-o OUTFILE, --output=OUTFILE: interactively merge file1 and file2 into outfile.", + "-s, --suppress-common-lines: skip identical lines.", + "-w WIDTH, --width=WIDTH: print a maximum of WIDTH characters on each line.", + "", + "Options passed to diff(1) are:", + "\t-a, --text: treat file1 and file2 as text files.", + "\t-b, --ignore-trailing-cr: ignore trailing blank spaces.", + "\t-d, --minimal: minimize diff size.", + "\t-I RE, --ignore-matching-lines=RE: ignore changes whose line matches RE.", + "\t-i, --ignore-case: do a case-insensitive comparison.", + "\t-t, --expand-tabs: expand tabs to spaces.", + "\t-W, --ignore-all-space: ignore all whitespace.", + "\t--speed-large-files: assume large file with scattered changes.", + "\t--strip-trailing-cr: strip trailing carriage return.", + "\t--ignore-file-name-case: ignore case of file names.", + "\t--no-ignore-file-name-case: do not ignore file name case", + "\t--tabsize NUM: change size of tabs (default 8.)", + + NULL, +}; + +/* + * Create temporary file if source_file is not a regular file. + * Returns temporary file name if one was malloced, NULL if unnecessary. + */ +static char * +mktmpcpy(const char *source_file) +{ + struct stat sb; + ssize_t rcount; + int ifd, ofd; + u_char buf[BUFSIZ]; + char *target_file; + + /* Open input and output. */ + ifd = open(source_file, O_RDONLY, 0); + /* File was opened successfully. */ + if (ifd != -1) { + if (fstat(ifd, &sb) == -1) + err(2, "error getting file status from %s", source_file); + + /* Regular file. */ + if (S_ISREG(sb.st_mode)) { + close(ifd); + return (NULL); + } + } else { + /* If ``-'' does not exist the user meant stdin. */ + if (errno == ENOENT && strcmp(source_file, "-") == 0) + ifd = STDIN_FILENO; + else + err(2, "error opening %s", source_file); + } + + /* Not a regular file, so copy input into temporary file. */ + if (asprintf(&target_file, "%s/sdiff.XXXXXXXXXX", tmpdir) == -1) + err(2, "asprintf"); + if ((ofd = mkstemp(target_file)) == -1) { + warn("error opening %s", target_file); + goto FAIL; + } + while ((rcount = read(ifd, buf, sizeof(buf))) != -1 && + rcount != 0) { + ssize_t wcount; + + wcount = write(ofd, buf, (size_t)rcount); + if (-1 == wcount || rcount != wcount) { + warn("error writing to %s", target_file); + goto FAIL; + } + } + if (rcount == -1) { + warn("error reading from %s", source_file); + goto FAIL; + } + + close(ifd); + close(ofd); + + return (target_file); + +FAIL: + unlink(target_file); + exit(2); +} + +int +sdiff_main(int argc, char **argv) +{ + FILE *diffpipe, *file1, *file2; + size_t diffargc = 0, flagc = 0, wval = WIDTH; + int ch, fd[2], i, ret, status; + pid_t pid; + const char *errstr, *outfile = NULL; + char **diffargv, *diffprog = diff_path, *flagv; + char *filename1, *filename2, *tmp1, *tmp2, *s1, *s2; + char I_arg[] = "-I"; + char speed_lf[] = "--speed-large-files"; + + /* + * Process diff flags. + */ + /* + * Allocate memory for diff arguments and NULL. + * Each flag has at most one argument, so doubling argc gives an + * upper limit of how many diff args can be passed. argv[0], + * file1, and file2 won't have arguments so doubling them will + * waste some memory; however we need an extra space for the + * NULL at the end, so it sort of works out. + */ + if ((diffargv = calloc(argc, sizeof(char *) * 2)) == NULL) + err(2, NULL); + + /* Add first argument, the program name. */ + diffargv[diffargc++] = diffprog; + + /* create a dynamic string for merging single-character options */ + if ((flagv = malloc(flagc + 2)) == NULL) + err(2, NULL); + flagv[flagc] = '-'; + flagv[flagc + 1] = '\0'; + diffargv[diffargc++] = flagv; + + while ((ch = getopt_long(argc, argv, "aBbdEHI:ilo:stWw:", + longopts, NULL)) != -1) { + switch (ch) { + /* only compatible --long-name-form with diff */ + case FCASE_IGNORE_OPT: + case FCASE_SENSITIVE_OPT: + case STRIPCR_OPT: + case 'S': + break; + /* combine no-arg single switches */ + case 'a': + case 'B': + case 'b': + case 'd': + case 'E': + case 'i': + case 'W': + flagc++; + flagv = realloc(flagv, flagc + 2); + /* + * In diff, the 'W' option is 'w' and the 'w' is 'W'. + */ + flagv[flagc] = ch == 'W' ? 'w' : ch; + flagv[flagc + 1] = '\0'; + break; + case 'H': + diffargv[diffargc++] = speed_lf; + break; + case DIFFPROG_OPT: + diffargv[0] = diffprog = optarg; + break; + case 'I': + Iflag = true; + diffargv[diffargc++] = I_arg; + diffargv[diffargc++] = optarg; + break; + case 'l': + lflag = true; + break; + case 'o': + outfile = optarg; + break; + case 's': + sflag = true; + break; + case 't': + tflag = true; + break; + case 'w': + wval = strtonum(optarg, WIDTH_MIN, + INT_MAX, &errstr); + if (errstr) + errx(2, "width is %s: %s", errstr, optarg); + break; + case HELP_OPT: + for (i = 0; help_msg[i] != NULL; i++) + printf("%s\n", help_msg[i]); + exit(0); + break; + case TSIZE_OPT: + tabsize = strtonum(optarg, 1, INT_MAX, &errstr); + if (errstr) + errx(2, "tabsize is %s: %s", errstr, optarg); + break; + default: + usage(); + break; + } + } + + /* no single-character options were used */ + if (flagc == 0) { + memmove(diffargv + 1, diffargv + 2, + sizeof(char *) * (diffargc - 2)); + diffargc--; + free(flagv); + } + + argc -= optind; + argv += optind; + + if (argc != 2) + usage(); + + if (outfile && (outfp = fopen(outfile, "w")) == NULL) + err(2, "could not open: %s", optarg); + + if ((tmpdir = getenv("TMPDIR")) == NULL || *tmpdir == '\0') + tmpdir = _PATH_TMP; + + filename1 = argv[0]; + filename2 = argv[1]; + + /* + * Create temporary files for diff and sdiff to share if file1 + * or file2 are not regular files. This allows sdiff and diff + * to read the same inputs if one or both inputs are stdin. + * + * If any temporary files were created, their names would be + * saved in tmp1 or tmp2. tmp1 should never equal tmp2. + */ + tmp1 = tmp2 = NULL; + /* file1 and file2 are the same, so copy to same temp file. */ + if (strcmp(filename1, filename2) == 0) { + if ((tmp1 = mktmpcpy(filename1))) + filename1 = filename2 = tmp1; + /* Copy file1 and file2 into separate temp files. */ + } else { + if ((tmp1 = mktmpcpy(filename1))) + filename1 = tmp1; + if ((tmp2 = mktmpcpy(filename2))) + filename2 = tmp2; + } + + if ((file1 = fopen(filename1, "r")) == NULL) + err(2, "could not open %s", filename1); + if ((file2 = fopen(filename2, "r")) == NULL) + err(2, "could not open %s", filename2); + if (!istextfile(file1) || !istextfile(file2)) { + ret = bindiff(file1, filename1, file2, filename2); + goto done; + } + + diffargv[diffargc++] = filename1; + diffargv[diffargc++] = filename2; + /* Add NULL to end of array to indicate end of array. */ + diffargv[diffargc++] = NULL; + + /* Subtract column divider and divide by two. */ + width = (wval - 3) / 2; + /* Make sure line_width can fit in size_t. */ + if (width > (SIZE_MAX - 3) / 2) + errx(2, "width is too large: %zu", width); + line_width = width * 2 + 3; + + if (pipe(fd)) + err(2, "pipe"); + + if ((pid = fork()) < 0) + err(1, "fork()"); + if (pid == 0) { + /* child */ + /* We don't read from the pipe. */ + close(fd[0]); + if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO) + _exit(2); + /* Free unused descriptor. */ + close(fd[1]); + execvp(diffprog, diffargv); + _exit(2); + } + + /* parent */ + /* We don't write to the pipe. */ + close(fd[1]); + + /* Open pipe to diff command. */ + if ((diffpipe = fdopen(fd[0], "r")) == NULL) + err(2, "could not open diff pipe"); + + /* Line numbers start at one. */ + file1ln = file2ln = 1; + + /* Read and parse diff output. */ + while (parsecmd(diffpipe, file1, file2) != EOF) + ; + fclose(diffpipe); + + /* Wait for diff to exit. */ + if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status) || + WEXITSTATUS(status) >= 2) + errx(2, "diff exited abnormally"); + ret = WEXITSTATUS(status); + + /* No more diffs, so enqueue common lines. */ + if (lflag) + while ((s1 = xfgets(file1))) + enqueue(s1, ' ', NULL); + else + for (;;) { + s1 = xfgets(file1); + s2 = xfgets(file2); + if (s1 || s2) + enqueue(s1, ' ', s2); + else + break; + } + fclose(file1); + fclose(file2); + /* Process unmodified lines. */ + processq(); + +done: + /* Delete and free unneeded temporary files. */ + if (tmp1 != NULL) { + if (unlink(tmp1) != 0) + warn("failed to delete %s", tmp1); + free(tmp1); + } + if (tmp2 != NULL) { + if (unlink(tmp2) != 0) + warn("failed to delete %s", tmp2); + free(tmp2); + } + + /* Return diff exit status. */ + free(diffargv); + if (flagc > 0) + free(flagv); + return (ret); +} + +/* + * When sdiff detects a binary file as input. + */ +static int +bindiff(FILE *f1, char *fn1, FILE *f2, char *fn2) +{ + int ch1, ch2; + + flockfile(f1); + flockfile(f2); + do { + ch1 = getc_unlocked(f1); + ch2 = getc_unlocked(f2); + } while (ch1 != EOF && ch2 != EOF && ch1 == ch2); + funlockfile(f2); + funlockfile(f1); + if (ferror(f1)) { + warn("%s", fn1); + return (2); + } + if (ferror(f2)) { + warn("%s", fn2); + return (2); + } + if (ch1 != EOF || ch2 != EOF) { + printf("Binary files %s and %s differ\n", fn1, fn2); + return (1); + } + return (0); +} + +/* + * Checks whether a file appears to be a text file. + */ +static int +istextfile(FILE *f) +{ + int ch, i; + + if (f == NULL) + return (1); + rewind(f); + for (i = 0; i <= MAX_CHECK; i++) { + ch = fgetc(f); + if (ch == '\0') { + rewind(f); + return (0); + } + if (ch == EOF) + break; + } + rewind(f); + return (1); +} + +/* + * Prints an individual column (left or right), taking into account + * that tabs are variable-width. Takes a string, the current column + * the cursor is on the screen, and the maximum value of the column. + * The column value is updated as we go along. + */ +static void +printcol(const char *s, size_t *col, const size_t col_max) +{ + + for (; *s && *col < col_max; ++s) { + size_t new_col; + + switch (*s) { + case '\t': + /* + * If rounding to next multiple of eight causes + * an integer overflow, just return. + */ + if (*col > SIZE_MAX - tabsize) + return; + + /* Round to next multiple of eight. */ + new_col = (*col / tabsize + 1) * tabsize; + + /* + * If printing the tab goes past the column + * width, don't print it and just quit. + */ + if (new_col > col_max) + return; + + if (tflag) { + do { + putchar(' '); + } while (++*col < new_col); + } else { + putchar(*s); + *col = new_col; + } + break; + default: + ++*col; + putchar(*s); + } + } +} + +/* + * Prompts user to either choose between two strings or edit one, both, + * or neither. + */ +static void +prompt(const char *s1, const char *s2) +{ + char *cmd; + + /* Print command prompt. */ + putchar('%'); + + /* Get user input. */ + for (; (cmd = xfgets(stdin)); free(cmd)) { + const char *p; + + /* Skip leading whitespace. */ + for (p = cmd; isspace((unsigned char)*p); ++p) + ; + switch (*p) { + case 'e': + /* Skip `e'. */ + ++p; + if (eparse(p, s1, s2) == -1) + goto USAGE; + break; + case 'l': + case '1': + /* Choose left column as-is. */ + if (s1 != NULL) + fprintf(outfp, "%s\n", s1); + /* End of command parsing. */ + break; + case 'q': + goto QUIT; + case 'r': + case '2': + /* Choose right column as-is. */ + if (s2 != NULL) + fprintf(outfp, "%s\n", s2); + /* End of command parsing. */ + break; + case 's': + sflag = true; + goto PROMPT; + case 'v': + sflag = false; + /* FALLTHROUGH */ + default: + /* Interactive usage help. */ +USAGE: + int_usage(); +PROMPT: + putchar('%'); + + /* Prompt user again. */ + continue; + } + free(cmd); + return; + } + + /* + * If there was no error, we received an EOF from stdin, so we + * should quit. + */ +QUIT: + fclose(outfp); + exit(0); +} + +/* + * Takes two strings, separated by a column divider. NULL strings are + * treated as empty columns. If the divider is the ` ' character, the + * second column is not printed (-l flag). In this case, the second + * string must be NULL. When the second column is NULL, the divider + * does not print the trailing space following the divider character. + * + * Takes into account that tabs can take multiple columns. + */ +static void +println(const char *s1, const char divider, const char *s2) +{ + size_t col; + + /* Print first column. Skips if s1 == NULL. */ + col = 0; + if (s1) { + /* Skip angle bracket and space. */ + printcol(s1, &col, width); + + } + + /* Otherwise, we pad this column up to width. */ + for (; col < width; ++col) + putchar(' '); + + /* Only print left column. */ + if (divider == ' ' && !s2) { + printf(" (\n"); + return; + } + + /* + * Print column divider. If there is no second column, we don't + * need to add the space for padding. + */ + if (!s2) { + printf(" %c\n", divider); + return; + } + printf(" %c ", divider); + col += 3; + + /* Skip angle bracket and space. */ + printcol(s2, &col, line_width); + + putchar('\n'); +} + +/* + * Reads a line from file and returns as a string. If EOF is reached, + * NULL is returned. The returned string must be freed afterwards. + */ +static char * +xfgets(FILE *file) +{ + size_t linecap; + ssize_t l; + char *s; + + clearerr(file); + linecap = 0; + s = NULL; + + if ((l = getline(&s, &linecap, file)) == -1) { + if (ferror(file)) + err(2, "error reading file"); + return (NULL); + } + + if (s[l-1] == '\n') + s[l-1] = '\0'; + + return (s); +} + +/* + * Parse ed commands from diffpipe and print lines from file1 (lines + * to change or delete) or file2 (lines to add or change). + * Returns EOF or 0. + */ +static int +parsecmd(FILE *diffpipe, FILE *file1, FILE *file2) +{ + size_t file1start, file1end, file2start, file2end, n; + /* ed command line and pointer to characters in line */ + char *line, *p, *q; + const char *errstr; + char c, cmd; + + /* Read ed command. */ + if (!(line = xfgets(diffpipe))) + return (EOF); + + p = line; + /* Go to character after line number. */ + while (isdigit((unsigned char)*p)) + ++p; + c = *p; + *p++ = 0; + file1start = strtonum(line, 0, INT_MAX, &errstr); + if (errstr) + errx(2, "file1 start is %s: %s", errstr, line); + + /* A range is specified for file1. */ + if (c == ',') { + q = p; + /* Go to character after file2end. */ + while (isdigit((unsigned char)*p)) + ++p; + c = *p; + *p++ = 0; + file1end = strtonum(q, 0, INT_MAX, &errstr); + if (errstr) + errx(2, "file1 end is %s: %s", errstr, line); + if (file1start > file1end) + errx(2, "invalid line range in file1: %s", line); + } else + file1end = file1start; + + cmd = c; + /* Check that cmd is valid. */ + if (!(cmd == 'a' || cmd == 'c' || cmd == 'd')) + errx(2, "ed command not recognized: %c: %s", cmd, line); + + q = p; + /* Go to character after line number. */ + while (isdigit((unsigned char)*p)) + ++p; + c = *p; + *p++ = 0; + file2start = strtonum(q, 0, INT_MAX, &errstr); + if (errstr) + errx(2, "file2 start is %s: %s", errstr, line); + + /* + * There should either be a comma signifying a second line + * number or the line should just end here. + */ + if (c != ',' && c != '\0') + errx(2, "invalid line range in file2: %c: %s", c, line); + + if (c == ',') { + + file2end = strtonum(p, 0, INT_MAX, &errstr); + if (errstr) + errx(2, "file2 end is %s: %s", errstr, line); + if (file2start >= file2end) + errx(2, "invalid line range in file2: %s", line); + } else + file2end = file2start; + + /* Appends happen _after_ stated line. */ + if (cmd == 'a') { + if (file1start != file1end) + errx(2, "append cannot have a file1 range: %s", + line); + if (file1start == SIZE_MAX) + errx(2, "file1 line range too high: %s", line); + file1start = ++file1end; + } + /* + * I'm not sure what the deal is with the line numbers for + * deletes, though. + */ + else if (cmd == 'd') { + if (file2start != file2end) + errx(2, "delete cannot have a file2 range: %s", + line); + if (file2start == SIZE_MAX) + errx(2, "file2 line range too high: %s", line); + file2start = ++file2end; + } + + /* + * Continue reading file1 and file2 until we reach line numbers + * specified by diff. Should only happen with -I flag. + */ + for (; file1ln < file1start && file2ln < file2start; + ++file1ln, ++file2ln) { + char *s1, *s2; + + if (!(s1 = xfgets(file1))) + errx(2, "file1 shorter than expected"); + if (!(s2 = xfgets(file2))) + errx(2, "file2 shorter than expected"); + + /* If the -l flag was specified, print only left column. */ + if (lflag) { + free(s2); + /* + * XXX - If -l and -I are both specified, all + * unchanged or ignored lines are shown with a + * `(' divider. This matches GNU sdiff, but I + * believe it is a bug. Just check out: + * gsdiff -l -I '^$' samefile samefile. + */ + if (Iflag) + enqueue(s1, '(', NULL); + else + enqueue(s1, ' ', NULL); + } else + enqueue(s1, ' ', s2); + } + /* Ignore deleted lines. */ + for (; file1ln < file1start; ++file1ln) { + char *s; + + if (!(s = xfgets(file1))) + errx(2, "file1 shorter than expected"); + + enqueue(s, '(', NULL); + } + /* Ignore added lines. */ + for (; file2ln < file2start; ++file2ln) { + char *s; + + if (!(s = xfgets(file2))) + errx(2, "file2 shorter than expected"); + + /* If -l flag was given, don't print right column. */ + if (lflag) + free(s); + else + enqueue(NULL, ')', s); + } + + /* Process unmodified or skipped lines. */ + processq(); + + switch (cmd) { + case 'a': + printa(file2, file2end); + n = file2end - file2start + 1; + break; + case 'c': + printc(file1, file1end, file2, file2end); + n = file1end - file1start + 1 + 1 + file2end - file2start + 1; + break; + case 'd': + printd(file1, file1end); + n = file1end - file1start + 1; + break; + default: + errx(2, "invalid diff command: %c: %s", cmd, line); + } + free(line); + + /* Skip to next ed line. */ + while (n--) { + if (!(line = xfgets(diffpipe))) + errx(2, "diff ended early"); + free(line); + } + + return (0); +} + +/* + * Queues up a diff line. + */ +static void +enqueue(char *left, char divider, char *right) +{ + struct diffline *diffp; + + if (!(diffp = malloc(sizeof(struct diffline)))) + err(2, "enqueue"); + diffp->left = left; + diffp->div = divider; + diffp->right = right; + STAILQ_INSERT_TAIL(&diffhead, diffp, diffentries); +} + +/* + * Free a diffline structure and its elements. + */ +static void +freediff(struct diffline *diffp) +{ + + free(diffp->left); + free(diffp->right); + free(diffp); +} + +/* + * Append second string into first. Repeated appends to the same string + * are cached, making this an O(n) function, where n = strlen(append). + */ +static void +astrcat(char **s, const char *append) +{ + /* Length of string in previous run. */ + static size_t offset = 0; + size_t newsiz; + /* + * String from previous run. Compared to *s to see if we are + * dealing with the same string. If so, we can use offset. + */ + static const char *oldstr = NULL; + char *newstr; + + /* + * First string is NULL, so just copy append. + */ + if (!*s) { + if (!(*s = strdup(append))) + err(2, "astrcat"); + + /* Keep track of string. */ + offset = strlen(*s); + oldstr = *s; + + return; + } + + /* + * *s is a string so concatenate. + */ + + /* Did we process the same string in the last run? */ + /* + * If this is a different string from the one we just processed + * cache new string. + */ + if (oldstr != *s) { + offset = strlen(*s); + oldstr = *s; + } + + /* Size = strlen(*s) + \n + strlen(append) + '\0'. */ + newsiz = offset + 1 + strlen(append) + 1; + + /* Resize *s to fit new string. */ + newstr = realloc(*s, newsiz); + if (newstr == NULL) + err(2, "astrcat"); + *s = newstr; + + /* *s + offset should be end of string. */ + /* Concatenate. */ + strlcpy(*s + offset, "\n", newsiz - offset); + strlcat(*s + offset, append, newsiz - offset); + + /* New string length should be exactly newsiz - 1 characters. */ + /* Store generated string's values. */ + offset = newsiz - 1; + oldstr = *s; +} + +/* + * Process diff set queue, printing, prompting, and saving each diff + * line stored in queue. + */ +static void +processq(void) +{ + struct diffline *diffp; + char divc, *left, *right; + + /* Don't process empty queue. */ + if (STAILQ_EMPTY(&diffhead)) + return; + + /* Remember the divider. */ + divc = STAILQ_FIRST(&diffhead)->div; + + left = NULL; + right = NULL; + /* + * Go through set of diffs, concatenating each line in left or + * right column into two long strings, `left' and `right'. + */ + STAILQ_FOREACH(diffp, &diffhead, diffentries) { + /* + * Print changed lines if -s was given, + * print all lines if -s was not given. + */ + if (!sflag || diffp->div == '|' || diffp->div == '<' || + diffp->div == '>') + println(diffp->left, diffp->div, diffp->right); + + /* Append new lines to diff set. */ + if (diffp->left) + astrcat(&left, diffp->left); + if (diffp->right) + astrcat(&right, diffp->right); + } + + /* Empty queue and free each diff line and its elements. */ + while (!STAILQ_EMPTY(&diffhead)) { + diffp = STAILQ_FIRST(&diffhead); + STAILQ_REMOVE_HEAD(&diffhead, diffentries); + freediff(diffp); + } + + /* Write to outfp, prompting user if lines are different. */ + if (outfp) + switch (divc) { + case ' ': case '(': case ')': + fprintf(outfp, "%s\n", left); + break; + case '|': case '<': case '>': + prompt(left, right); + break; + default: + errx(2, "invalid divider: %c", divc); + } + + /* Free left and right. */ + free(left); + free(right); +} + +/* + * Print lines following an (a)ppend command. + */ +static void +printa(FILE *file, size_t line2) +{ + char *line; + + for (; file2ln <= line2; ++file2ln) { + if (!(line = xfgets(file))) + errx(2, "append ended early"); + enqueue(NULL, '>', line); + } + processq(); +} + +/* + * Print lines following a (c)hange command, from file1ln to file1end + * and from file2ln to file2end. + */ +static void +printc(FILE *file1, size_t file1end, FILE *file2, size_t file2end) +{ + struct fileline { + STAILQ_ENTRY(fileline) fileentries; + char *line; + }; + STAILQ_HEAD(, fileline) delqhead = STAILQ_HEAD_INITIALIZER(delqhead); + + /* Read lines to be deleted. */ + for (; file1ln <= file1end; ++file1ln) { + struct fileline *linep; + char *line1; + + /* Read lines from both. */ + if (!(line1 = xfgets(file1))) + errx(2, "error reading file1 in delete in change"); + + /* Add to delete queue. */ + if (!(linep = malloc(sizeof(struct fileline)))) + err(2, "printc"); + linep->line = line1; + STAILQ_INSERT_TAIL(&delqhead, linep, fileentries); + } + + /* Process changed lines.. */ + for (; !STAILQ_EMPTY(&delqhead) && file2ln <= file2end; + ++file2ln) { + struct fileline *del; + char *add; + + /* Get add line. */ + if (!(add = xfgets(file2))) + errx(2, "error reading add in change"); + + del = STAILQ_FIRST(&delqhead); + enqueue(del->line, '|', add); + STAILQ_REMOVE_HEAD(&delqhead, fileentries); + /* + * Free fileline structure but not its elements since + * they are queued up. + */ + free(del); + } + processq(); + + /* Process remaining lines to add. */ + for (; file2ln <= file2end; ++file2ln) { + char *add; + + /* Get add line. */ + if (!(add = xfgets(file2))) + errx(2, "error reading add in change"); + + enqueue(NULL, '>', add); + } + processq(); + + /* Process remaining lines to delete. */ + while (!STAILQ_EMPTY(&delqhead)) { + struct fileline *filep; + + filep = STAILQ_FIRST(&delqhead); + enqueue(filep->line, '<', NULL); + STAILQ_REMOVE_HEAD(&delqhead, fileentries); + free(filep); + } + processq(); +} + +/* + * Print deleted lines from file, from file1ln to file1end. + */ +static void +printd(FILE *file1, size_t file1end) +{ + char *line1; + + /* Print out lines file1ln to line2. */ + for (; file1ln <= file1end; ++file1ln) { + if (!(line1 = xfgets(file1))) + errx(2, "file1 ended early in delete"); + enqueue(line1, '<', NULL); + } + processq(); +} + +/* + * Interactive mode usage. + */ +static void +int_usage(void) +{ + + puts("e:\tedit blank diff\n" + "eb:\tedit both diffs concatenated\n" + "el:\tedit left diff\n" + "er:\tedit right diff\n" + "l | 1:\tchoose left diff\n" + "r | 2:\tchoose right diff\n" + "s:\tsilent mode--don't print identical lines\n" + "v:\tverbose mode--print identical lines\n" + "q:\tquit"); +} + +static void +usage(void) +{ + + fprintf(stderr, + "usage: sdiff [-abdilstHW] [-I regexp] [-o outfile] [-w width] file1" + " file2\n"); + exit(2); +} diff --git a/src/sdiff/tests/Makefile b/src/sdiff/tests/Makefile new file mode 100644 index 0000000..6d06996 --- /dev/null +++ b/src/sdiff/tests/Makefile @@ -0,0 +1,34 @@ + +PACKAGE= tests + +ATF_TESTS_SH= sdiff_test + +${PACKAGE}FILES+= \ + d_dot.in \ + d_flags_l.out \ + d_flags_s.out \ + d_flags_w.out \ + d_iflags_a1.out \ + d_iflags_a2.out \ + d_iflags_b1.out \ + d_iflags_b2.out \ + d_iflags_c1.out \ + d_iflags_c2.out \ + d_iflags_d1.out \ + d_iflags_d2.out \ + d_input1 \ + d_input2 \ + d_oneline.in \ + d_oneline_a.out \ + d_oneline_b.out \ + d_same.out \ + d_short.out \ + d_tabends.in \ + d_tabends_a.out \ + d_tabends_b.out \ + d_tabends_c.out \ + d_tabs.out \ + d_tabs1.in \ + d_tabs2.in + +.include diff --git a/src/sdiff/tests/Makefile.depend b/src/sdiff/tests/Makefile.depend new file mode 100644 index 0000000..11aba52 --- /dev/null +++ b/src/sdiff/tests/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/src/sdiff/tests/d_dot.in b/src/sdiff/tests/d_dot.in new file mode 100644 index 0000000..9c558e3 --- /dev/null +++ b/src/sdiff/tests/d_dot.in @@ -0,0 +1 @@ +. diff --git a/src/sdiff/tests/d_flags_l.out b/src/sdiff/tests/d_flags_l.out new file mode 100644 index 0000000..ac8d2ad --- /dev/null +++ b/src/sdiff/tests/d_flags_l.out @@ -0,0 +1,102 @@ +Policy: /usr/bin/lynx, Emulation: native ( + > native-issetugid: permit + > native-mprotect: permit + > native-mmap: permit + native-__sysctl: permit ( + > native-fsread: filename eq "/var/run/ld.so.hints" then pe + > native-fstat: permit + native-close: permit ( + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-fsread: filename match "/usr/lib/libssl.so.*" then + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-read: permit + native-exit: permit | native-fsread: filename match "/usr/lib/libcrypto.so.*" t + native-fcntl: cmd eq "F_SETFD" then permit | native-fsread: filename match "/usr/lib/libncurses.so.*" + native-fsread: filename eq "/" then permit | native-fsread: filename match "/usr/lib/libc.so.*" then p + native-fsread: filename match "/: | native-munmap: permit + native-fsread: filename eq "/etc/lynx.cfg" then permit | native-sigprocmask: permit + native-fsread: filename eq "/etc/malloc.conf" then permit ( + native-fsread: filename eq "/etc/resolv.conf" then permit | native-getpid: permit + native-fsread: filename eq "/etc/utmp" then permit < + native-fsread: filename eq "/home" then permit < + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "$HOME/.lynxrc" then permit < + native-fsread: filename eq "$HOME/.mailcap" then permit < + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "/obj" then permit < + native-fsread: filename eq "/tmp" then permit ( + > native-fswrite: filename match "/tmp/lynx-*" then permit + native-fsread: filename match "/tmp/lynx-*/." then permit ( + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "/etc/lynx.cfg" then permit + > native-fsread: filename eq "/" then permit + > native-fsread: filename eq "/usr/obj/bin/systrace/." then + > native-fsread: filename eq "/usr/obj/bin" then permit + > native-fcntl: permit + > native-getdirentries: permit + > native-lseek: permit + > native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "/usr" then permit ( + native-fsread: filename eq "/usr/bin" then permit ( + native-fsread: filename eq "/usr/games" then permit ( + native-fsread: filename eq "/usr/include" then permit ( + native-fsread: filename eq "/usr/lib" then permit ( + native-fsread: filename match "/usr/lib/libc.so.*" then p < + native-fsread: filename match "/usr/lib/libcrypto.so.*" t < + native-fsread: filename match "/usr/lib/libncurses.so.*" < + native-fsread: filename match "/usr/lib/libssl.so.*" then < + native-fsread: filename eq "/usr/libdata" then permit ( + native-fsread: filename eq "/usr/libexec" then permit ( + native-fsread: filename eq "/usr/lkm" then permit ( + native-fsread: filename eq "/usr/local" then permit ( + native-fsread: filename eq "/usr/mdec" then permit ( + native-fsread: filename eq "/usr/obj" then permit | native-fsread: filename eq "/home" then permit + native-fsread: filename eq "/usr/obj/bin" then permit | native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/." then | native-fsread: filename eq "$HOME/.lynxrc" then permit + > native-fsread: filename match "/: + native-fsread: filename eq "/usr/obj/bin/systrace/.mailca ( + > native-fsread: filename eq "$HOME/.mailcap" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t ( + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-sigaction: permit + > native-ioctl: permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "$HOME/.terminfo" then permit + native-fsread: filename eq "/usr/share/misc/terminfo.db" ( + > native-pread: permit + > native-write: permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + native-fsread: filename eq "/var/run/dev.db" then permit ( + native-fsread: filename eq "/var/run/ld.so.hints" then pe | native-fsread: filename eq "/etc/utmp" then permit + native-fstat: permit < + native-fswrite: filename match "/tmp/lynx-*" then permit < + native-getdirentries: permit < + native-getpid: permit < + native-gettimeofday: permit < + native-ioctl: permit < + native-issetugid: permit < + native-lseek: permit < + native-mmap: permit < + native-mprotect: prot eq "PROT_READ" then permit < + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi < + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm < + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" < + native-munmap: permit < + native-nanosleep: permit < + native-poll: permit ( + native-pread: permit | native-nanosleep: permit + native-read: permit | native-gettimeofday: permit + native-recvfrom: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + native-select: permit < + native-sendto: true then permit < + native-sigaction: permit < + native-sigprocmask: permit < + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK ( + > native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + > native-sendto: true then permit + > native-select: permit + > native-recvfrom: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK ( + native-write: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + > native-exit: permit diff --git a/src/sdiff/tests/d_flags_s.out b/src/sdiff/tests/d_flags_s.out new file mode 100644 index 0000000..8af11f3 --- /dev/null +++ b/src/sdiff/tests/d_flags_s.out @@ -0,0 +1,79 @@ + > native-issetugid: permit + > native-mprotect: permit + > native-mmap: permit + > native-fsread: filename eq "/var/run/ld.so.hints" then pe + > native-fstat: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-fsread: filename match "/usr/lib/libssl.so.*" then + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-read: permit + native-exit: permit | native-fsread: filename match "/usr/lib/libcrypto.so.*" t + native-fcntl: cmd eq "F_SETFD" then permit | native-fsread: filename match "/usr/lib/libncurses.so.*" + native-fsread: filename eq "/" then permit | native-fsread: filename match "/usr/lib/libc.so.*" then p + native-fsread: filename match "/: | native-munmap: permit + native-fsread: filename eq "/etc/lynx.cfg" then permit | native-sigprocmask: permit + native-fsread: filename eq "/etc/resolv.conf" then permit | native-getpid: permit + native-fsread: filename eq "/etc/utmp" then permit < + native-fsread: filename eq "/home" then permit < + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "$HOME/.lynxrc" then permit < + native-fsread: filename eq "$HOME/.mailcap" then permit < + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "/obj" then permit < + > native-fswrite: filename match "/tmp/lynx-*" then permit + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "/etc/lynx.cfg" then permit + > native-fsread: filename eq "/" then permit + > native-fsread: filename eq "/usr/obj/bin/systrace/." then + > native-fsread: filename eq "/usr/obj/bin" then permit + > native-fcntl: permit + > native-getdirentries: permit + > native-lseek: permit + > native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then p < + native-fsread: filename match "/usr/lib/libcrypto.so.*" t < + native-fsread: filename match "/usr/lib/libncurses.so.*" < + native-fsread: filename match "/usr/lib/libssl.so.*" then < + native-fsread: filename eq "/usr/obj" then permit | native-fsread: filename eq "/home" then permit + native-fsread: filename eq "/usr/obj/bin" then permit | native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/." then | native-fsread: filename eq "$HOME/.lynxrc" then permit + > native-fsread: filename match "/: + > native-fsread: filename eq "$HOME/.mailcap" then permit + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-sigaction: permit + > native-ioctl: permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "$HOME/.terminfo" then permit + > native-pread: permit + > native-write: permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + native-fsread: filename eq "/var/run/ld.so.hints" then pe | native-fsread: filename eq "/etc/utmp" then permit + native-fstat: permit < + native-fswrite: filename match "/tmp/lynx-*" then permit < + native-getdirentries: permit < + native-getpid: permit < + native-gettimeofday: permit < + native-ioctl: permit < + native-issetugid: permit < + native-lseek: permit < + native-mmap: permit < + native-mprotect: prot eq "PROT_READ" then permit < + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi < + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm < + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" < + native-munmap: permit < + native-nanosleep: permit < + native-pread: permit | native-nanosleep: permit + native-read: permit | native-gettimeofday: permit + native-recvfrom: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + native-select: permit < + native-sendto: true then permit < + native-sigaction: permit < + native-sigprocmask: permit < + > native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + > native-sendto: true then permit + > native-select: permit + > native-recvfrom: permit + native-write: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + > native-exit: permit diff --git a/src/sdiff/tests/d_flags_w.out b/src/sdiff/tests/d_flags_w.out new file mode 100644 index 0000000..19ea79b --- /dev/null +++ b/src/sdiff/tests/d_flags_w.out @@ -0,0 +1,102 @@ +Policy: /usr/bin/lynx, Emulation: native Policy: /usr/bin/lynx, Emulation: native + > native-issetugid: permit + > native-mprotect: permit + > native-mmap: permit + native-__sysctl: permit native-__sysctl: permit + > native-fsread: filename eq "/var/run/ld.so.hints" then pe + > native-fstat: permit + native-close: permit native-close: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-fsread: filename match "/usr/lib/libssl.so.*" then + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-read: permit + native-exit: permit | native-fsread: filename match "/usr/lib/libcrypto.so.*" t + native-fcntl: cmd eq "F_SETFD" then permit | native-fsread: filename match "/usr/lib/libncurses.so.*" + native-fsread: filename eq "/" then permit | native-fsread: filename match "/usr/lib/libc.so.*" then p + native-fsread: filename match "/: | native-munmap: permit + native-fsread: filename eq "/etc/lynx.cfg" then permit | native-sigprocmask: permit + native-fsread: filename eq "/etc/malloc.conf" then permit native-fsread: filename eq "/etc/malloc.conf" then permit + native-fsread: filename eq "/etc/resolv.conf" then permit | native-getpid: permit + native-fsread: filename eq "/etc/utmp" then permit < + native-fsread: filename eq "/home" then permit < + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "$HOME/.lynxrc" then permit < + native-fsread: filename eq "$HOME/.mailcap" then permit < + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "/obj" then permit < + native-fsread: filename eq "/tmp" then permit native-fsread: filename eq "/tmp" then permit + > native-fswrite: filename match "/tmp/lynx-*" then permit + native-fsread: filename match "/tmp/lynx-*/." then permit native-fsread: filename match "/tmp/lynx-*/." then permit + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "/etc/lynx.cfg" then permit + > native-fsread: filename eq "/" then permit + > native-fsread: filename eq "/usr/obj/bin/systrace/." then + > native-fsread: filename eq "/usr/obj/bin" then permit + > native-fcntl: permit + > native-getdirentries: permit + > native-lseek: permit + > native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "/usr" then permit native-fsread: filename eq "/usr" then permit + native-fsread: filename eq "/usr/bin" then permit native-fsread: filename eq "/usr/bin" then permit + native-fsread: filename eq "/usr/games" then permit native-fsread: filename eq "/usr/games" then permit + native-fsread: filename eq "/usr/include" then permit native-fsread: filename eq "/usr/include" then permit + native-fsread: filename eq "/usr/lib" then permit native-fsread: filename eq "/usr/lib" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then p < + native-fsread: filename match "/usr/lib/libcrypto.so.*" t < + native-fsread: filename match "/usr/lib/libncurses.so.*" < + native-fsread: filename match "/usr/lib/libssl.so.*" then < + native-fsread: filename eq "/usr/libdata" then permit native-fsread: filename eq "/usr/libdata" then permit + native-fsread: filename eq "/usr/libexec" then permit native-fsread: filename eq "/usr/libexec" then permit + native-fsread: filename eq "/usr/lkm" then permit native-fsread: filename eq "/usr/lkm" then permit + native-fsread: filename eq "/usr/local" then permit native-fsread: filename eq "/usr/local" then permit + native-fsread: filename eq "/usr/mdec" then permit native-fsread: filename eq "/usr/mdec" then permit + native-fsread: filename eq "/usr/obj" then permit | native-fsread: filename eq "/home" then permit + native-fsread: filename eq "/usr/obj/bin" then permit | native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/." then | native-fsread: filename eq "$HOME/.lynxrc" then permit + > native-fsread: filename match "/: + native-fsread: filename eq "/usr/obj/bin/systrace/.mailca native-fsread: filename eq "/usr/obj/bin/systrace/.mailca + > native-fsread: filename eq "$HOME/.mailcap" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-sigaction: permit + > native-ioctl: permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "$HOME/.terminfo" then permit + native-fsread: filename eq "/usr/share/misc/terminfo.db" native-fsread: filename eq "/usr/share/misc/terminfo.db" + > native-pread: permit + > native-write: permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + native-fsread: filename eq "/var/run/dev.db" then permit native-fsread: filename eq "/var/run/dev.db" then permit + native-fsread: filename eq "/var/run/ld.so.hints" then pe | native-fsread: filename eq "/etc/utmp" then permit + native-fstat: permit < + native-fswrite: filename match "/tmp/lynx-*" then permit < + native-getdirentries: permit < + native-getpid: permit < + native-gettimeofday: permit < + native-ioctl: permit < + native-issetugid: permit < + native-lseek: permit < + native-mmap: permit < + native-mprotect: prot eq "PROT_READ" then permit < + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi < + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm < + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" < + native-munmap: permit < + native-nanosleep: permit < + native-poll: permit native-poll: permit + native-pread: permit | native-nanosleep: permit + native-read: permit | native-gettimeofday: permit + native-recvfrom: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + native-select: permit < + native-sendto: true then permit < + native-sigaction: permit < + native-sigprocmask: permit < + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + > native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + > native-sendto: true then permit + > native-select: permit + > native-recvfrom: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + native-write: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + > native-exit: permit diff --git a/src/sdiff/tests/d_iflags_a1.out b/src/sdiff/tests/d_iflags_a1.out new file mode 100644 index 0000000..2a0f532 --- /dev/null +++ b/src/sdiff/tests/d_iflags_a1.out @@ -0,0 +1,100 @@ +Policy: /usr/bin/lynx, Emulation: native Policy: /usr/bin/lynx, Emulation: native + > native-issetugid: permit + > native-mprotect: permit + > native-mmap: permit + native-__sysctl: permit native-__sysctl: permit + > native-fsread: filename eq "/var/run/ld.so.hints" then pe + > native-fstat: permit + native-close: permit native-close: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-fsread: filename match "/usr/lib/libssl.so.*" then + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-read: permit + native-exit: permit | native-fsread: filename match "/usr/lib/libcrypto.so.*" t + native-fcntl: cmd eq "F_SETFD" then permit | native-fsread: filename match "/usr/lib/libncurses.so.*" + native-fsread: filename eq "/" then permit | native-fsread: filename match "/usr/lib/libc.so.*" then p + native-fsread: filename match "/: | native-munmap: permit + native-fsread: filename eq "/etc/lynx.cfg" then permit | native-sigprocmask: permit + native-fsread: filename eq "/etc/malloc.conf" then permit native-fsread: filename eq "/etc/malloc.conf" then permit + native-fsread: filename eq "/etc/resolv.conf" then permit | native-getpid: permit + native-fsread: filename eq "/etc/utmp" then permit < + native-fsread: filename eq "/home" then permit < + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "$HOME/.lynxrc" then permit < + native-fsread: filename eq "$HOME/.mailcap" then permit < + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "/obj" then permit < + native-fsread: filename eq "/tmp" then permit native-fsread: filename eq "/tmp" then permit + native-fsread: filename match "/tmp/lynx-*/." then permit native-fswrite: filename match "/tmp/lynx-*" then permit + ) native-fsread: filename match "/tmp/lynx-*/." then permit + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "/etc/lynx.cfg" then permit + > native-fsread: filename eq "/" then permit + > native-fsread: filename eq "/usr/obj/bin/systrace/." then + > native-fsread: filename eq "/usr/obj/bin" then permit + > native-fcntl: permit + > native-getdirentries: permit + > native-lseek: permit + > native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "/usr" then permit native-fsread: filename eq "/usr" then permit + native-fsread: filename eq "/usr/bin" then permit native-fsread: filename eq "/usr/bin" then permit + native-fsread: filename eq "/usr/games" then permit native-fsread: filename eq "/usr/games" then permit + native-fsread: filename eq "/usr/include" then permit native-fsread: filename eq "/usr/include" then permit + native-fsread: filename eq "/usr/lib" then permit native-fsread: filename eq "/usr/lib" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then p native-fsread: filename eq "/usr/libdata" then permit + native-fsread: filename match "/usr/lib/libcrypto.so.*" t native-fsread: filename eq "/usr/libexec" then permit + native-fsread: filename match "/usr/lib/libncurses.so.*" native-fsread: filename eq "/usr/lkm" then permit + native-fsread: filename match "/usr/lib/libssl.so.*" then native-fsread: filename eq "/usr/local" then permit + native-fsread: filename eq "/usr/libdata" then permit native-fsread: filename eq "/usr/mdec" then permit + native-fsread: filename eq "/usr/libexec" then permit native-fsread: filename eq "/home" then permit + native-fsread: filename eq "/usr/lkm" then permit native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "/usr/local" then permit native-fsread: filename eq "$HOME/.lynxrc" then permit + native-fsread: filename eq "/usr/mdec" then permit native-fsread: filename match "/: + native-fsread: filename eq "/usr/obj" then permit native-fsread: filename eq "/usr/obj/bin/systrace/.mailca + native-fsread: filename eq "/usr/obj/bin" then permit native-fsread: filename eq "$HOME/.mailcap" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/." then native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t + native-fsread: filename eq "/usr/obj/bin/systrace/.mailca ( + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t ( + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-sigaction: permit + > native-ioctl: permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "$HOME/.terminfo" then permit + native-fsread: filename eq "/usr/share/misc/terminfo.db" native-fsread: filename eq "/usr/share/misc/terminfo.db" + > native-pread: permit + > native-write: permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + native-fsread: filename eq "/var/run/dev.db" then permit native-fsread: filename eq "/var/run/dev.db" then permit + native-fsread: filename eq "/var/run/ld.so.hints" then pe | native-fsread: filename eq "/etc/utmp" then permit + native-fstat: permit < + native-fswrite: filename match "/tmp/lynx-*" then permit < + native-getdirentries: permit < + native-getpid: permit < + native-gettimeofday: permit < + native-ioctl: permit < + native-issetugid: permit < + native-lseek: permit < + native-mmap: permit < + native-mprotect: prot eq "PROT_READ" then permit < + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi < + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm < + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" < + native-munmap: permit < + native-nanosleep: permit < + native-poll: permit native-poll: permit + native-pread: permit | native-nanosleep: permit + native-read: permit | native-gettimeofday: permit + native-recvfrom: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + native-select: permit < + native-sendto: true then permit < + native-sigaction: permit < + native-sigprocmask: permit < + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + > native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + > native-sendto: true then permit + > native-select: permit + > native-recvfrom: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + native-write: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + > native-exit: permit diff --git a/src/sdiff/tests/d_iflags_a2.out b/src/sdiff/tests/d_iflags_a2.out new file mode 100644 index 0000000..4e0d349 --- /dev/null +++ b/src/sdiff/tests/d_iflags_a2.out @@ -0,0 +1,96 @@ +Policy: /usr/bin/lynx, Emulation: native Policy: /usr/bin/lynx, Emulation: native + native-issetugid: permit < + native-mprotect: permit < + native-mmap: permit < + native-__sysctl: permit native-__sysctl: permit + native-fsread: filename eq "/var/run/ld.so.hints" then pe < + native-fstat: permit < + native-close: permit native-close: permit + native-fsread: filename match "/usr/lib/libssl.so.*" then | native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + native-read: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + native-fsread: filename match "/usr/lib/libcrypto.so.*" t | native-exit: permit + native-fsread: filename match "/usr/lib/libncurses.so.*" | native-fcntl: cmd eq "F_SETFD" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then p | native-fsread: filename eq "/" then permit + native-munmap: permit | native-fsread: filename match "/: + native-sigprocmask: permit | native-fsread: filename eq "/etc/lynx.cfg" then permit + native-fsread: filename eq "/etc/malloc.conf" then permit native-fsread: filename eq "/etc/malloc.conf" then permit + native-getpid: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + > native-fsread: filename eq "/etc/utmp" then permit + > native-fsread: filename eq "/home" then permit + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + > native-fsread: filename eq "$HOME/.lynxrc" then permit + > native-fsread: filename eq "$HOME/.mailcap" then permit + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-fsread: filename eq "$HOME/.terminfo" then permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "/tmp" then permit native-fsread: filename eq "/tmp" then permit + native-fswrite: filename match "/tmp/lynx-*" then permit native-fsread: filename match "/tmp/lynx-*/." then permit + native-fsread: filename match "/tmp/lynx-*/." then permit ( + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "/etc/lynx.cfg" then permit < + native-fsread: filename eq "/" then permit < + native-fsread: filename eq "/usr/obj/bin/systrace/." then < + native-fsread: filename eq "/usr/obj/bin" then permit < + native-fcntl: permit < + native-getdirentries: permit < + native-lseek: permit < + native-fsread: filename eq "/usr/obj" then permit < + native-fsread: filename eq "/usr" then permit native-fsread: filename eq "/usr" then permit + native-fsread: filename eq "/usr/bin" then permit native-fsread: filename eq "/usr/bin" then permit + native-fsread: filename eq "/usr/games" then permit native-fsread: filename eq "/usr/games" then permit + native-fsread: filename eq "/usr/include" then permit native-fsread: filename eq "/usr/include" then permit + native-fsread: filename eq "/usr/lib" then permit native-fsread: filename eq "/usr/lib" then permit + native-fsread: filename eq "/usr/libdata" then permit native-fsread: filename match "/usr/lib/libc.so.*" then p + native-fsread: filename eq "/usr/libexec" then permit native-fsread: filename match "/usr/lib/libcrypto.so.*" t + native-fsread: filename eq "/usr/lkm" then permit native-fsread: filename match "/usr/lib/libncurses.so.*" + native-fsread: filename eq "/usr/local" then permit native-fsread: filename match "/usr/lib/libssl.so.*" then + native-fsread: filename eq "/usr/mdec" then permit native-fsread: filename eq "/usr/libdata" then permit + native-fsread: filename eq "/home" then permit native-fsread: filename eq "/usr/libexec" then permit + native-fsread: filename eq "/obj" then permit native-fsread: filename eq "/usr/lkm" then permit + native-fsread: filename eq "$HOME/.lynxrc" then permit native-fsread: filename eq "/usr/local" then permit + native-fsread: filename match "/: native-fsread: filename eq "/usr/mdec" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mailca native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "$HOME/.mailcap" then permit native-fsread: filename eq "/usr/obj/bin" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t native-fsread: filename eq "/usr/obj/bin/systrace/." then + ) native-fsread: filename eq "/usr/obj/bin/systrace/.mailca + ) native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-sigaction: permit < + native-ioctl: permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-fsread: filename eq "/usr/share/misc/terminfo.db" native-fsread: filename eq "/usr/share/misc/terminfo.db" + native-pread: permit < + native-write: permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "/var/run/dev.db" then permit native-fsread: filename eq "/var/run/dev.db" then permit + native-fsread: filename eq "/etc/utmp" then permit | native-fsread: filename eq "/var/run/ld.so.hints" then pe + native-poll: permit | native-fstat: permit + native-nanosleep: permit | native-fswrite: filename match "/tmp/lynx-*" then permit + > native-getdirentries: permit + > native-getpid: permit + native-gettimeofday: permit native-gettimeofday: permit + native-fsread: filename eq "/etc/resolv.conf" then permit | native-ioctl: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK | native-issetugid: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-lseek: permit + native-sendto: true then permit | native-mmap: permit + native-select: permit | native-mprotect: prot eq "PROT_READ" then permit + > native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi + > native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm + > native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" + > native-munmap: permit + > native-nanosleep: permit + > native-poll: permit + > native-pread: permit + > native-read: permit + native-recvfrom: permit native-recvfrom: permit + > native-select: permit + > native-sendto: true then permit + > native-sigaction: permit + > native-sigprocmask: permit + > native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-write: permit + native-exit: permit < diff --git a/src/sdiff/tests/d_iflags_b1.out b/src/sdiff/tests/d_iflags_b1.out new file mode 100644 index 0000000..3e548b6 --- /dev/null +++ b/src/sdiff/tests/d_iflags_b1.out @@ -0,0 +1,69 @@ + > native-issetugid: permit + > native-mprotect: permit + > native-mmap: permit + > native-fsread: filename eq "/var/run/ld.so.hints" then pe + > native-fstat: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-fsread: filename match "/usr/lib/libssl.so.*" then + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-read: permit + native-exit: permit | native-fsread: filename match "/usr/lib/libcrypto.so.*" t + native-fcntl: cmd eq "F_SETFD" then permit | native-fsread: filename match "/usr/lib/libncurses.so.*" + native-fsread: filename eq "/" then permit | native-fsread: filename match "/usr/lib/libc.so.*" then p + native-fsread: filename match "/: | native-munmap: permit + native-fsread: filename eq "/etc/lynx.cfg" then permit | native-sigprocmask: permit + native-fsread: filename eq "/etc/resolv.conf" then permit | native-getpid: permit + native-fsread: filename eq "/etc/utmp" then permit < + native-fsread: filename eq "/home" then permit < + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "$HOME/.lynxrc" then permit < + native-fsread: filename eq "$HOME/.mailcap" then permit < + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "/obj" then permit < + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "/etc/lynx.cfg" then permit + > native-fsread: filename eq "/" then permit + > native-fsread: filename eq "/usr/obj/bin/systrace/." then + > native-fsread: filename eq "/usr/obj/bin" then permit + > native-fcntl: permit + > native-getdirentries: permit + > native-lseek: permit + > native-fsread: filename eq "/usr/obj" then permit + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-sigaction: permit + > native-ioctl: permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "$HOME/.terminfo" then permit + > native-pread: permit + > native-write: permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + native-fsread: filename eq "/var/run/ld.so.hints" then pe | native-fsread: filename eq "/etc/utmp" then permit + native-fstat: permit < + native-fswrite: filename match "/tmp/lynx-*" then permit < + native-getdirentries: permit < + native-getpid: permit < + native-gettimeofday: permit < + native-ioctl: permit < + native-issetugid: permit < + native-lseek: permit < + native-mmap: permit < + native-mprotect: prot eq "PROT_READ" then permit < + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi < + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm < + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" < + native-munmap: permit < + native-nanosleep: permit < + native-pread: permit | native-nanosleep: permit + native-read: permit | native-gettimeofday: permit + native-recvfrom: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + native-select: permit < + native-sendto: true then permit < + native-sigaction: permit < + native-sigprocmask: permit < + > native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + > native-sendto: true then permit + > native-select: permit + > native-recvfrom: permit + native-write: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + > native-exit: permit diff --git a/src/sdiff/tests/d_iflags_b2.out b/src/sdiff/tests/d_iflags_b2.out new file mode 100644 index 0000000..4504c36 --- /dev/null +++ b/src/sdiff/tests/d_iflags_b2.out @@ -0,0 +1,65 @@ + native-issetugid: permit < + native-mprotect: permit < + native-mmap: permit < + native-fsread: filename eq "/var/run/ld.so.hints" then pe < + native-fstat: permit < + native-fsread: filename match "/usr/lib/libssl.so.*" then | native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + native-read: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + native-fsread: filename match "/usr/lib/libcrypto.so.*" t | native-exit: permit + native-fsread: filename match "/usr/lib/libncurses.so.*" | native-fcntl: cmd eq "F_SETFD" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then p | native-fsread: filename eq "/" then permit + native-munmap: permit | native-fsread: filename match "/: + native-sigprocmask: permit | native-fsread: filename eq "/etc/lynx.cfg" then permit + native-getpid: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + > native-fsread: filename eq "/etc/utmp" then permit + > native-fsread: filename eq "/home" then permit + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + > native-fsread: filename eq "$HOME/.lynxrc" then permit + > native-fsread: filename eq "$HOME/.mailcap" then permit + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-fsread: filename eq "$HOME/.terminfo" then permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "/etc/lynx.cfg" then permit < + native-fsread: filename eq "/" then permit < + native-fsread: filename eq "/usr/obj/bin/systrace/." then < + native-fsread: filename eq "/usr/obj/bin" then permit < + native-fcntl: permit < + native-getdirentries: permit < + native-lseek: permit < + native-fsread: filename eq "/usr/obj" then permit < + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-sigaction: permit < + native-ioctl: permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-pread: permit < + native-write: permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "/etc/utmp" then permit | native-fsread: filename eq "/var/run/ld.so.hints" then pe + native-poll: permit | native-fstat: permit + native-nanosleep: permit | native-fswrite: filename match "/tmp/lynx-*" then permit + > native-getdirentries: permit + > native-getpid: permit + native-fsread: filename eq "/etc/resolv.conf" then permit | native-ioctl: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK | native-issetugid: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-lseek: permit + native-sendto: true then permit | native-mmap: permit + native-select: permit | native-mprotect: prot eq "PROT_READ" then permit + > native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi + > native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm + > native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" + > native-munmap: permit + > native-nanosleep: permit + > native-poll: permit + > native-pread: permit + > native-read: permit + > native-select: permit + > native-sendto: true then permit + > native-sigaction: permit + > native-sigprocmask: permit + > native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-write: permit + native-exit: permit < diff --git a/src/sdiff/tests/d_iflags_c1.out b/src/sdiff/tests/d_iflags_c1.out new file mode 100644 index 0000000..e9ac88e --- /dev/null +++ b/src/sdiff/tests/d_iflags_c1.out @@ -0,0 +1,99 @@ +Policy: /usr/bin/lynx, Emulation: native ( + > native-issetugid: permit + > native-mprotect: permit + > native-mmap: permit + native-__sysctl: permit ( + > native-fsread: filename eq "/var/run/ld.so.hints" then pe + > native-fstat: permit + native-close: permit ( + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-fsread: filename match "/usr/lib/libssl.so.*" then + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-read: permit + native-exit: permit | native-fsread: filename match "/usr/lib/libcrypto.so.*" t + native-fcntl: cmd eq "F_SETFD" then permit | native-fsread: filename match "/usr/lib/libncurses.so.*" + native-fsread: filename eq "/" then permit | native-fsread: filename match "/usr/lib/libc.so.*" then p + native-fsread: filename match "/: | native-munmap: permit + native-fsread: filename eq "/etc/lynx.cfg" then permit | native-sigprocmask: permit + native-fsread: filename eq "/etc/malloc.conf" then permit ( + native-fsread: filename eq "/etc/resolv.conf" then permit | native-getpid: permit + native-fsread: filename eq "/etc/utmp" then permit < + native-fsread: filename eq "/home" then permit < + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "$HOME/.lynxrc" then permit < + native-fsread: filename eq "$HOME/.mailcap" then permit < + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "/obj" then permit < + native-fsread: filename eq "/tmp" then permit ( + native-fsread: filename match "/tmp/lynx-*/." then permit ( + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "/etc/lynx.cfg" then permit + > native-fsread: filename eq "/" then permit + > native-fsread: filename eq "/usr/obj/bin/systrace/." then + > native-fsread: filename eq "/usr/obj/bin" then permit + > native-fcntl: permit + > native-getdirentries: permit + > native-lseek: permit + > native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "/usr" then permit ( + native-fsread: filename eq "/usr/bin" then permit ( + native-fsread: filename eq "/usr/games" then permit ( + native-fsread: filename eq "/usr/include" then permit ( + native-fsread: filename eq "/usr/lib" then permit ( + native-fsread: filename match "/usr/lib/libc.so.*" then p ( + native-fsread: filename match "/usr/lib/libcrypto.so.*" t ( + native-fsread: filename match "/usr/lib/libncurses.so.*" ( + native-fsread: filename match "/usr/lib/libssl.so.*" then ( + native-fsread: filename eq "/usr/libdata" then permit ( + native-fsread: filename eq "/usr/libexec" then permit ( + native-fsread: filename eq "/usr/lkm" then permit ( + native-fsread: filename eq "/usr/local" then permit ( + native-fsread: filename eq "/usr/mdec" then permit ( + native-fsread: filename eq "/usr/obj" then permit ( + native-fsread: filename eq "/usr/obj/bin" then permit ( + native-fsread: filename eq "/usr/obj/bin/systrace/." then ( + native-fsread: filename eq "/usr/obj/bin/systrace/.mailca ( + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t ( + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-sigaction: permit + > native-ioctl: permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "$HOME/.terminfo" then permit + native-fsread: filename eq "/usr/share/misc/terminfo.db" ( + > native-pread: permit + > native-write: permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + native-fsread: filename eq "/var/run/dev.db" then permit ( + native-fsread: filename eq "/var/run/ld.so.hints" then pe | native-fsread: filename eq "/etc/utmp" then permit + native-fstat: permit < + native-fswrite: filename match "/tmp/lynx-*" then permit < + native-getdirentries: permit < + native-getpid: permit < + native-gettimeofday: permit < + native-ioctl: permit < + native-issetugid: permit < + native-lseek: permit < + native-mmap: permit < + native-mprotect: prot eq "PROT_READ" then permit < + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi < + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm < + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" < + native-munmap: permit < + native-nanosleep: permit < + native-poll: permit ( + native-pread: permit | native-nanosleep: permit + native-read: permit | native-gettimeofday: permit + native-recvfrom: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + native-select: permit < + native-sendto: true then permit < + native-sigaction: permit < + native-sigprocmask: permit < + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK ( + > native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + > native-sendto: true then permit + > native-select: permit + > native-recvfrom: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK ( + native-write: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + > native-exit: permit diff --git a/src/sdiff/tests/d_iflags_c2.out b/src/sdiff/tests/d_iflags_c2.out new file mode 100644 index 0000000..fe7af0e --- /dev/null +++ b/src/sdiff/tests/d_iflags_c2.out @@ -0,0 +1,94 @@ +Policy: /usr/bin/lynx, Emulation: native ( + native-issetugid: permit < + native-mprotect: permit < + native-mmap: permit < + native-__sysctl: permit ( + native-fsread: filename eq "/var/run/ld.so.hints" then pe < + native-fstat: permit < + native-close: permit ( + native-fsread: filename match "/usr/lib/libssl.so.*" then | native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + native-read: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + native-fsread: filename match "/usr/lib/libcrypto.so.*" t | native-exit: permit + native-fsread: filename match "/usr/lib/libncurses.so.*" | native-fcntl: cmd eq "F_SETFD" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then p | native-fsread: filename eq "/" then permit + native-munmap: permit | native-fsread: filename match "/: + native-sigprocmask: permit | native-fsread: filename eq "/etc/lynx.cfg" then permit + native-fsread: filename eq "/etc/malloc.conf" then permit ( + native-getpid: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + > native-fsread: filename eq "/etc/utmp" then permit + > native-fsread: filename eq "/home" then permit + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + > native-fsread: filename eq "$HOME/.lynxrc" then permit + > native-fsread: filename eq "$HOME/.mailcap" then permit + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-fsread: filename eq "$HOME/.terminfo" then permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "/tmp" then permit ( + native-fswrite: filename match "/tmp/lynx-*" then permit ( + native-fsread: filename match "/tmp/lynx-*/." then permit ( + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "/etc/lynx.cfg" then permit < + native-fsread: filename eq "/" then permit < + native-fsread: filename eq "/usr/obj/bin/systrace/." then < + native-fsread: filename eq "/usr/obj/bin" then permit < + native-fcntl: permit < + native-getdirentries: permit < + native-lseek: permit < + native-fsread: filename eq "/usr/obj" then permit < + native-fsread: filename eq "/usr" then permit ( + native-fsread: filename eq "/usr/bin" then permit ( + native-fsread: filename eq "/usr/games" then permit ( + native-fsread: filename eq "/usr/include" then permit ( + native-fsread: filename eq "/usr/lib" then permit ( + native-fsread: filename eq "/usr/libdata" then permit ( + native-fsread: filename eq "/usr/libexec" then permit ( + native-fsread: filename eq "/usr/lkm" then permit ( + native-fsread: filename eq "/usr/local" then permit ( + native-fsread: filename eq "/usr/mdec" then permit ( + native-fsread: filename eq "/home" then permit ( + native-fsread: filename eq "/obj" then permit ( + native-fsread: filename eq "$HOME/.lynxrc" then permit ( + native-fsread: filename match "/: ( + native-fsread: filename eq "/usr/obj/bin/systrace/.mailca ( + native-fsread: filename eq "$HOME/.mailcap" then permit ( + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t ( + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-sigaction: permit < + native-ioctl: permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-fsread: filename eq "/usr/share/misc/terminfo.db" ( + native-pread: permit < + native-write: permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "/var/run/dev.db" then permit ( + native-fsread: filename eq "/etc/utmp" then permit | native-fsread: filename eq "/var/run/ld.so.hints" then pe + native-poll: permit | native-fstat: permit + native-nanosleep: permit | native-fswrite: filename match "/tmp/lynx-*" then permit + > native-getdirentries: permit + > native-getpid: permit + native-gettimeofday: permit ( + native-fsread: filename eq "/etc/resolv.conf" then permit | native-ioctl: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK | native-issetugid: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-lseek: permit + native-sendto: true then permit | native-mmap: permit + native-select: permit | native-mprotect: prot eq "PROT_READ" then permit + > native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi + > native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm + > native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" + > native-munmap: permit + > native-nanosleep: permit + > native-poll: permit + > native-pread: permit + > native-read: permit + native-recvfrom: permit ( + > native-select: permit + > native-sendto: true then permit + > native-sigaction: permit + > native-sigprocmask: permit + > native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK ( + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-write: permit + native-exit: permit < diff --git a/src/sdiff/tests/d_iflags_d1.out b/src/sdiff/tests/d_iflags_d1.out new file mode 100644 index 0000000..3e548b6 --- /dev/null +++ b/src/sdiff/tests/d_iflags_d1.out @@ -0,0 +1,69 @@ + > native-issetugid: permit + > native-mprotect: permit + > native-mmap: permit + > native-fsread: filename eq "/var/run/ld.so.hints" then pe + > native-fstat: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-fsread: filename match "/usr/lib/libssl.so.*" then + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-read: permit + native-exit: permit | native-fsread: filename match "/usr/lib/libcrypto.so.*" t + native-fcntl: cmd eq "F_SETFD" then permit | native-fsread: filename match "/usr/lib/libncurses.so.*" + native-fsread: filename eq "/" then permit | native-fsread: filename match "/usr/lib/libc.so.*" then p + native-fsread: filename match "/: | native-munmap: permit + native-fsread: filename eq "/etc/lynx.cfg" then permit | native-sigprocmask: permit + native-fsread: filename eq "/etc/resolv.conf" then permit | native-getpid: permit + native-fsread: filename eq "/etc/utmp" then permit < + native-fsread: filename eq "/home" then permit < + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "$HOME/.lynxrc" then permit < + native-fsread: filename eq "$HOME/.mailcap" then permit < + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "/obj" then permit < + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "/etc/lynx.cfg" then permit + > native-fsread: filename eq "/" then permit + > native-fsread: filename eq "/usr/obj/bin/systrace/." then + > native-fsread: filename eq "/usr/obj/bin" then permit + > native-fcntl: permit + > native-getdirentries: permit + > native-lseek: permit + > native-fsread: filename eq "/usr/obj" then permit + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-sigaction: permit + > native-ioctl: permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "$HOME/.terminfo" then permit + > native-pread: permit + > native-write: permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + native-fsread: filename eq "/var/run/ld.so.hints" then pe | native-fsread: filename eq "/etc/utmp" then permit + native-fstat: permit < + native-fswrite: filename match "/tmp/lynx-*" then permit < + native-getdirentries: permit < + native-getpid: permit < + native-gettimeofday: permit < + native-ioctl: permit < + native-issetugid: permit < + native-lseek: permit < + native-mmap: permit < + native-mprotect: prot eq "PROT_READ" then permit < + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi < + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm < + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" < + native-munmap: permit < + native-nanosleep: permit < + native-pread: permit | native-nanosleep: permit + native-read: permit | native-gettimeofday: permit + native-recvfrom: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + native-select: permit < + native-sendto: true then permit < + native-sigaction: permit < + native-sigprocmask: permit < + > native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + > native-sendto: true then permit + > native-select: permit + > native-recvfrom: permit + native-write: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + > native-exit: permit diff --git a/src/sdiff/tests/d_iflags_d2.out b/src/sdiff/tests/d_iflags_d2.out new file mode 100644 index 0000000..4504c36 --- /dev/null +++ b/src/sdiff/tests/d_iflags_d2.out @@ -0,0 +1,65 @@ + native-issetugid: permit < + native-mprotect: permit < + native-mmap: permit < + native-fsread: filename eq "/var/run/ld.so.hints" then pe < + native-fstat: permit < + native-fsread: filename match "/usr/lib/libssl.so.*" then | native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + native-read: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + native-fsread: filename match "/usr/lib/libcrypto.so.*" t | native-exit: permit + native-fsread: filename match "/usr/lib/libncurses.so.*" | native-fcntl: cmd eq "F_SETFD" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then p | native-fsread: filename eq "/" then permit + native-munmap: permit | native-fsread: filename match "/: + native-sigprocmask: permit | native-fsread: filename eq "/etc/lynx.cfg" then permit + native-getpid: permit | native-fsread: filename eq "/etc/resolv.conf" then permit + > native-fsread: filename eq "/etc/utmp" then permit + > native-fsread: filename eq "/home" then permit + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then per + > native-fsread: filename eq "$HOME/.lynxrc" then permit + > native-fsread: filename eq "$HOME/.mailcap" then permit + > native-fsread: filename eq "$HOME/.mime.types" then permi + > native-fsread: filename eq "$HOME/.terminfo" then permit + > native-fsread: filename eq "$HOME/.terminfo.db" then perm + > native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "$HOME" then permit < + native-fsread: filename eq "/etc/lynx.cfg" then permit < + native-fsread: filename eq "/" then permit < + native-fsread: filename eq "/usr/obj/bin/systrace/." then < + native-fsread: filename eq "/usr/obj/bin" then permit < + native-fcntl: permit < + native-getdirentries: permit < + native-lseek: permit < + native-fsread: filename eq "/usr/obj" then permit < + native-fsread: filename eq "$HOME/.mime.types" then permi < + native-sigaction: permit < + native-ioctl: permit < + native-fsread: filename eq "$HOME/.terminfo.db" then perm < + native-fsread: filename eq "$HOME/.terminfo" then permit < + native-pread: permit < + native-write: permit < + native-fsread: filename eq "$HOME/.lynx-keymaps" then per < + native-fsread: filename eq "/etc/utmp" then permit | native-fsread: filename eq "/var/run/ld.so.hints" then pe + native-poll: permit | native-fstat: permit + native-nanosleep: permit | native-fswrite: filename match "/tmp/lynx-*" then permit + > native-getdirentries: permit + > native-getpid: permit + native-fsread: filename eq "/etc/resolv.conf" then permit | native-ioctl: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK | native-issetugid: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe | native-lseek: permit + native-sendto: true then permit | native-mmap: permit + native-select: permit | native-mprotect: prot eq "PROT_READ" then permit + > native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi + > native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm + > native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" + > native-munmap: permit + > native-nanosleep: permit + > native-poll: permit + > native-pread: permit + > native-read: permit + > native-select: permit + > native-sendto: true then permit + > native-sigaction: permit + > native-sigprocmask: permit + > native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p | native-write: permit + native-exit: permit < diff --git a/src/sdiff/tests/d_input1 b/src/sdiff/tests/d_input1 new file mode 100644 index 0000000..686e8ea --- /dev/null +++ b/src/sdiff/tests/d_input1 @@ -0,0 +1,72 @@ +Policy: /usr/bin/lynx, Emulation: native + native-__sysctl: permit + native-close: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then permit + native-connect: sockaddr match "inet-\\\[*\\\]:80" then permit + native-exit: permit + native-fcntl: cmd eq "F_SETFD" then permit + native-fsread: filename eq "/" then permit + native-fsread: filename match "/: *" then permit + native-fsread: filename eq "/etc/lynx.cfg" then permit + native-fsread: filename eq "/etc/malloc.conf" then permit + native-fsread: filename eq "/etc/resolv.conf" then permit + native-fsread: filename eq "/etc/utmp" then permit + native-fsread: filename eq "/home" then permit + native-fsread: filename eq "$HOME" then permit + native-fsread: filename eq "$HOME/.lynx-keymaps" then permit + native-fsread: filename eq "$HOME/.lynxrc" then permit + native-fsread: filename eq "$HOME/.mailcap" then permit + native-fsread: filename eq "$HOME/.mime.types" then permit + native-fsread: filename eq "$HOME/.terminfo" then permit + native-fsread: filename eq "$HOME/.terminfo.db" then permit + native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "/tmp" then permit + native-fsread: filename match "/tmp/lynx-*/." then permit + native-fsread: filename eq "/usr" then permit + native-fsread: filename eq "/usr/bin" then permit + native-fsread: filename eq "/usr/games" then permit + native-fsread: filename eq "/usr/include" then permit + native-fsread: filename eq "/usr/lib" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then permit + native-fsread: filename match "/usr/lib/libcrypto.so.*" then permit + native-fsread: filename match "/usr/lib/libncurses.so.*" then permit + native-fsread: filename match "/usr/lib/libssl.so.*" then permit + native-fsread: filename eq "/usr/libdata" then permit + native-fsread: filename eq "/usr/libexec" then permit + native-fsread: filename eq "/usr/lkm" then permit + native-fsread: filename eq "/usr/local" then permit + native-fsread: filename eq "/usr/mdec" then permit + native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "/usr/obj/bin" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/." then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mailcap" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.types" then permit + native-fsread: filename eq "/usr/share/misc/terminfo.db" then permit + native-fsread: filename eq "/var/run/dev.db" then permit + native-fsread: filename eq "/var/run/ld.so.hints" then permit + native-fstat: permit + native-fswrite: filename match "/tmp/lynx-*" then permit + native-getdirentries: permit + native-getpid: permit + native-gettimeofday: permit + native-ioctl: permit + native-issetugid: permit + native-lseek: permit + native-mmap: permit + native-mprotect: prot eq "PROT_READ" then permit + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permit + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then permit + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" then permit + native-munmap: permit + native-nanosleep: permit + native-poll: permit + native-pread: permit + native-read: permit + native-recvfrom: permit + native-select: permit + native-sendto: true then permit + native-sigaction: permit + native-sigprocmask: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK_DGRAM" then permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK_STREAM" then permit + native-write: permit diff --git a/src/sdiff/tests/d_input2 b/src/sdiff/tests/d_input2 new file mode 100644 index 0000000..70e1b57 --- /dev/null +++ b/src/sdiff/tests/d_input2 @@ -0,0 +1,69 @@ +Policy: /usr/bin/lynx, Emulation: native + native-issetugid: permit + native-mprotect: permit + native-mmap: permit + native-__sysctl: permit + native-fsread: filename eq "/var/run/ld.so.hints" then permit + native-fstat: permit + native-close: permit + native-fsread: filename match "/usr/lib/libssl.so.*" then permit + native-read: permit + native-fsread: filename match "/usr/lib/libcrypto.so.*" then permit + native-fsread: filename match "/usr/lib/libncurses.so.*" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then permit + native-munmap: permit + native-sigprocmask: permit + native-fsread: filename eq "/etc/malloc.conf" then permit + native-getpid: permit + native-fsread: filename eq "/tmp" then permit + native-fswrite: filename match "/tmp/lynx-*" then permit + native-fsread: filename match "/tmp/lynx-*/." then permit + native-fsread: filename eq "$HOME" then permit + native-fsread: filename eq "/etc/lynx.cfg" then permit + native-fsread: filename eq "/" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/." then permit + native-fsread: filename eq "/usr/obj/bin" then permit + native-fcntl: permit + native-getdirentries: permit + native-lseek: permit + native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "/usr" then permit + native-fsread: filename eq "/usr/bin" then permit + native-fsread: filename eq "/usr/games" then permit + native-fsread: filename eq "/usr/include" then permit + native-fsread: filename eq "/usr/lib" then permit + native-fsread: filename eq "/usr/libdata" then permit + native-fsread: filename eq "/usr/libexec" then permit + native-fsread: filename eq "/usr/lkm" then permit + native-fsread: filename eq "/usr/local" then permit + native-fsread: filename eq "/usr/mdec" then permit + native-fsread: filename eq "/home" then permit + native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "$HOME/.lynxrc" then permit + native-fsread: filename match "/: *" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mailcap" then permit + native-fsread: filename eq "$HOME/.mailcap" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.types" then permit + native-fsread: filename eq "$HOME/.mime.types" then permit + native-sigaction: permit + native-ioctl: permit + native-fsread: filename eq "$HOME/.terminfo.db" then permit + native-fsread: filename eq "$HOME/.terminfo" then permit + native-fsread: filename eq "/usr/share/misc/terminfo.db" then permit + native-pread: permit + native-write: permit + native-fsread: filename eq "$HOME/.lynx-keymaps" then permit + native-fsread: filename eq "/var/run/dev.db" then permit + native-fsread: filename eq "/etc/utmp" then permit + native-poll: permit + native-nanosleep: permit + native-gettimeofday: permit + native-fsread: filename eq "/etc/resolv.conf" then permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK_DGRAM" then permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then permit + native-sendto: true then permit + native-select: permit + native-recvfrom: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK_STREAM" then permit + native-connect: sockaddr match "inet-\\\[*\\\]:80" then permit + native-exit: permit diff --git a/src/sdiff/tests/d_oneline.in b/src/sdiff/tests/d_oneline.in new file mode 100644 index 0000000..acbe86c --- /dev/null +++ b/src/sdiff/tests/d_oneline.in @@ -0,0 +1 @@ +abcd diff --git a/src/sdiff/tests/d_oneline_a.out b/src/sdiff/tests/d_oneline_a.out new file mode 100644 index 0000000..18f7c6b --- /dev/null +++ b/src/sdiff/tests/d_oneline_a.out @@ -0,0 +1 @@ + > abcd diff --git a/src/sdiff/tests/d_oneline_b.out b/src/sdiff/tests/d_oneline_b.out new file mode 100644 index 0000000..04cad47 --- /dev/null +++ b/src/sdiff/tests/d_oneline_b.out @@ -0,0 +1 @@ +abcd < diff --git a/src/sdiff/tests/d_same.out b/src/sdiff/tests/d_same.out new file mode 100644 index 0000000..f929fcf --- /dev/null +++ b/src/sdiff/tests/d_same.out @@ -0,0 +1,72 @@ +Policy: /usr/bin/lynx, Emulation: native Policy: /usr/bin/lynx, Emulation: native + native-__sysctl: permit native-__sysctl: permit + native-close: permit native-close: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe native-connect: sockaddr eq "inet-[127.0.0.1]:53" then pe + native-connect: sockaddr match "inet-\\\[*\\\]:80" then p native-connect: sockaddr match "inet-\\\[*\\\]:80" then p + native-exit: permit native-exit: permit + native-fcntl: cmd eq "F_SETFD" then permit native-fcntl: cmd eq "F_SETFD" then permit + native-fsread: filename eq "/" then permit native-fsread: filename eq "/" then permit + native-fsread: filename match "/: native-fsread: filename match "/: + native-fsread: filename eq "/etc/lynx.cfg" then permit native-fsread: filename eq "/etc/lynx.cfg" then permit + native-fsread: filename eq "/etc/malloc.conf" then permit native-fsread: filename eq "/etc/malloc.conf" then permit + native-fsread: filename eq "/etc/resolv.conf" then permit native-fsread: filename eq "/etc/resolv.conf" then permit + native-fsread: filename eq "/etc/utmp" then permit native-fsread: filename eq "/etc/utmp" then permit + native-fsread: filename eq "/home" then permit native-fsread: filename eq "/home" then permit + native-fsread: filename eq "$HOME" then permit native-fsread: filename eq "$HOME" then permit + native-fsread: filename eq "$HOME/.lynx-keymaps" then per native-fsread: filename eq "$HOME/.lynx-keymaps" then per + native-fsread: filename eq "$HOME/.lynxrc" then permit native-fsread: filename eq "$HOME/.lynxrc" then permit + native-fsread: filename eq "$HOME/.mailcap" then permit native-fsread: filename eq "$HOME/.mailcap" then permit + native-fsread: filename eq "$HOME/.mime.types" then permi native-fsread: filename eq "$HOME/.mime.types" then permi + native-fsread: filename eq "$HOME/.terminfo" then permit native-fsread: filename eq "$HOME/.terminfo" then permit + native-fsread: filename eq "$HOME/.terminfo.db" then perm native-fsread: filename eq "$HOME/.terminfo.db" then perm + native-fsread: filename eq "/obj" then permit native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "/tmp" then permit native-fsread: filename eq "/tmp" then permit + native-fsread: filename match "/tmp/lynx-*/." then permit native-fsread: filename match "/tmp/lynx-*/." then permit + native-fsread: filename eq "/usr" then permit native-fsread: filename eq "/usr" then permit + native-fsread: filename eq "/usr/bin" then permit native-fsread: filename eq "/usr/bin" then permit + native-fsread: filename eq "/usr/games" then permit native-fsread: filename eq "/usr/games" then permit + native-fsread: filename eq "/usr/include" then permit native-fsread: filename eq "/usr/include" then permit + native-fsread: filename eq "/usr/lib" then permit native-fsread: filename eq "/usr/lib" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then p native-fsread: filename match "/usr/lib/libc.so.*" then p + native-fsread: filename match "/usr/lib/libcrypto.so.*" t native-fsread: filename match "/usr/lib/libcrypto.so.*" t + native-fsread: filename match "/usr/lib/libncurses.so.*" native-fsread: filename match "/usr/lib/libncurses.so.*" + native-fsread: filename match "/usr/lib/libssl.so.*" then native-fsread: filename match "/usr/lib/libssl.so.*" then + native-fsread: filename eq "/usr/libdata" then permit native-fsread: filename eq "/usr/libdata" then permit + native-fsread: filename eq "/usr/libexec" then permit native-fsread: filename eq "/usr/libexec" then permit + native-fsread: filename eq "/usr/lkm" then permit native-fsread: filename eq "/usr/lkm" then permit + native-fsread: filename eq "/usr/local" then permit native-fsread: filename eq "/usr/local" then permit + native-fsread: filename eq "/usr/mdec" then permit native-fsread: filename eq "/usr/mdec" then permit + native-fsread: filename eq "/usr/obj" then permit native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "/usr/obj/bin" then permit native-fsread: filename eq "/usr/obj/bin" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/." then native-fsread: filename eq "/usr/obj/bin/systrace/." then + native-fsread: filename eq "/usr/obj/bin/systrace/.mailca native-fsread: filename eq "/usr/obj/bin/systrace/.mailca + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t native-fsread: filename eq "/usr/obj/bin/systrace/.mime.t + native-fsread: filename eq "/usr/share/misc/terminfo.db" native-fsread: filename eq "/usr/share/misc/terminfo.db" + native-fsread: filename eq "/var/run/dev.db" then permit native-fsread: filename eq "/var/run/dev.db" then permit + native-fsread: filename eq "/var/run/ld.so.hints" then pe native-fsread: filename eq "/var/run/ld.so.hints" then pe + native-fstat: permit native-fstat: permit + native-fswrite: filename match "/tmp/lynx-*" then permit native-fswrite: filename match "/tmp/lynx-*" then permit + native-getdirentries: permit native-getdirentries: permit + native-getpid: permit native-getpid: permit + native-gettimeofday: permit native-gettimeofday: permit + native-ioctl: permit native-ioctl: permit + native-issetugid: permit native-issetugid: permit + native-lseek: permit native-lseek: permit + native-mmap: permit native-mmap: permit + native-mprotect: prot eq "PROT_READ" then permit native-mprotect: prot eq "PROT_READ" then permit + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permi + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm native-mprotect: prot eq "PROT_READ|PROT_WRITE" then perm + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" + native-munmap: permit native-munmap: permit + native-nanosleep: permit native-nanosleep: permit + native-poll: permit native-poll: permit + native-pread: permit native-pread: permit + native-read: permit native-read: permit + native-recvfrom: permit native-recvfrom: permit + native-select: permit native-select: permit + native-sendto: true then permit native-sendto: true then permit + native-sigaction: permit native-sigaction: permit + native-sigprocmask: permit native-sigprocmask: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK native-socket: sockdom eq "AF_INET" and socktype eq "SOCK + native-write: permit native-write: permit diff --git a/src/sdiff/tests/d_short.out b/src/sdiff/tests/d_short.out new file mode 100644 index 0000000..7a33460 --- /dev/null +++ b/src/sdiff/tests/d_short.out @@ -0,0 +1,15 @@ +Policy: /usr/bin/lynx, Emulation: native + native-issetugid: permit + native-mprotect: permit + native-mmap: permit + native-__sysctl: permit + native-close: permit + native-fsread: filename match "/usr/lib/libssl.so.*" then permit + native-read: permit + native-fsread: filename match "/usr/lib/libcrypto.so.*" then permit + native-fsread: filename match "/usr/lib/libncurses.so.*" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then permit + native-munmap: permit + native-sigprocmask: permit + native-fsread: filename eq "/etc/malloc.conf" then permit + native-fsread: filename eq "/etc/resolv.conf" then permit diff --git a/src/sdiff/tests/d_tabends.in b/src/sdiff/tests/d_tabends.in new file mode 100644 index 0000000..0547049 --- /dev/null +++ b/src/sdiff/tests/d_tabends.in @@ -0,0 +1,17 @@ + +0 +01 +012 +0123 +01234 +012345 +0123456 +01234567 +012345670 +0123456701 +01234567012 +012345670123 +0123456701234 +01234567012345 +012345670123456 +0123456701234567 diff --git a/src/sdiff/tests/d_tabends_a.out b/src/sdiff/tests/d_tabends_a.out new file mode 100644 index 0000000..423bd02 --- /dev/null +++ b/src/sdiff/tests/d_tabends_a.out @@ -0,0 +1,17 @@ + < +0 < +01 < +012 < +0123 < +01234 < +012345 < +0123456 < +01234567 < +012345670 < +0123456701 < +01234567012 < +012345670123 < +0123456701234 < +0123456701234 < +0123456701234 < +0123456701234 < diff --git a/src/sdiff/tests/d_tabends_b.out b/src/sdiff/tests/d_tabends_b.out new file mode 100644 index 0000000..b180705 --- /dev/null +++ b/src/sdiff/tests/d_tabends_b.out @@ -0,0 +1,17 @@ + > + > 0 + > 01 + > 012 + > 0123 + > 01234 + > 012345 + > 0123456 + > 01234567 + > 012345670 + > 0123456701 + > 01234567012 + > 012345670123 + > 0123456701234 + > 0123456701234 + > 0123456701234 + > 0123456701234 diff --git a/src/sdiff/tests/d_tabends_c.out b/src/sdiff/tests/d_tabends_c.out new file mode 100644 index 0000000..c301382 --- /dev/null +++ b/src/sdiff/tests/d_tabends_c.out @@ -0,0 +1,17 @@ + < +0 < +01 < +012 < +0123 < +01234 < +012345 < +0123456 < +01234567 < +01234567 < +01234567 < +01234567 < +01234567 < +01234567 < +01234567 < +01234567 < +01234567 < diff --git a/src/sdiff/tests/d_tabs.out b/src/sdiff/tests/d_tabs.out new file mode 100644 index 0000000..a67d7b1 --- /dev/null +++ b/src/sdiff/tests/d_tabs.out @@ -0,0 +1,102 @@ +Policy: /usr/bin/lynx, Emulation: native Policy: /usr/bin/lynx, Emulation: native + > native-issetugid: permit + > native-mprotect: permit + > native-mmap: permit + native-__sysctl: permit native-__sysctl: permit + > native-fsread: filename eq "/var/run/ld.so.hints" the + > native-fstat: permit + native-close: permit native-close: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" the | native-fsread: filename match "/usr/lib/libssl.so.*" + native-connect: sockaddr match "inet-\\\[*\\\]:80" th | native-read: permit + native-exit: permit | native-fsread: filename match "/usr/lib/libcrypto.so. + native-fcntl: cmd eq "F_SETFD" then permit | native-fsread: filename match "/usr/lib/libncurses.so + native-fsread: filename eq "/" then permit | native-fsread: filename match "/usr/lib/libc.so.*" th + native-fsread: filename match "/ native-fswrite: filename match "/tmp/lynx-*" then per + native-fsread: filename match "/tmp/lynx-*/." then pe native-fsread: filename match "/tmp/lynx-*/." then pe + > native-fsread: filename eq "$HOME" then permit + > native-fsread: filename eq "/etc/lynx.cfg" then permi + > native-fsread: filename eq "/" then permit + > native-fsread: filename eq "/usr/obj/bin/systrace/." + > native-fsread: filename eq "/usr/obj/bin" then permit + > native-fcntl: permit + > native-getdirentries: permit + > native-lseek: permit + > native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "/usr" then permit native-fsread: filename eq "/usr" then permit + native-fsread: filename eq "/usr/bin" then permit native-fsread: filename eq "/usr/bin" then permit + native-fsread: filename eq "/usr/games" then permit native-fsread: filename eq "/usr/games" then permit + native-fsread: filename eq "/usr/include" then permit native-fsread: filename eq "/usr/include" then permit + native-fsread: filename eq "/usr/lib" then permit native-fsread: filename eq "/usr/lib" then permit + native-fsread: filename match "/usr/lib/libc.so.*" th < + native-fsread: filename match "/usr/lib/libcrypto.so. < + native-fsread: filename match "/usr/lib/libncurses.so < + native-fsread: filename match "/usr/lib/libssl.so.*" < + native-fsread: filename eq "/usr/libdata" then permit native-fsread: filename eq "/usr/libdata" then permit + native-fsread: filename eq "/usr/libexec" then permit native-fsread: filename eq "/usr/libexec" then permit + native-fsread: filename eq "/usr/lkm" then permit native-fsread: filename eq "/usr/lkm" then permit + native-fsread: filename eq "/usr/local" then permit native-fsread: filename eq "/usr/local" then permit + native-fsread: filename eq "/usr/mdec" then permit native-fsread: filename eq "/usr/mdec" then permit + native-fsread: filename eq "/usr/obj" then permit | native-fsread: filename eq "/home" then permit + native-fsread: filename eq "/usr/obj/bin" then permit | native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/." | native-fsread: filename eq "$HOME/.lynxrc" then permi + > native-fsread: filename match "/ native-fsread: filename eq "$HOME/.mailcap" then perm + native-fsread: filename eq "/usr/obj/bin/systrace/.mi native-fsread: filename eq "/usr/obj/bin/systrace/.mi + > native-fsread: filename eq "$HOME/.mime.types" then p + > native-sigaction: permit + > native-ioctl: permit + > native-fsread: filename eq "$HOME/.terminfo.db" then + > native-fsread: filename eq "$HOME/.terminfo" then per + native-fsread: filename eq "/usr/share/misc/terminfo. native-fsread: filename eq "/usr/share/misc/terminfo. + > native-pread: permit + > native-write: permit + > native-fsread: filename eq "$HOME/.lynx-keymaps" then + native-fsread: filename eq "/var/run/dev.db" then per native-fsread: filename eq "/var/run/dev.db" then per + native-fsread: filename eq "/var/run/ld.so.hints" the | native-fsread: filename eq "/etc/utmp" then permit + native-fstat: permit < + native-fswrite: filename match "/tmp/lynx-*" then per < + native-getdirentries: permit < + native-getpid: permit < + native-gettimeofday: permit < + native-ioctl: permit < + native-issetugid: permit < + native-lseek: permit < + native-mmap: permit < + native-mprotect: prot eq "PROT_READ" then permit < + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then p < + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then < + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_E < + native-munmap: permit < + native-nanosleep: permit < + native-poll: permit native-poll: permit + native-pread: permit | native-nanosleep: permit + native-read: permit | native-gettimeofday: permit + native-recvfrom: permit | native-fsread: filename eq "/etc/resolv.conf" then pe + native-select: permit < + native-sendto: true then permit < + native-sigaction: permit < + native-sigprocmask: permit < + native-socket: sockdom eq "AF_INET" and socktype eq " native-socket: sockdom eq "AF_INET" and socktype eq " + > native-connect: sockaddr eq "inet-[127.0.0.1]:53" the + > native-sendto: true then permit + > native-select: permit + > native-recvfrom: permit + native-socket: sockdom eq "AF_INET" and socktype eq " native-socket: sockdom eq "AF_INET" and socktype eq " + native-write: permit | native-connect: sockaddr match "inet-\\\[*\\\]:80" th + > native-exit: permit diff --git a/src/sdiff/tests/d_tabs1.in b/src/sdiff/tests/d_tabs1.in new file mode 100644 index 0000000..b5a1834 --- /dev/null +++ b/src/sdiff/tests/d_tabs1.in @@ -0,0 +1,72 @@ +Policy: /usr/bin/lynx, Emulation: native + native-__sysctl: permit + native-close: permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then permit + native-connect: sockaddr match "inet-\\\[*\\\]:80" then permit + native-exit: permit + native-fcntl: cmd eq "F_SETFD" then permit + native-fsread: filename eq "/" then permit + native-fsread: filename match "/: *" then permit + native-fsread: filename eq "/etc/lynx.cfg" then permit + native-fsread: filename eq "/etc/malloc.conf" then permit + native-fsread: filename eq "/etc/resolv.conf" then permit + native-fsread: filename eq "/etc/utmp" then permit + native-fsread: filename eq "/home" then permit + native-fsread: filename eq "$HOME" then permit + native-fsread: filename eq "$HOME/.lynx-keymaps" then permit + native-fsread: filename eq "$HOME/.lynxrc" then permit + native-fsread: filename eq "$HOME/.mailcap" then permit + native-fsread: filename eq "$HOME/.mime.types" then permit + native-fsread: filename eq "$HOME/.terminfo" then permit + native-fsread: filename eq "$HOME/.terminfo.db" then permit + native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "/tmp" then permit + native-fsread: filename match "/tmp/lynx-*/." then permit + native-fsread: filename eq "/usr" then permit + native-fsread: filename eq "/usr/bin" then permit + native-fsread: filename eq "/usr/games" then permit + native-fsread: filename eq "/usr/include" then permit + native-fsread: filename eq "/usr/lib" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then permit + native-fsread: filename match "/usr/lib/libcrypto.so.*" then permit + native-fsread: filename match "/usr/lib/libncurses.so.*" then permit + native-fsread: filename match "/usr/lib/libssl.so.*" then permit + native-fsread: filename eq "/usr/libdata" then permit + native-fsread: filename eq "/usr/libexec" then permit + native-fsread: filename eq "/usr/lkm" then permit + native-fsread: filename eq "/usr/local" then permit + native-fsread: filename eq "/usr/mdec" then permit + native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "/usr/obj/bin" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/." then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mailcap" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.types" then permit + native-fsread: filename eq "/usr/share/misc/terminfo.db" then permit + native-fsread: filename eq "/var/run/dev.db" then permit + native-fsread: filename eq "/var/run/ld.so.hints" then permit + native-fstat: permit + native-fswrite: filename match "/tmp/lynx-*" then permit + native-getdirentries: permit + native-getpid: permit + native-gettimeofday: permit + native-ioctl: permit + native-issetugid: permit + native-lseek: permit + native-mmap: permit + native-mprotect: prot eq "PROT_READ" then permit + native-mprotect: prot eq "PROT_READ|PROT_EXEC" then permit + native-mprotect: prot eq "PROT_READ|PROT_WRITE" then permit + native-mprotect: prot eq "PROT_READ|PROT_WRITE|PROT_EXEC" then permit + native-munmap: permit + native-nanosleep: permit + native-poll: permit + native-pread: permit + native-read: permit + native-recvfrom: permit + native-select: permit + native-sendto: true then permit + native-sigaction: permit + native-sigprocmask: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK_DGRAM" then permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK_STREAM" then permit + native-write: permit diff --git a/src/sdiff/tests/d_tabs2.in b/src/sdiff/tests/d_tabs2.in new file mode 100644 index 0000000..d00f415 --- /dev/null +++ b/src/sdiff/tests/d_tabs2.in @@ -0,0 +1,69 @@ +Policy: /usr/bin/lynx, Emulation: native + native-issetugid: permit + native-mprotect: permit + native-mmap: permit + native-__sysctl: permit + native-fsread: filename eq "/var/run/ld.so.hints" then permit + native-fstat: permit + native-close: permit + native-fsread: filename match "/usr/lib/libssl.so.*" then permit + native-read: permit + native-fsread: filename match "/usr/lib/libcrypto.so.*" then permit + native-fsread: filename match "/usr/lib/libncurses.so.*" then permit + native-fsread: filename match "/usr/lib/libc.so.*" then permit + native-munmap: permit + native-sigprocmask: permit + native-fsread: filename eq "/etc/malloc.conf" then permit + native-getpid: permit + native-fsread: filename eq "/tmp" then permit + native-fswrite: filename match "/tmp/lynx-*" then permit + native-fsread: filename match "/tmp/lynx-*/." then permit + native-fsread: filename eq "$HOME" then permit + native-fsread: filename eq "/etc/lynx.cfg" then permit + native-fsread: filename eq "/" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/." then permit + native-fsread: filename eq "/usr/obj/bin" then permit + native-fcntl: permit + native-getdirentries: permit + native-lseek: permit + native-fsread: filename eq "/usr/obj" then permit + native-fsread: filename eq "/usr" then permit + native-fsread: filename eq "/usr/bin" then permit + native-fsread: filename eq "/usr/games" then permit + native-fsread: filename eq "/usr/include" then permit + native-fsread: filename eq "/usr/lib" then permit + native-fsread: filename eq "/usr/libdata" then permit + native-fsread: filename eq "/usr/libexec" then permit + native-fsread: filename eq "/usr/lkm" then permit + native-fsread: filename eq "/usr/local" then permit + native-fsread: filename eq "/usr/mdec" then permit + native-fsread: filename eq "/home" then permit + native-fsread: filename eq "/obj" then permit + native-fsread: filename eq "$HOME/.lynxrc" then permit + native-fsread: filename match "/: *" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mailcap" then permit + native-fsread: filename eq "$HOME/.mailcap" then permit + native-fsread: filename eq "/usr/obj/bin/systrace/.mime.types" then permit + native-fsread: filename eq "$HOME/.mime.types" then permit + native-sigaction: permit + native-ioctl: permit + native-fsread: filename eq "$HOME/.terminfo.db" then permit + native-fsread: filename eq "$HOME/.terminfo" then permit + native-fsread: filename eq "/usr/share/misc/terminfo.db" then permit + native-pread: permit + native-write: permit + native-fsread: filename eq "$HOME/.lynx-keymaps" then permit + native-fsread: filename eq "/var/run/dev.db" then permit + native-fsread: filename eq "/etc/utmp" then permit + native-poll: permit + native-nanosleep: permit + native-gettimeofday: permit + native-fsread: filename eq "/etc/resolv.conf" then permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK_DGRAM" then permit + native-connect: sockaddr eq "inet-[127.0.0.1]:53" then permit + native-sendto: true then permit + native-select: permit + native-recvfrom: permit + native-socket: sockdom eq "AF_INET" and socktype eq "SOCK_STREAM" then permit + native-connect: sockaddr match "inet-\\\[*\\\]:80" then permit + native-exit: permit diff --git a/src/sdiff/tests/sdiff_test.sh b/src/sdiff/tests/sdiff_test.sh new file mode 100755 index 0000000..83ed935 --- /dev/null +++ b/src/sdiff/tests/sdiff_test.sh @@ -0,0 +1,242 @@ +# $NetBSD: t_sdiff.sh,v 1.1 2012/03/17 16:33:15 jruoho Exp $ +# +# Copyright (c) 2008, 2009 The NetBSD Foundation, 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: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. 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 FOUNDATION 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. +# + +atf_test_case flags +flags_head() +{ + atf_set "descr" "Checks -l, -s and -w flags" +} +flags_body() +{ + atf_check -o file:$(atf_get_srcdir)/d_flags_l.out -s eq:1 \ + sdiff -l "$(atf_get_srcdir)/d_input1" "$(atf_get_srcdir)/d_input2" + + atf_check -o file:$(atf_get_srcdir)/d_flags_s.out -s eq:1 \ + sdiff -s "$(atf_get_srcdir)/d_input1" "$(atf_get_srcdir)/d_input2" + + atf_check -o file:$(atf_get_srcdir)/d_flags_w.out -s eq:1 \ + sdiff -w 125 "$(atf_get_srcdir)/d_input1" "$(atf_get_srcdir)/d_input2" +} + +atf_test_case iflags +iflags_head() +{ + atf_set "descr" "Checks flags -l, -s and -w combined with -I" +} +iflags_body() +{ + tail1="-w 125 -I .*filename.* $(atf_get_srcdir)/d_input1 $(atf_get_srcdir)/d_input2" + tail2="-w 125 -I .*filename.* $(atf_get_srcdir)/d_input2 $(atf_get_srcdir)/d_input1" + + atf_check -o file:$(atf_get_srcdir)/d_iflags_a1.out -s eq:1 sdiff ${tail1} + atf_check -o file:$(atf_get_srcdir)/d_iflags_a2.out -s eq:1 sdiff ${tail2} + atf_check -o file:$(atf_get_srcdir)/d_iflags_b1.out -s eq:1 sdiff -s ${tail1} + atf_check -o file:$(atf_get_srcdir)/d_iflags_b2.out -s eq:1 sdiff -s ${tail2} + atf_check -o file:$(atf_get_srcdir)/d_iflags_c1.out -s eq:1 sdiff -l ${tail1} + atf_check -o file:$(atf_get_srcdir)/d_iflags_c2.out -s eq:1 sdiff -l ${tail2} + atf_check -o file:$(atf_get_srcdir)/d_iflags_d1.out -s eq:1 sdiff -s ${tail1} + atf_check -o file:$(atf_get_srcdir)/d_iflags_d2.out -s eq:1 sdiff -s ${tail2} +} + +atf_test_case tabs +tabs_head() +{ + atf_set "descr" "Checks comparing files containing tabs" +} +tabs_body() +{ + atf_check -o file:$(atf_get_srcdir)/d_tabs.out -s eq:1 \ + sdiff "$(atf_get_srcdir)/d_tabs1.in" "$(atf_get_srcdir)/d_tabs2.in" +} + +atf_test_case tabends +tabends_head() +{ + atf_set "descr" "Checks correct handling of lines ended with tabs" +} +tabends_body() +{ + atf_check -o file:$(atf_get_srcdir)/d_tabends_a.out -s eq:1 \ + sdiff -w30 "$(atf_get_srcdir)/d_tabends.in" /dev/null + + atf_check -o file:$(atf_get_srcdir)/d_tabends_b.out -s eq:1 \ + sdiff -w30 /dev/null "$(atf_get_srcdir)/d_tabends.in" + + atf_check -o file:$(atf_get_srcdir)/d_tabends_c.out -s eq:1 \ + sdiff -w19 "$(atf_get_srcdir)/d_tabends.in" /dev/null +} + +atf_test_case merge +merge_head() +{ + atf_set "descr" "Checks interactive merging" +} +merge_body() +{ + merge_tail="-o merge.out $(atf_get_srcdir)/d_input1 \ +$(atf_get_srcdir)/d_input2 >/dev/null ; cat merge.out" + + cp $(atf_get_srcdir)/d_input* . + + atf_check -o file:d_input1 -x "yes l | sdiff ${merge_tail}" + atf_check -o file:d_input2 -x "yes r | sdiff ${merge_tail}" + + atf_check -o file:d_input1 -x \ + "yes el | EDITOR=cat VISUAL=cat sdiff ${merge_tail}" + atf_check -o file:d_input2 -x \ + "yes er | EDITOR=cat VISUAL=cat sdiff ${merge_tail}" + + atf_check -o file:d_input1 -x "yes l | sdiff -s ${merge_tail}" + atf_check -o file:d_input2 -x "yes r | sdiff -s ${merge_tail}" + atf_check -o file:d_input1 -x "yes l | sdiff -l ${merge_tail}" + atf_check -o file:d_input2 -x "yes r | sdiff -l ${merge_tail}" + atf_check -o file:d_input1 -x "yes l | sdiff -ls ${merge_tail}" + atf_check -o file:d_input2 -x "yes r | sdiff -ls ${merge_tail}" + + atf_check -o file:d_input1 -x "{ while :; do echo s; echo l; \ +echo v; echo l; done; } | sdiff ${merge_tail}" + + atf_check -o file:d_input2 -x "{ while :; do echo s; echo r; \ +echo v; echo r; done; } | sdiff ${merge_tail}" +} + +atf_test_case same +same_head() +{ + atf_set "descr" "Checks comparing file with itself" +} +same_body() +{ + atf_check -o file:$(atf_get_srcdir)/d_same.out \ + sdiff "$(atf_get_srcdir)/d_input1" "$(atf_get_srcdir)/d_input1" +} + +atf_test_case oneline +oneline_head() +{ + atf_set "descr" "Checks comparing one-line files" +} +oneline_body() +{ + atf_check -o file:$(atf_get_srcdir)/d_oneline_a.out -s eq:1 \ + sdiff /dev/null "$(atf_get_srcdir)/d_oneline.in" + + atf_check -o file:$(atf_get_srcdir)/d_oneline_b.out -s eq:1 \ + sdiff "$(atf_get_srcdir)/d_oneline.in" /dev/null +} + +atf_test_case dot +dot_head() +{ + atf_set "descr" "Checks comparing with file containing only one character" +} +dot_body() +{ + echo ". <" > expout + atf_check -o file:expout -s eq:1 sdiff "$(atf_get_srcdir)/d_dot.in" /dev/null + + echo " > ." > expout + atf_check -o file:expout -s eq:1 sdiff /dev/null "$(atf_get_srcdir)/d_dot.in" +} + +atf_test_case stdin +stdin_head() +{ + atf_set "descr" "Checks reading data from stdin" +} +stdin_body() +{ + echo " > stdin" > expout + atf_check -o file:expout -s eq:1 -x \ + "echo stdin | sdiff /dev/null /dev/stdin" + + echo "stdin <" > expout + atf_check -o file:expout -s eq:1 -x \ + "echo stdin | sdiff /dev/stdin /dev/null" +} + +atf_test_case short +short_head() +{ + atf_set "descr" "Checks premature stop of merging" +} +short_body() +{ + atf_check -o file:$(atf_get_srcdir)/d_short.out -x \ + "printf \"r\\nl\\nr\\nl\" | sdiff -o merge.out $(atf_get_srcdir)/d_input1 \ +$(atf_get_srcdir)/d_input2 >/dev/null ; cat merge.out" +} + +atf_test_case tflag +tflag_head() +{ + atf_set "descr" "Checks tab expansion" +} +tflag_body() +{ + printf "a\tb\n" >a + printf "b\ta\n" >b + atf_check -s exit:1 -o match:$'a\tb' \ + sdiff a b + atf_check -s exit:1 -o match:"a {7}b" \ + sdiff -t a b + atf_check -s exit:1 -o match:"a {3}b" \ + sdiff -t --tabsize 4 a b +} + +atf_test_case binary +binary_head() +{ + atf_set "descr" "Checks binary file handling" +} +binary_body() +{ + printf "a\0\n" >a + printf "b\0\n" >b + atf_check -o empty sdiff a a + atf_check -o empty sdiff a - +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" 3. All advertising materials mentioning features or use of this software +.\" must display the following acknowledgement: +.\" This product includes software developed by the FreeBSD Project +.\" its contributors. +.\" 4. Neither the name of the FreeBSD Project 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 CONTRIBUTOR ``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 CONTRIBUTOR 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. +.\" +.Dd September 24, 2020 +.Dt WHICH 1 +.Os +.Sh NAME +.Nm which +.Nd "locate a program file in the user's path" +.Sh SYNOPSIS +.Nm +.Op Fl as +.Ar program ... +.Sh DESCRIPTION +The +.Nm +utility +takes a list of command names and searches the path for each executable +file that would be run had these commands actually been invoked. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl a +List all instances of executables found (instead of just the first one +of each). +.It Fl s +No output, just return 0 if all of the executables are found, or 1 if +some were not found. +.El +.Pp +Some shells may provide a builtin +.Nm +command which is similar or identical to this utility. +Consult the +.Xr builtin 1 +manual page. +.Sh EXAMPLES +Locate the +.Xr ls 1 +and +.Xr cp 1 +commands: +.Bd -literal -offset indent +$ /usr/bin/which ls cp +/bin/ls +/bin/cp +.Ed +.Pp +Same as above with a specific +.Ev PATH +and showing all occurrences: +.Bd -literal -offset indent +$ PATH=/bin:/rescue /usr/bin/which -a ls cp +/bin/ls +/rescue/ls +/bin/cp +/rescue/cp +.Ed +.Pp +.Nm which +will show duplicates if the same executable is found more than once: +.Bd -literal -offset indent +$ PATH=/bin:/bin /usr/bin/which -a ls +/bin/ls +/bin/ls +.Ed +.Pp +Do not show output. +Just exit with an appropriate return code: +.Bd -literal -offset indent +$ /usr/bin/which -s ls cp +$ echo $? +0 + +$ /usr/bin/which -s fakecommand +$ echo $? +1 +.Ed +.Sh SEE ALSO +.Xr builtin 1 , +.Xr csh 1 , +.Xr find 1 , +.Xr locate 1 , +.Xr whereis 1 +.Sh HISTORY +The +.Nm +command first appeared in +.Fx 2.1 . +.Sh AUTHORS +.An -nosplit +The +.Nm +utility was originally written in Perl and was contributed by +.An Wolfram Schneider Aq Mt wosch@FreeBSD.org . +The current version of +.Nm +was rewritten in C by +.An Daniel Papasian Aq Mt dpapasia@andrew.cmu.edu . diff --git a/src/which/which.c b/src/which/which.c new file mode 100644 index 0000000..0e6a55e --- /dev/null +++ b/src/which/which.c @@ -0,0 +1,144 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2000 Dan Papasian. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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 +#include +#include +#include +#include +#include +#include + +static void usage(void); +static int print_matches(char *, char *); + +static int silent; +static int allpaths; + +int +which_main(int argc, char **argv) +{ + char *p, *path; + size_t pathlen; + int opt, status; + + status = EXIT_SUCCESS; + silent = 0; + allpaths = 0; + + while ((opt = getopt(argc, argv, "as")) != -1) { + switch (opt) { + case 'a': + allpaths = 1; + break; + case 's': + silent = 1; + break; + default: + usage(); + break; + } + } + + argv += optind; + argc -= optind; + + if (argc == 0) + usage(); + + if ((p = getenv("PATH")) == NULL) + exit(EXIT_FAILURE); + pathlen = strlen(p) + 1; + path = malloc(pathlen); + if (path == NULL) + err(EXIT_FAILURE, NULL); + + while (argc > 0) { + memcpy(path, p, pathlen); + + if (strlen(*argv) >= FILENAME_MAX || + print_matches(path, *argv) == -1) + status = EXIT_FAILURE; + + argv++; + argc--; + } + + free(path); + return status; +} + +static void +usage(void) +{ + (void)fprintf(stderr, "usage: which [-as] program ...\n"); + exit(EXIT_FAILURE); +} + +static int +is_there(char *candidate) +{ + struct stat fin; + + /* Work around access(2) false positives for superuser */ + if (access(candidate, X_OK) == 0 && + stat(candidate, &fin) == 0 && + S_ISREG(fin.st_mode) && + (getuid() != 0 || + (fin.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0)) { + if (!silent) + printf("%s\n", candidate); + return (1); + } + return (0); +} + +static int +print_matches(char *path, char *filename) +{ + char candidate[PATH_MAX]; + const char *d; + int found; + + if (strchr(filename, '/') != NULL) + return (is_there(filename) ? 0 : -1); + found = 0; + while ((d = strsep(&path, ":")) != NULL) { + if (*d == '\0') + d = "."; + if (snprintf(candidate, sizeof(candidate), "%s/%s", d, + filename) >= (int)sizeof(candidate)) + continue; + if (is_there(candidate)) { + found = 1; + if (!allpaths) + break; + } + } + return (found ? 0 : -1); +} diff --git a/src/xargs/pathnames.h b/src/xargs/pathnames.h new file mode 100644 index 0000000..0a387e1 --- /dev/null +++ b/src/xargs/pathnames.h @@ -0,0 +1,34 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + * + * @(#)pathnames.h 8.1 (Berkeley) 6/6/93 + */ + +#define _PATH_ECHO "/bin/echo" diff --git a/src/xargs/strnsubst.c b/src/xargs/strnsubst.c new file mode 100644 index 0000000..ea5edc1 --- /dev/null +++ b/src/xargs/strnsubst.c @@ -0,0 +1,111 @@ +/* $xMach: strnsubst.c,v 1.3 2002/02/23 02:10:24 jmallett Exp $ */ + +/* + * Copyright (c) 2002 J. Mallett. All rights reserved. + * You may do whatever you want with this file as long as + * the above copyright and this notice remain intact, along + * with the following statement: + * For the man who taught me vi, and who got too old, too young. + */ + +#include +#include +#include +#include +#include + +bool strnsubst(char **, const char *, const char *, size_t); + +/* + * Replaces str with a string consisting of str with match replaced with + * replstr as many times as can be done before the constructed string is + * maxsize bytes large. It does not free the string pointed to by str, it + * is up to the calling program to be sure that the original contents of + * str as well as the new contents are handled in an appropriate manner. + * If replstr is NULL, then that internally is changed to a nil-string, so + * that we can still pretend to do somewhat meaningful substitution. + * + * Returns true if truncation was needed to do the replacement, false if + * truncation was not required. + */ +bool +strnsubst(char **str, const char *match, const char *replstr, size_t maxsize) +{ + char *s1, *s2, *this; + bool error = false; + + s1 = *str; + if (s1 == NULL) + return false; + /* + * If maxsize is 0 then set it to the length of s1, because we have + * to duplicate s1. XXX we maybe should double-check whether the match + * appears in s1. If it doesn't, then we also have to set the length + * to the length of s1, to avoid modifying the argument. It may make + * sense to check if maxsize is <= strlen(s1), because in that case we + * want to return the unmodified string, too. + */ + if (maxsize == 0) { + match = NULL; + maxsize = strlen(s1) + 1; + } + s2 = calloc(1, maxsize); + if (s2 == NULL) + err(1, "calloc"); + + if (replstr == NULL) + replstr = ""; + + if (match == NULL || replstr == NULL || maxsize == strlen(s1)) { + strlcpy(s2, s1, maxsize); + goto done; + } + + for (;;) { + this = strstr(s1, match); + if (this == NULL) + break; + if ((strlen(s2) + strlen(s1) + strlen(replstr) - + strlen(match) + 1) > maxsize) { + strlcat(s2, s1, maxsize); + error = true; + goto done; + } + strncat(s2, s1, (uintptr_t)this - (uintptr_t)s1); + strcat(s2, replstr); + s1 = this + strlen(match); + } + strcat(s2, s1); +done: + *str = s2; + return error; +} + +#ifdef TEST +#include + +int +main(void) +{ + char *x, *y, *z, *za; + + x = "{}%$"; + strnsubst(&x, "%$", "{} enpury!", 255); + y = x; + strnsubst(&y, "}{}", "ybir", 255); + z = y; + strnsubst(&z, "{", "v ", 255); + za = z; + strnsubst(&z, NULL, za, 255); + if (strcmp(z, "v ybir enpury!") == 0) + printf("strnsubst() seems to work!\n"); + else + printf("strnsubst() is broken.\n"); + printf("%s\n", z); + free(x); + free(y); + free(z); + free(za); + return 0; +} +#endif diff --git a/src/xargs/xargs.1 b/src/xargs/xargs.1 new file mode 100644 index 0000000..0a64434 --- /dev/null +++ b/src/xargs/xargs.1 @@ -0,0 +1,390 @@ +.\" Copyright (c) 1990, 1991, 1993 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" This code is derived from software contributed to Berkeley by +.\" John B. Roll Jr. and the Institute of Electrical and Electronics +.\" Engineers, Inc. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. 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. +.\" 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. +.\" +.\" @(#)xargs.1 8.1 (Berkeley) 6/6/93 +.\" $xMach: xargs.1,v 1.2 2002/02/23 05:23:37 tim Exp $ +.\" +.Dd September 21, 2020 +.Dt XARGS 1 +.Os +.Sh NAME +.Nm xargs +.Nd "construct argument list(s) and execute utility" +.Sh SYNOPSIS +.Nm +.Op Fl 0oprt +.Op Fl E Ar eofstr +.Oo +.Fl I Ar replstr +.Op Fl R Ar replacements +.Op Fl S Ar replsize +.Oc +.Op Fl J Ar replstr +.Op Fl L Ar number +.Oo +.Fl n Ar number +.Op Fl x +.Oc +.Op Fl P Ar maxprocs +.Op Fl s Ar size +.Op Ar utility Op Ar argument ... +.Sh DESCRIPTION +The +.Nm +utility reads space, tab, newline and end-of-file delimited strings +from the standard input and executes +.Ar utility +with the strings as +arguments. +.Pp +Any arguments specified on the command line are given to +.Ar utility +upon each invocation, followed by some number of the arguments read +from the standard input of +.Nm . +This is repeated until standard input is exhausted. +.Pp +Spaces, tabs and newlines may be embedded in arguments using single +(``\ '\ '') +or double (``"'') quotes or backslashes (``\e''). +Single quotes escape all non-single quote characters, excluding newlines, +up to the matching single quote. +Double quotes escape all non-double quote characters, excluding newlines, +up to the matching double quote. +Any single character, including newlines, may be escaped by a backslash. +.Pp +The options are as follows: +.Bl -tag -width indent +.It Fl 0 , Fl -null +Change +.Nm +to expect NUL +(``\\0'') +characters as separators, instead of spaces and newlines. +This is expected to be used in concert with the +.Fl print0 +function in +.Xr find 1 . +.It Fl E Ar eofstr +Use +.Ar eofstr +as a logical EOF marker. +.It Fl I Ar replstr +Execute +.Ar utility +for each input line, replacing one or more occurrences of +.Ar replstr +in up to +.Ar replacements +(or 5 if no +.Fl R +flag is specified) arguments to +.Ar utility +with the entire line of input. +The resulting arguments, after replacement is done, will not be allowed to grow +beyond +.Ar replsize +(or 255 if no +.Fl S +flag is specified) +bytes; this is implemented by concatenating as much of the argument +containing +.Ar replstr +as possible, to the constructed arguments to +.Ar utility , +up to +.Ar replsize +bytes. +The size limit does not apply to arguments to +.Ar utility +which do not contain +.Ar replstr , +and furthermore, no replacement will be done on +.Ar utility +itself. +Implies +.Fl x . +.It Fl J Ar replstr +If this option is specified, +.Nm +will use the data read from standard input to replace the first occurrence of +.Ar replstr +instead of appending that data after all other arguments. +This option will not affect how many arguments will be read from input +.Pq Fl n , +or the size of the command(s) +.Nm +will generate +.Pq Fl s . +The option just moves where those arguments will be placed in the command(s) +that are executed. +The +.Ar replstr +must show up as a distinct +.Ar argument +to +.Nm . +It will not be recognized if, for instance, it is in the middle of a +quoted string. +Furthermore, only the first occurrence of the +.Ar replstr +will be replaced. +For example, the following command will copy the list of files and +directories which start with an uppercase letter in the current +directory to +.Pa destdir : +.Pp +.Dl /bin/ls -1d [A-Z]* | xargs -J % cp -Rp % destdir +.It Fl L Ar number +Call +.Ar utility +for every +.Ar number +lines read. +If EOF is reached and fewer lines have been read than +.Ar number +then +.Ar utility +will be called with the available lines. +.It Fl n Ar number , Fl -max-args= Ns Ar number +Set the maximum number of arguments taken from standard input for each +invocation of +.Ar utility . +An invocation of +.Ar utility +will use less than +.Ar number +standard input arguments if the number of bytes accumulated (see the +.Fl s +option) exceeds the specified +.Ar size +or there are fewer than +.Ar number +arguments remaining for the last invocation of +.Ar utility . +The current default value for +.Ar number +is 5000. +.It Fl o +Reopen stdin as +.Pa /dev/tty +in the child process before executing the command. +This is useful if you want +.Nm +to run an interactive application. +.It Fl P Ar maxprocs , Fl -max-procs= Ns Ar maxprocs +Parallel mode: run at most +.Ar maxprocs +invocations of +.Ar utility +at once. +If +.Ar maxprocs +is set to 0, +.Nm +will run as many processes as possible. +.It Fl p , Fl -interactive +Echo each command to be executed and ask the user whether it should be +executed. +An affirmative response, +.Ql y +in the POSIX locale, +causes the command to be executed, any other response causes it to be +skipped. +No commands are executed if the process is not attached to a terminal. +.It Fl r , Fl -no-run-if-empty +Compatibility with GNU +.Nm . +The GNU version of +.Nm +runs the +.Ar utility +argument at least once, even if +.Nm +input is empty, and it supports a +.Fl r +option to inhibit this behavior. +The +.Fx +version of +.Nm +does not run the +.Ar utility +argument on empty input, but it supports the +.Fl r +option for command-line compatibility with GNU +.Nm , +but the +.Fl r +option does nothing in the +.Fx +version of +.Nm . +.It Fl R Ar replacements +Specify the maximum number of arguments that +.Fl I +will do replacement in. +If +.Ar replacements +is negative, the number of arguments in which to replace is unbounded. +.It Fl S Ar replsize +Specify the amount of space (in bytes) that +.Fl I +can use for replacements. +The default for +.Ar replsize +is 255. +.It Fl s Ar size , Fl -max-chars= Ns Ar size +Set the maximum number of bytes for the command line length provided to +.Ar utility . +The sum of the length of the utility name, the arguments passed to +.Ar utility +(including +.Dv NULL +terminators) and the current environment will be less than or equal to +this number. +The current default value for +.Ar size +is +.Dv ARG_MAX +- 4096. +.It Fl t , Fl -verbose +Echo the command to be executed to standard error immediately before it +is executed. +.It Fl x , Fl -exit +Force +.Nm +to terminate immediately if a command line containing +.Ar number +arguments will not fit in the specified (or default) command line length. +.El +.Pp +If +.Ar utility +is omitted, +.Xr echo 1 +is used. +.Pp +Undefined behavior may occur if +.Ar utility +reads from the standard input. +.Pp +If a command line cannot be assembled, or +cannot be invoked, or if an invocation of +.Ar utility +is terminated by a signal, +or an invocation of +.Ar utility +exits with a value of 255, the +.Nm +utility stops processing input and exits after all invocations of +.Ar utility +finish processing. +.Sh EXIT STATUS +The +.Nm +utility exits with a value of 0 if no error occurs. +If +.Ar utility +cannot be found, +.Nm +exits with a value of 127, otherwise if +.Ar utility +cannot be executed, +.Nm +exits with a value of 126. +If any other error occurs, +.Nm +exits with a value of 1. +.Sh EXAMPLES +Create a 3x3 matrix with numbers from 1 to 9. +Every +.Xr echo 1 +instance receives three lines as arguments: +.Bd -literal -offset indent +$ seq 1 9 | xargs -L3 echo +1 2 3 +4 5 6 +7 8 9 +.Ed +.Pp +Duplicate every line from standard input: +.Bd -literal -offset indent +$ echo -e "one\\ntwo\\nthree" | xargs -I % echo % % +one one +two two +three three +.Ed +.Pp +Execute at most 2 concurrent instances of +.Xr find 1 +every one of them using one of the directories from the standard input: +.Bd -literal -offset indent +echo -e "/usr/ports\\n/etc\\n/usr/local" | xargs -J % -P2 -n1 find % -name file +.Ed +.Sh SEE ALSO +.Xr echo 1 , +.Xr find 1 , +.Xr execvp 3 +.Sh STANDARDS +The +.Nm +utility is expected to be +.St -p1003.2 +compliant. +The +.Fl J , o , P , R +and +.Fl S +options are non-standard +.Fx +extensions which may not be available on other operating systems. +.Sh HISTORY +The +.Nm +utility appeared in PWB UNIX. +.Sh BUGS +If +.Ar utility +attempts to invoke another command such that the number of arguments or the +size of the environment is increased, it risks +.Xr execvp 3 +failing with +.Er E2BIG . +.Pp +The +.Nm +utility does not take multibyte characters into account when performing +string comparisons for the +.Fl I +and +.Fl J +options, which may lead to incorrect results in some locales. diff --git a/src/xargs/xargs.c b/src/xargs/xargs.c new file mode 100644 index 0000000..ce79401 --- /dev/null +++ b/src/xargs/xargs.c @@ -0,0 +1,814 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * John B. Roll Jr. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the University 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 REGENTS 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 REGENTS 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. + * + * $xMach: xargs.c,v 1.6 2002/02/23 05:27:47 tim Exp $ + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pathnames.h" + +static void parse_input(int, char *[]); +static void prerun(int, char *[]); +static int prompt(void); +static void run(char **); +static void usage(void); +bool strnsubst(char **, const char *, const char *, size_t); +static pid_t xwait(int block, int *status); +static void xexit(const char *, const int); +static void waitchildren(const char *, int); +static void pids_init(void); +static int pids_empty(void); +static int pids_full(void); +static void pids_add(pid_t pid); +static int pids_remove(pid_t pid); +static int findslot(pid_t pid); +static int findfreeslot(void); +static void clearslot(int slot); + +static char echo[] = _PATH_ECHO; +static char **av, **bxp, **ep, **endxp, **xp; +static char *argp, *bbp, *ebp, *inpline, *p, *replstr; +static const char *eofstr; +static long eoflen; +static int count, insingle, indouble, oflag, pflag, tflag, Rflag, rval, zflag; +static int cnt, Iflag, jfound, Lflag, Sflag, wasquoted, xflag; +static int curprocs, maxprocs; +static pid_t *childpids; + +static volatile int childerr; + +extern char **environ; + +static const char *optstr = "+0E:I:J:L:n:oP:pR:S:s:rtx"; + +static const struct option long_options[] = +{ + {"exit", no_argument, NULL, 'x'}, + {"interactive", no_argument, NULL, 'p'}, + {"max-args", required_argument, NULL, 'n'}, + {"max-chars", required_argument, NULL, 's'}, + {"max-procs", required_argument, NULL, 'P'}, + {"no-run-if-empty", no_argument, NULL, 'r'}, + {"null", no_argument, NULL, '0'}, + {"verbose", no_argument, NULL, 't'}, + + {NULL, no_argument, NULL, 0}, +}; + +int +xargs_main(int argc, char *argv[]) +{ + long arg_max; + int ch, Jflag, nargs, nflag, nline; + size_t linelen; + struct rlimit rl; + const char *errstr; + + /* Reset global state for multicall use */ + av = bxp = ep = endxp = xp = NULL; + argp = bbp = ebp = inpline = p = replstr = NULL; + eofstr = ""; + eoflen = 0; + count = insingle = indouble = oflag = pflag = tflag = Rflag = rval = zflag = 0; + cnt = Iflag = jfound = Lflag = Sflag = wasquoted = xflag = 0; + curprocs = 0; + maxprocs = 0; + childpids = NULL; + childerr = 0; + optind = 1; + + inpline = replstr = NULL; + ep = environ; + Jflag = nflag = 0; + + (void)setlocale(LC_ALL, ""); + + /* + * POSIX.2 limits the exec line length to ARG_MAX - 2K. Running that + * caused some E2BIG errors, so it was changed to ARG_MAX - 4K. Given + * that the smallest argument is 2 bytes in length, this means that + * the number of arguments is limited to: + * + * (ARG_MAX - 4K - LENGTH(utility + arguments)) / 2. + * + * We arbitrarily limit the number of arguments to 5000. This is + * allowed by POSIX.2 as long as the resulting minimum exec line is + * at least LINE_MAX. Realloc'ing as necessary is possible, but + * probably not worthwhile. + */ + nargs = 5000; + if ((arg_max = sysconf(_SC_ARG_MAX)) == -1) + errx(1, "sysconf(_SC_ARG_MAX) failed"); + nline = arg_max - 4 * 1024; + while (*ep != NULL) { + /* 1 byte for each '\0' */ + nline -= strlen(*ep++) + 1 + sizeof(*ep); + } + maxprocs = 1; + while ((ch = getopt_long(argc, argv, optstr, long_options, NULL)) != -1) + switch (ch) { + case 'E': + eofstr = optarg; + eoflen = strlen(eofstr); + break; + case 'I': + Jflag = 0; + Iflag = 1; + Lflag = 1; + replstr = optarg; + break; + case 'J': + Iflag = 0; + Jflag = 1; + replstr = optarg; + break; + case 'L': + Lflag = (int)strtonum(optarg, 1, INT_MAX, &errstr); + if (errstr) + errx(1, "-%c %s: %s", ch, optarg, errstr); + break; + case 'n': + nflag = 1; + nargs = (int)strtonum(optarg, 1, arg_max, &errstr); + if (errstr) + errx(1, "-%c %s: %s", ch, optarg, errstr); + break; + case 'o': + oflag = 1; + break; + case 'P': + maxprocs = (int)strtonum(optarg, 0, INT_MAX, &errstr); + if (errstr) + errx(1, "-%c %s: %s", ch, optarg, errstr); + if (getrlimit(RLIMIT_NPROC, &rl) != 0) + errx(1, "getrlimit failed"); + if (maxprocs == 0 || maxprocs > rl.rlim_cur) + maxprocs = rl.rlim_cur; + break; + case 'p': + pflag = 1; + break; + case 'R': + Rflag = (int)strtonum(optarg, INT_MIN, INT_MAX, &errstr); + if (errstr) + errx(1, "-%c %s: %s", ch, optarg, errstr); + if (!Rflag) + errx(1, "-%c %s: %s", ch, optarg, "must be non-zero"); + break; + case 'r': + /* GNU compatibility */ + break; + case 'S': + Sflag = (int)strtonum(optarg, 0, INT_MAX, &errstr); + if (errstr) + errx(1, "-%c %s: %s", ch, optarg, errstr); + break; + case 's': + nline = (int)strtonum(optarg, 0, INT_MAX, &errstr); + if (errstr) + errx(1, "-%c %s: %s", ch, optarg, errstr); + break; + case 't': + tflag = 1; + break; + case 'x': + xflag = 1; + break; + case '0': + zflag = 1; + break; + case '?': + default: + usage(); + } + argc -= optind; + argv += optind; + + if (!Iflag && Rflag) + usage(); + if (!Iflag && Sflag) + usage(); + if (Iflag && !Rflag) + Rflag = 5; + if (Iflag && !Sflag) + Sflag = 255; + if (xflag && !nflag) + usage(); + if (Iflag || Lflag) + xflag = 1; + if (replstr != NULL && *replstr == '\0') + errx(1, "replstr may not be empty"); + + pids_init(); + + /* + * Allocate pointers for the utility name, the utility arguments, + * the maximum arguments to be read from stdin and the trailing + * NULL. + */ + linelen = 1 + argc + (size_t)nargs + 1; + if ((av = bxp = malloc(linelen * sizeof(char *))) == NULL) + errx(1, "malloc failed"); + + /* + * Use the user's name for the utility as argv[0], just like the + * shell. Echo is the default. Set up pointers for the user's + * arguments. + */ + if (*argv == NULL) + cnt = strlen(*bxp++ = echo); + else { + do { + if (Jflag && strcmp(*argv, replstr) == 0) { + char **avj; + jfound = 1; + argv++; + for (avj = argv; *avj; avj++) + cnt += strlen(*avj) + 1; + break; + } + cnt += strlen(*bxp++ = *argv) + 1; + } while (*++argv != NULL); + } + + /* + * Set up begin/end/traversing pointers into the array. The -n + * count doesn't include the trailing NULL pointer, so the malloc + * added in an extra slot. + */ + endxp = (xp = bxp) + nargs; + + /* + * Allocate buffer space for the arguments read from stdin and the + * trailing NULL. Buffer space is defined as the default or specified + * space, minus the length of the utility name and arguments. Set up + * begin/end/traversing pointers into the array. The -s count does + * include the trailing NULL, so the malloc didn't add in an extra + * slot. + */ + nline -= cnt; + if (nline <= 0) + errx(1, "insufficient space for command"); + + if ((bbp = malloc((size_t)(nline + 1))) == NULL) + errx(1, "malloc failed"); + ebp = (argp = p = bbp) + nline - 1; + for (;;) + parse_input(argc, argv); +} + +static void +parse_input(int argc, char *argv[]) +{ + int ch, foundeof; + char **avj; + + foundeof = 0; + + switch (ch = getchar()) { + case EOF: + /* No arguments since last exec. */ + if (p == bbp) { + waitchildren(*av, 1); + exit(rval); + } + goto arg1; + case ' ': + case '\t': + /* Quotes escape tabs and spaces. */ + if (insingle || indouble || zflag) + goto addch; + goto arg2; + case '\0': + if (zflag) { + /* + * Increment 'count', so that nulls will be treated + * as end-of-line, as well as end-of-argument. This + * is needed so -0 works properly with -I and -L. + */ + count++; + goto arg2; + } + goto addch; + case '\n': + if (zflag) + goto addch; + count++; /* Indicate end-of-line (used by -L) */ + + /* Quotes do not escape newlines. */ +arg1: if (insingle || indouble) { + warnx("unterminated quote"); + xexit(*av, 1); + } +arg2: + foundeof = eoflen != 0 && p - argp == eoflen && + strncmp(argp, eofstr, eoflen) == 0; + + /* Do not make empty args unless they are quoted */ + if ((argp != p || wasquoted) && !foundeof) { + *p++ = '\0'; + *xp++ = argp; + if (Iflag) { + size_t curlen; + + if (inpline == NULL) + curlen = 0; + else { + /* + * If this string is not zero + * length, append a space for + * separation before the next + * argument. + */ + if ((curlen = strlen(inpline))) + strcat(inpline, " "); + } + curlen++; + /* + * Allocate enough to hold what we will + * be holding in a second, and to append + * a space next time through, if we have + * to. + */ + inpline = realloc(inpline, curlen + 2 + + strlen(argp)); + if (inpline == NULL) { + warnx("realloc failed"); + xexit(*av, 1); + } + if (curlen == 1) + strcpy(inpline, argp); + else + strcat(inpline, argp); + } + } + + /* + * If max'd out on args or buffer, or reached EOF, + * run the command. If xflag and max'd out on buffer + * but not on args, object. Having reached the limit + * of input lines, as specified by -L is the same as + * maxing out on arguments. + */ + if (xp == endxp || p > ebp || ch == EOF || + (Lflag <= count && xflag) || foundeof) { + if (xflag && xp != endxp && p > ebp) { + warnx("insufficient space for arguments"); + xexit(*av, 1); + } + if (jfound) { + for (avj = argv; *avj; avj++) + *xp++ = *avj; + } + prerun(argc, av); + if (ch == EOF || foundeof) { + waitchildren(*av, 1); + exit(rval); + } + p = bbp; + xp = bxp; + count = 0; + } + argp = p; + wasquoted = 0; + break; + case '\'': + if (indouble || zflag) + goto addch; + insingle = !insingle; + wasquoted = 1; + break; + case '"': + if (insingle || zflag) + goto addch; + indouble = !indouble; + wasquoted = 1; + break; + case '\\': + if (zflag) + goto addch; + /* Backslash escapes anything, is escaped by quotes. */ + if (!insingle && !indouble && (ch = getchar()) == EOF) { + warnx("backslash at EOF"); + xexit(*av, 1); + } + /* FALLTHROUGH */ + default: +addch: if (p < ebp) { + *p++ = ch; + break; + } + + /* If only one argument, not enough buffer space. */ + if (bxp == xp) { + warnx("insufficient space for argument"); + xexit(*av, 1); + } + /* Didn't hit argument limit, so if xflag object. */ + if (xflag) { + warnx("insufficient space for arguments"); + xexit(*av, 1); + } + + if (jfound) { + for (avj = argv; *avj; avj++) + *xp++ = *avj; + } + prerun(argc, av); + xp = bxp; + cnt = ebp - argp; + memcpy(bbp, argp, (size_t)cnt); + p = (argp = bbp) + cnt; + *p++ = ch; + break; + } +} + +/* + * Do things necessary before run()'ing, such as -I substitution, + * and then call run(). + */ +static void +prerun(int argc, char *argv[]) +{ + char **tmp, **tmp2, **avj; + int repls; + + repls = Rflag; + + if (argc == 0 || repls == 0) { + *xp = NULL; + run(argv); + return; + } + + avj = argv; + + /* + * Allocate memory to hold the argument list, and + * a NULL at the tail. + */ + tmp = malloc((argc + 1) * sizeof(char *)); + if (tmp == NULL) { + warnx("malloc failed"); + xexit(*argv, 1); + } + tmp2 = tmp; + + /* + * Save the first argument and iterate over it, we + * cannot do strnsubst() to it. + */ + if ((*tmp++ = strdup(*avj++)) == NULL) { + warnx("strdup failed"); + xexit(*argv, 1); + } + + /* + * For each argument to utility, if we have not used up + * the number of replacements we are allowed to do, and + * if the argument contains at least one occurrence of + * replstr, call strnsubst(), else just save the string. + * Iterations over elements of avj and tmp are done + * where appropriate. + */ + while (--argc) { + *tmp = *avj++; + if (repls && strstr(*tmp, replstr) != NULL) { + if (strnsubst(tmp++, replstr, inpline, (size_t)Sflag)) { + warnx("command line cannot be assembled, too long"); + xexit(*argv, 1); + } + if (repls > 0) + repls--; + } else { + if ((*tmp = strdup(*tmp)) == NULL) { + warnx("strdup failed"); + xexit(*argv, 1); + } + tmp++; + } + } + + /* + * Run it. + */ + *tmp = NULL; + run(tmp2); + + /* + * Walk from the tail to the head, free along the way. + */ + for (; tmp2 != tmp; tmp--) + free(*tmp); + /* + * Now free the list itself. + */ + free(tmp2); + + /* + * Free the input line buffer, if we have one. + */ + if (inpline != NULL) { + free(inpline); + inpline = NULL; + } +} + +static void +run(char **argv) +{ + pid_t pid; + int fd; + char **avec; + + /* + * If the user wants to be notified of each command before it is + * executed, notify them. If they want the notification to be + * followed by a prompt, then prompt them. + */ + if (tflag || pflag) { + (void)fprintf(stderr, "%s", *argv); + for (avec = argv + 1; *avec != NULL; ++avec) + (void)fprintf(stderr, " %s", *avec); + /* + * If the user has asked to be prompted, do so. + */ + if (pflag) + /* + * If they asked not to exec, return without execution + * but if they asked to, go to the execution. If we + * could not open their tty, break the switch and drop + * back to -t behaviour. + */ + switch (prompt()) { + case 0: + return; + case 1: + goto exec; + case 2: + break; + } + (void)fprintf(stderr, "\n"); + (void)fflush(stderr); + } +exec: + childerr = 0; + switch (pid = vfork()) { + case -1: + warn("vfork"); + xexit(*argv, 1); + case 0: + if (oflag) { + if ((fd = open(_PATH_TTY, O_RDONLY)) == -1) + err(1, "can't open /dev/tty"); + } else { + fd = open(_PATH_DEVNULL, O_RDONLY); + } + if (fd > STDIN_FILENO) { + if (dup2(fd, STDIN_FILENO) != 0) + err(1, "can't dup2 to stdin"); + close(fd); + } + execvp(argv[0], argv); + childerr = errno; + _exit(1); + } + pids_add(pid); + waitchildren(*argv, 0); +} + +/* + * Wait for a tracked child to exit and return its pid and exit status. + * + * Ignores (discards) all untracked child processes. + * Returns -1 and sets errno to ECHILD if no tracked children exist. + * If block is set, waits indefinitely for a child process to exit. + * If block is not set and no children have exited, returns 0 immediately. + */ +static pid_t +xwait(int block, int *status) { + pid_t pid; + + if (pids_empty()) { + errno = ECHILD; + return (-1); + } + + while ((pid = waitpid(-1, status, block ? 0 : WNOHANG)) > 0) + if (pids_remove(pid)) + break; + + return (pid); +} + +static void +xexit(const char *name, const int exit_code) { + waitchildren(name, 1); + exit(exit_code); +} + +static void +waitchildren(const char *name, int waitall) +{ + pid_t pid; + int status; + int cause_exit = 0; + + while ((pid = xwait(waitall || pids_full(), &status)) > 0) { + /* + * If we couldn't invoke the utility or if utility exited + * because of a signal or with a value of 255, warn (per + * POSIX), and then wait until all other children have + * exited before exiting 1-125. POSIX requires us to stop + * reading if child exits because of a signal or with 255, + * but it does not require us to exit immediately; waiting + * is preferable to orphaning. + */ + if (childerr != 0 && cause_exit == 0) { + errno = childerr; + waitall = 1; + cause_exit = errno == ENOENT ? 127 : 126; + warn("%s", name); + } else if (WIFSIGNALED(status)) { + waitall = cause_exit = 1; + warnx("%s: terminated with signal %d; aborting", + name, WTERMSIG(status)); + } else if (WEXITSTATUS(status) == 255) { + waitall = cause_exit = 1; + warnx("%s: exited with status 255; aborting", name); + } else if (WEXITSTATUS(status)) + rval = 1; + } + + if (cause_exit) + exit(cause_exit); + if (pid == -1 && errno != ECHILD) + err(1, "waitpid"); +} + +#define NOPID (0) + +static void +pids_init(void) +{ + int i; + + if ((childpids = malloc(maxprocs * sizeof(*childpids))) == NULL) + errx(1, "malloc failed"); + + for (i = 0; i < maxprocs; i++) + clearslot(i); +} + +static int +pids_empty(void) +{ + + return (curprocs == 0); +} + +static int +pids_full(void) +{ + + return (curprocs >= maxprocs); +} + +static void +pids_add(pid_t pid) +{ + int slot; + + slot = findfreeslot(); + childpids[slot] = pid; + curprocs++; +} + +static int +pids_remove(pid_t pid) +{ + int slot; + + if ((slot = findslot(pid)) < 0) + return (0); + + clearslot(slot); + curprocs--; + return (1); +} + +static int +findfreeslot(void) +{ + int slot; + + if ((slot = findslot(NOPID)) < 0) + errx(1, "internal error: no free pid slot"); + return (slot); +} + +static int +findslot(pid_t pid) +{ + int slot; + + for (slot = 0; slot < maxprocs; slot++) + if (childpids[slot] == pid) + return (slot); + return (-1); +} + +static void +clearslot(int slot) +{ + + childpids[slot] = NOPID; +} + +/* + * Prompt the user about running a command. + */ +static int +prompt(void) +{ + regex_t cre; + size_t rsize; + int match; + char *response; + FILE *ttyfp; + + if ((ttyfp = fopen(_PATH_TTY, "r")) == NULL) + return (2); /* Indicate that the TTY failed to open. */ + (void)fprintf(stderr, "?..."); + (void)fflush(stderr); + if ((response = fgetln(ttyfp, &rsize)) == NULL || + regcomp(&cre, nl_langinfo(YESEXPR), REG_EXTENDED) != 0) { + (void)fclose(ttyfp); + return (0); + } + response[rsize - 1] = '\0'; + match = regexec(&cre, response, 0, NULL, 0); + (void)fclose(ttyfp); + regfree(&cre); + return (match == 0); +} + +static void +usage(void) +{ + + fprintf(stderr, +"usage: xargs [-0opt] [-E eofstr] [-I replstr [-R replacements] [-S replsize]]\n" +" [-J replstr] [-L number] [-n number [-x]] [-P maxprocs]\n" +" [-s size] [utility [argument ...]]\n"); + exit(1); +} diff --git a/tests/harness.sh b/tests/harness.sh new file mode 100755 index 0000000..3b56097 --- /dev/null +++ b/tests/harness.sh @@ -0,0 +1,109 @@ +#!/bin/sh +# tests/harness.sh - Minimal POSIX test harness for VX. +# +# Source this file at the top of each test script. It provides: +# check_exit CMD EXPECTED_EXIT -- assert exit code +# check_eq ACTUAL EXPECTED MSG -- assert string equality +# check_file ACTUAL_FILE EXPECTED_FILE MSG -- assert file equality +# check_grep PATTERN FILE MSG -- assert pattern in file +# pass / fail / summary +# +# The test script must call summary() at the end. + +_pass=0 +_fail=0 +_harness_tmpdir="" + +# Resolve VX binary: prefer $VX env var, else ../builddir/vx from script dir +if [ -z "$VX" ]; then + _script_dir="$(cd "$(dirname "$0")" && pwd)" + VX="${_script_dir}/../../builddir/vx" +fi +export VX + +setup_tmpdir() { + _harness_tmpdir="$(mktemp -d)" + cd "$_harness_tmpdir" || exit 1 +} + +cleanup_tmpdir() { + if [ -n "$_harness_tmpdir" ] && [ -d "$_harness_tmpdir" ]; then + rm -rf "$_harness_tmpdir" + fi +} + +pass() { + _pass=$((_pass + 1)) + printf " PASS: %s\n" "$1" +} + +fail() { + _fail=$((_fail + 1)) + printf " FAIL: %s\n" "$1" + if [ -n "$2" ]; then + printf " %s\n" "$2" + fi +} + +# check_exit "description" expected_exit command [args...] +check_exit() { + _desc="$1"; shift + _expect="$1"; shift + "$@" >/dev/null 2>&1 + _got=$? + if [ "$_got" -eq "$_expect" ]; then + pass "$_desc" + else + fail "$_desc" "expected exit=$_expect, got exit=$_got" + fi +} + +# check_output "description" "expected_stdout" command [args...] +check_output() { + _desc="$1"; shift + _expect="$1"; shift + _got="$("$@" 2>/dev/null)" || true + if [ "$_got" = "$_expect" ]; then + pass "$_desc" + else + fail "$_desc" "expected: $(printf '%s' "$_expect" | head -c 200), got: $(printf '%s' "$_got" | head -c 200)" + fi +} + +# check_eq "got" "expected" "description" +check_eq() { + if [ "$1" = "$2" ]; then + pass "$3" + else + fail "$3" "expected: $(printf '%s' "$2" | head -c 200), got: $(printf '%s' "$1" | head -c 200)" + fi +} + +# check_file actual_file expected_file "description" +check_file() { + if diff -q "$1" "$2" >/dev/null 2>&1; then + pass "$3" + else + fail "$3" "files differ: $1 vs $2" + fi +} + +# check_grep pattern file "description" +check_grep() { + if grep -q "$1" "$2" 2>/dev/null; then + pass "$3" + else + fail "$3" "pattern '$1' not found in $2" + fi +} + +summary() { + cleanup_tmpdir + _total=$((_pass + _fail)) + echo "" + printf "%s: %d passed, %d failed (of %d)\n" "$_test_name" "$_pass" "$_fail" "$_total" + if [ "$_fail" -gt 0 ]; then + exit 1 + fi + exit 0 +} diff --git a/tests/test_cmp.sh b/tests/test_cmp.sh new file mode 100755 index 0000000..b933623 --- /dev/null +++ b/tests/test_cmp.sh @@ -0,0 +1,60 @@ +#!/bin/sh +# tests/test_cmp.sh - Tests for cmp applet +_test_name="cmp" +. "$(dirname "$0")/harness.sh" +_srcdir="$(cd "$(dirname "$0")/../src/cmp/tests" && pwd)" + +setup_tmpdir +CMP="$VX cmp" + +echo "Running cmp tests..." + +# --- basic: identical files --- +echo "0123456789abcdef" > a +cp a a_copy +check_exit "basic: identical files exit 0" 0 $CMP a a_copy + +# --- basic: different files --- +echo "0123456789abcdeg" > b +check_exit "basic: different files exit 1" 1 $CMP a b + +# --- stdin comparison --- +check_exit "special: cmp a - (stdin identical)" 0 sh -c "$CMP a - < a" +check_exit "special: cmp - a (stdin identical)" 0 sh -c "$CMP - a < a" +check_exit "special: cmp a - (stdin different)" 1 sh -c "$CMP a - < b" +check_exit "special: cmp - a (stdin different)" 1 sh -c "$CMP - a < b" + +# --- silent mode --- +check_exit "silent: -s identical" 0 $CMP -s a a_copy +check_exit "silent: -s different" 1 $CMP -s a b + +# --- byte offset skip --- +echo -n '1234567890' > skip_a +echo -n 'abc567890' > skip_b +check_exit "skip: -i 4:3 matches" 0 $CMP -s -i 4:3 skip_a skip_b + +# --- limit (-n) --- +echo -n "aaaabbbb" > lim_a +echo -n "aaaaxxxx" > lim_b +check_exit "limit: -n 4 identical" 0 $CMP -s -n 4 lim_a lim_b +check_exit "limit: -n 5 different" 1 $CMP -s -n 5 lim_a lim_b +check_exit "limit: full different" 1 $CMP -s lim_a lim_b + +# --- -b flag (print differing bytes) --- +echo -n "abcd" > a +echo -n "abdd" > b +_got="$($CMP -b a b 2>/dev/null)" || true +_exp="$(cat "$_srcdir/b_flag.out")" +check_eq "$_got" "$_exp" "bflag: -b output matches" + +# --- -bl flags --- +_got="$($CMP -bl a b 2>/dev/null)" || true +_exp="$(cat "$_srcdir/bl_flag.out")" +check_eq "$_got" "$_exp" "blflag: -bl output matches" + +# --- nonexistent file --- +$CMP a NOFILE >/dev/null 2>&1 +_rc=$? +check_eq "$_rc" "2" "error: nonexistent file exits 2" + +summary diff --git a/tests/test_diff.sh b/tests/test_diff.sh new file mode 100755 index 0000000..d088764 --- /dev/null +++ b/tests/test_diff.sh @@ -0,0 +1,101 @@ +#!/bin/sh +# tests/test_diff.sh - Tests for diff applet +_test_name="diff" +. "$(dirname "$0")/harness.sh" +_srcdir="$(cd "$(dirname "$0")/../src/diff/tests" && pwd)" + +setup_tmpdir +DIFF="$VX diff" + +echo "Running diff tests..." + +# --- simple: normal, ed, unified, nreverse, brief, ignore-case, whitespace --- +check_exit "simple: normal diff" 1 \ + $DIFF "$_srcdir/input1.in" "$_srcdir/input2.in" + +_got="$($DIFF "$_srcdir/input1.in" "$_srcdir/input2.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/simple.out")" +check_eq "$_got" "$_exp" "simple: normal output matches" + +_got="$($DIFF -e "$_srcdir/input1.in" "$_srcdir/input2.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/simple_e.out")" +check_eq "$_got" "$_exp" "simple: ed output matches" + +_got="$($DIFF -u -L input1 -L input2 "$_srcdir/input1.in" "$_srcdir/input2.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/simple_u.out")" +check_eq "$_got" "$_exp" "simple: unified output matches" + +_got="$($DIFF -n "$_srcdir/input1.in" "$_srcdir/input2.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/simple_n.out")" +check_eq "$_got" "$_exp" "simple: nreverse output matches" + +# brief mode +$DIFF -q "$_srcdir/input1.in" "$_srcdir/input2.in" >/dev/null 2>&1 +check_eq "$?" "1" "simple: brief exit 1 on different" + +check_exit "simple: brief exit 0 on identical" 0 \ + $DIFF -q "$_srcdir/input1.in" "$_srcdir/input1.in" + +# ignore case +_got="$($DIFF -i "$_srcdir/input_c1.in" "$_srcdir/input_c2.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/simple_i.out")" +check_eq "$_got" "$_exp" "simple: ignore-case output matches" + +# ignore whitespace +_got="$($DIFF -w "$_srcdir/input_c1.in" "$_srcdir/input_c2.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/simple_w.out")" +check_eq "$_got" "$_exp" "simple: ignore-whitespace output matches" + +# ignore space changes +_got="$($DIFF -b "$_srcdir/input_c1.in" "$_srcdir/input_c2.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/simple_b.out")" +check_eq "$_got" "$_exp" "simple: space-change output matches" + +# --- unified with function names --- +_got="$($DIFF -up -L input_c1.in -L input_c2.in "$_srcdir/input_c1.in" "$_srcdir/input_c2.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/unified_p.out")" +check_eq "$_got" "$_exp" "unified: -p output matches" + +# --- strip trailing CR --- +printf 'a\nb\r\nc\n' > cr_a.in +printf 'a\r\nb\r\nc\r\n' > cr_b.in +check_exit "stripcr: identical after strip" 0 \ + $DIFF -up --strip-trailing-cr -L a -L b cr_a.in cr_b.in + +# --- -B (ignore blank lines) --- +printf "A\nB\n" > bflag_a +printf "A\n\nB\n" > bflag_b +check_exit "Bflag: ignore blank lines" 0 $DIFF -B bflag_a bflag_b + +# --- -w (ignore all whitespace) --- +printf 'a b\n' > ws_a +printf 'a b\n' > ws_b +check_exit "whitespace: -qw identical" 0 $DIFF -qw ws_a ws_b + +# --- side-by-side --- +printf "A\nB\nC\n" > sby_a +printf "D\nB\nE\n" > sby_b +$DIFF -y sby_a sby_b > sby_out 2>&1 || true +check_grep "B" sby_out "side-by-side: contains common line" +check_grep "|" sby_out "side-by-side: contains change marker" + +# --- brief recursive --- +mkdir -p rA rB +echo 1 > rA/test-file +echo 2 > rB/test-file +_got="$($DIFF -rq rA rB 2>/dev/null)" || true +check_eq "$_got" "Files rA/test-file and rB/test-file differ" "brief: recursive diff" + +# --- identical files --- +check_exit "identical: exit 0" 0 $DIFF "$_srcdir/input1.in" "$_srcdir/input1.in" + +# --- Nflag: treat absent files as empty --- +printf "foo" > nflag_a +check_exit "Nflag: absent second file" 1 $DIFF -N nflag_a NOFILE + +# --- conflicting format flags --- +printf "A\n" > cfmt_a +printf "B\n" > cfmt_b +check_exit "conflicting: -c -u exits 2" 2 $DIFF -c -u cfmt_a cfmt_b + +summary diff --git a/tests/test_diff3.sh b/tests/test_diff3.sh new file mode 100755 index 0000000..a3bf4fa --- /dev/null +++ b/tests/test_diff3.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# tests/test_diff3.sh - Tests for diff3 applet +_test_name="diff3" +. "$(dirname "$0")/harness.sh" +_srcdir="$(cd "$(dirname "$0")/../src/diff3/tests" && pwd)" + +setup_tmpdir +DIFF3="$VX diff3" + +echo "Running diff3 tests..." + +# --- basic 3-way diff --- +_got="$($DIFF3 "$_srcdir/1.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" 2>/dev/null)" +_exp="$(cat "$_srcdir/1.out")" +check_eq "$_got" "$_exp" "basic: default output" + +# --- strip trailing CR --- +_got="$($DIFF3 --strip-trailing-cr "$_srcdir/1cr.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" 2>/dev/null)" +_exp="$(cat "$_srcdir/1.out")" +check_eq "$_got" "$_exp" "stripcr: matches default output" + +# --- -T flag --- +_got="$($DIFF3 -T "$_srcdir/1.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" 2>/dev/null)" +_exp="$(cat "$_srcdir/1t.out")" +check_eq "$_got" "$_exp" "Tflag: initial tab output" + +# --- -e (ed script) --- +_got="$($DIFF3 -e "$_srcdir/1.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" 2>/dev/null)" +_exp="$(cat "$_srcdir/2.out")" +check_eq "$_got" "$_exp" "ed: -e output" + +# --- -E (overlap ed script with labels) --- +_got="$($DIFF3 -E -L 1 -L 2 -L 3 "$_srcdir/1.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" 2>/dev/null)" +_exp="$(cat "$_srcdir/3.out")" +check_eq "$_got" "$_exp" "overlap: -E output" + +# --- -X (only overlapping changes) --- +_got="$($DIFF3 -X -L 1 -L 2 -L 3 "$_srcdir/1.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" 2>/dev/null)" +_exp="$(cat "$_srcdir/4.out")" +check_eq "$_got" "$_exp" "Xflag: -X output" + +# --- -x (only overlapping, no labels) --- +_got="$($DIFF3 -x "$_srcdir/1.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" 2>/dev/null)" +_exp="$(cat "$_srcdir/5.out")" +check_eq "$_got" "$_exp" "xflag: -x output" + +# --- -3 (ed script for file 3 changes only) --- +_got="$($DIFF3 -3 "$_srcdir/1.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" 2>/dev/null)" +_exp="$(cat "$_srcdir/6.out")" +check_eq "$_got" "$_exp" "3flag: -3 output" + +# --- -i (append w/q commands) --- +_got="$($DIFF3 -i "$_srcdir/1.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" 2>/dev/null)" +_exp="$(cat "$_srcdir/7.out")" +check_eq "$_got" "$_exp" "iflag: -i output" + +# --- -A (show all changes) --- +$DIFF3 -A -L 1 -L 2 -L 3 "$_srcdir/1.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" > Aout 2>/dev/null +_rc=$? +check_eq "$_rc" "1" "Aflag: exits 1 on conflicts" +check_file Aout "$_srcdir/8.out" "Aflag: output matches" + +# --- long ed script --- +_got="$($DIFF3 -e "$_srcdir/long-m.txt" "$_srcdir/long-o.txt" "$_srcdir/long-y.txt" 2>/dev/null)" +_exp="$(cat "$_srcdir/long-ed.out")" +check_eq "$_got" "$_exp" "long-ed: output matches" + +# --- long A --- +$DIFF3 -A -L long-m.txt -L long-o.txt -L long-y.txt \ + "$_srcdir/long-m.txt" "$_srcdir/long-o.txt" "$_srcdir/long-y.txt" > longAout 2>/dev/null +check_file longAout "$_srcdir/long-A.out" "long-A: output matches" + +# --- merge --- +$DIFF3 -m -L 1 -L 2 -L 3 "$_srcdir/1.txt" "$_srcdir/2.txt" "$_srcdir/3.txt" > mergeout 2>/dev/null +check_file mergeout "$_srcdir/9.out" "merge: output matches" + +$DIFF3 -m -L long-m.txt -L long-o.txt -L long-y.txt \ + "$_srcdir/long-m.txt" "$_srcdir/long-o.txt" "$_srcdir/long-y.txt" > longmergeout 2>/dev/null +check_file longmergeout "$_srcdir/long-merge.out" "long-merge: output matches" + +summary diff --git a/tests/test_find.sh b/tests/test_find.sh new file mode 100755 index 0000000..55dc8f8 --- /dev/null +++ b/tests/test_find.sh @@ -0,0 +1,60 @@ +#!/bin/sh +# tests/test_find.sh - Tests for find applet +_test_name="find" +. "$(dirname "$0")/harness.sh" + +setup_tmpdir +FIND="$VX find" + +echo "Running find tests..." + +mkdir -p tree/sub +printf 'hello\n' > tree/a.txt +printf 'world!!' > tree/sub/b.c +ln -s ../a.txt tree/sub/link_to_a +chmod 754 tree/sub/b.c + +a_count="$($FIND tree -name '*.txt' -type f | wc -l | tr -d ' ')" +check_eq "$a_count" "1" "basic: -name and -type" + +out="$($FIND tree -maxdepth 1 -type d | sort)" +exp="$(printf '%s\n' tree tree/sub)" +check_eq "$out" "$exp" "maxdepth: top-level directories" + +out="$($FIND tree/sub -name '*.c' -exec echo FOUND:{} ';')" +check_eq "$out" "FOUND:tree/sub/b.c" "exec: substitution works" + +out="$($FIND tree/sub -name 'b.c' -printf '%f|%h|%p|%s|%m\n')" +check_eq "$out" "b.c|tree/sub|tree/sub/b.c|7|0754" "printf: common directives" + +out="$($FIND tree/sub -name 'b.c' -printf '%TY-%Tm-%Td %TH:%TM\n')" +case "$out" in + ????-??-??' '??:??) pass "printf: time fields" ;; + *) fail "printf: time fields" "got: $out" ;; +esac + +out="$($FIND tree/sub -type l -printf '%l\n')" +check_eq "$out" "../a.txt" "printf: symlink target" + +expected_printf="$(printf 'A\nB\tC')" +out="$($FIND tree/sub -name 'b.c' -printf 'A\nB\tC')" +check_eq "$out" "$expected_printf" "printf: backslash escapes" + +fstype="$(stat -f -c '%T' . 2>/dev/null)" +if [ -n "$fstype" ]; then + out="$($FIND . -maxdepth 1 -fstype "$fstype" -name tree)" + check_eq "$out" "./tree" "fstype: current fs matches" +fi + +mkdir daystart +printf 'y\n' > daystart/late_yesterday +if touch -d 'yesterday 23:59' daystart/late_yesterday 2>/dev/null; then + out="$($FIND daystart -daystart -name late_yesterday -mtime 1)" + check_eq "$out" "daystart/late_yesterday" "daystart: shifts boundary to start of day" +else + pass "daystart: touch -d unavailable (skip)" +fi + +check_exit "exit: no path exits 1" 1 $FIND + +summary diff --git a/tests/test_grep.sh b/tests/test_grep.sh new file mode 100755 index 0000000..6da86db --- /dev/null +++ b/tests/test_grep.sh @@ -0,0 +1,171 @@ +#!/bin/sh +# tests/test_grep.sh - Tests for grep/egrep/fgrep applets +_test_name="grep" +. "$(dirname "$0")/harness.sh" + +setup_tmpdir +GREP="$VX grep" +EGREP="$VX egrep" +FGREP="$VX fgrep" + +echo "Running grep tests..." + +# --- create test files --- +cat > haystack.txt <<'EOF' +Hello World +hello world +HELLO WORLD +foo bar baz +The quick brown fox +jumps over the lazy dog +line with numbers 12345 +another line here +foobar +EOF + +cat > patterns.txt <<'EOF' +foo +bar +EOF + +# --- basic match --- +_got="$($GREP 'Hello' haystack.txt)" +check_eq "$_got" "Hello World" "basic: literal match" + +# --- case insensitive --- +_count="$($GREP -ic 'hello' haystack.txt)" +check_eq "$_count" "3" "iflag: case insensitive count" + +# --- -v invert --- +_count="$($GREP -vic 'hello' haystack.txt | tr -d ' ')" +_total="$(wc -l < haystack.txt | tr -d ' ')" +_expect=$((${_total} - 3)) +check_eq "$_count" "$_expect" "vflag: invert count" + +# --- -n line numbers --- +_got="$($GREP -n 'foo bar' haystack.txt)" +check_eq "$_got" "4:foo bar baz" "nflag: line number" + +# --- -c count --- +_got="$($GREP -c 'line' haystack.txt)" +check_eq "$_got" "2" "cflag: count lines" + +# --- -l files-with-matches --- +_got="$($GREP -l 'Hello' haystack.txt)" +check_eq "$_got" "haystack.txt" "lflag: filename" + +# --- -L files-without-match --- +echo "no match here" > other.txt +_got="$($GREP -L 'Hello' haystack.txt other.txt)" +check_eq "$_got" "other.txt" "Lflag: no-match filename" + +# --- -w word match --- +_got="$($GREP -wc 'foo' haystack.txt)" +check_eq "$_got" "1" "wflag: word boundary (foo not foobar)" + +# --- -x line match --- +_got="$($GREP -x 'foobar' haystack.txt)" +check_eq "$_got" "foobar" "xflag: exact line match" + +# --- -o only matching --- +_got="$($GREP -o 'fox' haystack.txt)" +check_eq "$_got" "fox" "oflag: only matching part" + +# --- -m max count --- +_got="$($GREP -m1 'line' haystack.txt)" +_count="$(printf '%s\n' "$_got" | wc -l | tr -d ' ')" +check_eq "$_count" "1" "mflag: max count 1" + +# --- -e multiple patterns --- +_count="$($GREP -c -e 'Hello' -e 'quick' haystack.txt)" +check_eq "$_count" "2" "eflag: multiple patterns" + +# --- -f pattern file --- +_count="$($GREP -c -f patterns.txt haystack.txt)" +check_eq "$_count" "2" "fflag: pattern file (foo, bar match 2 lines)" + +# --- -q quiet mode --- +$GREP -q 'Hello' haystack.txt +check_eq "$?" "0" "qflag: quiet match exits 0" +$GREP -q 'NOMATCH_XYZ' haystack.txt +check_eq "$?" "1" "qflag: quiet no-match exits 1" + +# --- -s suppress errors --- +$GREP -s 'test' nonexistent_file 2>err_out +_err="$(cat err_out)" +check_eq "$_err" "" "sflag: suppresses error messages" + +# --- -b byte offset --- +_got="$($GREP -b 'foo bar' haystack.txt | cut -d: -f1)" +# "foo bar" is on line 4, after 3 lines + newlines +test -n "$_got" +check_eq "$?" "0" "bflag: byte offset present" + +# --- -H / -h filename control --- +_got="$($GREP -H 'Hello' haystack.txt)" +echo "$_got" | grep -q '^haystack.txt:' +check_eq "$?" "0" "Hflag: filename shown" + +_got="$($GREP -h 'Hello' haystack.txt)" +echo "$_got" | grep -qv ':' +check_eq "$?" "0" "hflag: no filename" + +# --- extended regex (egrep / -E) --- +_got="$($GREP -Ec 'foo|quick' haystack.txt)" +check_eq "$_got" "3" "Eflag: extended regex" + +_got="$($EGREP -c 'foo|quick' haystack.txt)" +check_eq "$_got" "3" "egrep: dispatch works" + +# --- fixed strings (fgrep / -F) --- +_got="$($FGREP -c 'foo' haystack.txt)" +check_eq "$_got" "2" "fgrep: fixed strings" + +# --- context: -A after --- +_count="$($GREP -A1 'quick' haystack.txt | wc -l | tr -d ' ')" +check_eq "$_count" "2" "Aflag: after context" + +# --- context: -B before --- +_count="$($GREP -B1 'quick' haystack.txt | wc -l | tr -d ' ')" +check_eq "$_count" "2" "Bflag: before context" + +# --- context: -C both --- +_count="$($GREP -C1 'quick' haystack.txt | wc -l | tr -d ' ')" +check_eq "$_count" "3" "Cflag: context both sides" + +# --- stdin --- +_got="$(echo 'test line' | $GREP 'test')" +check_eq "$_got" "test line" "stdin: pipe works" + +# --- exit codes --- +check_exit "exit: match returns 0" 0 $GREP -q 'Hello' haystack.txt +check_exit "exit: no match returns 1" 1 $GREP -q 'ZZZNOTFOUND' haystack.txt +check_exit "exit: error returns 2" 2 $GREP 'test' --bogus-flag 2>/dev/null + +# --- recursive --- +mkdir -p subdir +echo "findme here" > subdir/nested.txt +_got="$($GREP -r 'findme' .)" +echo "$_got" | grep -q 'nested.txt' +check_eq "$?" "0" "rflag: recursive find" + +# --- --include --- +echo "match" > inc_yes.c +echo "match" > inc_no.txt +_got="$($GREP -r --include='*.c' 'match' .)" +echo "$_got" | grep -q 'inc_yes.c' +check_eq "$?" "0" "include: *.c matches" +echo "$_got" | grep -q 'inc_no.txt' +check_eq "$?" "1" "include: *.txt excluded" + +# --- --exclude --- +_got="$($GREP -r --exclude='*.txt' 'match' .)" +echo "$_got" | grep -q 'inc_yes.c' +check_eq "$?" "0" "exclude: *.c still matches" + +# --- -z null data --- +printf 'aaa\0bbb\0ccc\0' > nulldata +_count="$($GREP -zc 'bbb' nulldata)" +check_eq "$_count" "1" "zflag: null-delimited" + +summary diff --git a/tests/test_gzip.sh b/tests/test_gzip.sh new file mode 100755 index 0000000..ec2f48f --- /dev/null +++ b/tests/test_gzip.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# tests/test_gzip.sh - Tests for gzip/gunzip/zcat applets +_test_name="gzip" +. "$(dirname "$0")/harness.sh" + +setup_tmpdir +GZIP_CMD="$VX gzip" +GUNZIP="$VX gunzip" +ZCAT="$VX zcat" + +echo "Running gzip tests..." + +# --- basic compression/decompression --- +printf 'Hello, World!\n' > hello.txt +cp hello.txt hello.orig +$GZIP_CMD hello.txt +check_exit "basic: compress creates .gz" 0 test -f hello.txt.gz +check_exit "basic: original removed" 1 test -f hello.txt + +$GUNZIP hello.txt.gz +check_exit "basic: decompress creates original" 0 test -f hello.txt +check_file hello.txt hello.orig "basic: roundtrip matches" + +# --- -k (keep) --- +cp hello.orig keeptest.txt +$GZIP_CMD -k keeptest.txt +check_exit "keep: .gz created" 0 test -f keeptest.txt.gz +check_exit "keep: original kept" 0 test -f keeptest.txt + +# --- -c (stdout) --- +cp hello.orig stdout_test.txt +$GZIP_CMD -c stdout_test.txt > stdout_test.gz +check_exit "stdout: .gz created" 0 test -f stdout_test.gz +check_exit "stdout: original kept" 0 test -f stdout_test.txt +$GUNZIP -c stdout_test.gz > stdout_test_round.txt +check_file stdout_test_round.txt hello.orig "stdout: roundtrip matches" + +# --- zcat --- +_got="$($ZCAT stdout_test.gz)" +_exp="$(cat hello.orig)" +check_eq "$_got" "$_exp" "zcat: output matches original" + +# --- -d flag (decompress via gzip -d) --- +cp stdout_test.gz dtest.gz +$GZIP_CMD -d dtest.gz +check_exit "dflag: decompresses" 0 test -f dtest +check_file dtest hello.orig "dflag: output matches" + +# --- -f (force overwrite) --- +cp hello.orig force_test.txt +$GZIP_CMD force_test.txt +cp hello.orig force_test.txt +$GZIP_CMD -f force_test.txt +check_exit "force: overwrites .gz" 0 test -f force_test.txt.gz + +# --- -t (test integrity) --- +cp hello.orig ttest.txt +$GZIP_CMD ttest.txt +check_exit "test: valid gz passes" 0 $GZIP_CMD -t ttest.txt.gz + +# corrupt the gzip header magic bytes +printf '\x00\x00' | dd of=ttest.txt.gz bs=1 seek=0 conv=notrunc 2>/dev/null +check_exit "test: corrupt gz fails" 1 $GZIP_CMD -t ttest.txt.gz + +# --- -l (list) --- +cp hello.orig ltest.txt +$GZIP_CMD ltest.txt +$GZIP_CMD -l ltest.txt.gz > list_out 2>&1 +_rc=$? +check_eq "$_rc" "0" "list: exits 0" +check_grep "compressed" list_out "list: prints header" + +# --- -n (no name) --- +cp hello.orig ntest.txt +$GZIP_CMD -n ntest.txt +check_exit "nflag: compress succeeds" 0 test -f ntest.txt.gz + +# --- -1 through -9 (compression levels) --- +for lvl in 1 9; do + cp hello.orig "level${lvl}.txt" + check_exit "level: -${lvl} compresses" 0 $GZIP_CMD "-${lvl}" "level${lvl}.txt" +done + +# --- pipe mode (stdin/stdout) --- +$GZIP_CMD < hello.orig > pipe_test.gz +$GUNZIP < pipe_test.gz > pipe_test.txt +check_file pipe_test.txt hello.orig "pipe: stdin roundtrip" + +# --- multiple files --- +printf 'aaa\n' > multi_a.txt +printf 'bbb\n' > multi_b.txt +$GZIP_CMD multi_a.txt multi_b.txt +check_exit "multi: a.gz exists" 0 test -f multi_a.txt.gz +check_exit "multi: b.gz exists" 0 test -f multi_b.txt.gz + +# --- gunzip argv[0] dispatch --- +cp hello.orig gun_test.txt +$GZIP_CMD gun_test.txt +$GUNZIP gun_test.txt.gz +check_file gun_test.txt hello.orig "gunzip: dispatch works" + +# --- exit codes --- +check_exit "exit: compress nonexistent fails" 1 $GZIP_CMD nonexistent_file_xyz + +summary diff --git a/tests/test_patch.sh b/tests/test_patch.sh new file mode 100755 index 0000000..bc258ce --- /dev/null +++ b/tests/test_patch.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# tests/test_patch.sh - Tests for patch applet +_test_name="patch" +. "$(dirname "$0")/harness.sh" +_srcdir="$(cd "$(dirname "$0")/../src/patch/tests" && pwd)" + +setup_tmpdir +PATCH="$VX patch" + +echo "Running patch tests..." + +# --- basic: apply unified diffs from various starting points --- +printf "a\nb\nc\nd\ne\nf\ng\nh\ni\n" > foo_full +printf "a\nb\nc\n" > foo_start +printf "g\nh\ni\n" > foo_end +printf "d\ne\nf\n" > foo_middle + +$VX diff -u foo_start foo_full > foo_start2full.diff || true +$VX diff -u foo_end foo_full > foo_end2full.diff || true +$VX diff -u foo_middle foo_full > foo_mid2full.diff || true + +# Each diff should be 12 lines (9 content lines + 3 header) +_lines=$(wc -l < foo_start2full.diff | tr -d ' ') +check_eq "$_lines" "12" "basic: start diff is 12 lines" + +_lines=$(wc -l < foo_end2full.diff | tr -d ' ') +check_eq "$_lines" "12" "basic: end diff is 12 lines" + +_lines=$(wc -l < foo_mid2full.diff | tr -d ' ') +check_eq "$_lines" "12" "basic: mid diff is 12 lines" + +# Apply patches +$PATCH foo_start foo_start2full.diff -o foo_start2full >/dev/null 2>&1 +check_exit "basic: apply start->full" 0 diff -q foo_start2full foo_full + +$PATCH foo_end foo_end2full.diff -o foo_end2full >/dev/null 2>&1 +check_exit "basic: apply end->full" 0 diff -q foo_end2full foo_full + +$PATCH foo_middle foo_mid2full.diff -o foo_mid2full >/dev/null 2>&1 +check_exit "basic: apply mid->full" 0 diff -q foo_mid2full foo_full + +# --- limited context: PR74127 regression --- +check_exit "limited_ctx: bad diff rejected" 1 \ + $PATCH -o _.out "$_srcdir/PR74127.in" "$_srcdir/PR74127-repro.diff" + +check_exit "limited_ctx: good diff accepted" 0 \ + $PATCH -o _.out "$_srcdir/PR74127.in" "$_srcdir/PR74127-good.diff" + +# --- file creation --- +echo "x" > fc_foo +$VX diff -u /dev/null fc_foo > fc_foo.diff || true +rm fc_foo +$PATCH -s < fc_foo.diff >/dev/null 2>&1 +check_exit "file_creation: creates file" 0 test -f fc_foo + +# --- reverse removal --- +echo "x" > rev_foo +$VX diff -u /dev/null rev_foo > rev_foo.diff || true +$PATCH -Rs < rev_foo.diff >/dev/null 2>&1 +check_exit "file_removal: file removed" 1 test -f rev_foo + +summary diff --git a/tests/test_sdiff.sh b/tests/test_sdiff.sh new file mode 100755 index 0000000..8e3fb88 --- /dev/null +++ b/tests/test_sdiff.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# tests/test_sdiff.sh - Tests for sdiff applet +_test_name="sdiff" +. "$(dirname "$0")/harness.sh" +_srcdir="$(cd "$(dirname "$0")/../src/sdiff/tests" && pwd)" + +setup_tmpdir +SDIFF="$VX sdiff" + +echo "Running sdiff tests..." + +# --- flags: -l, -s, -w --- +_got="$($SDIFF -l "$_srcdir/d_input1" "$_srcdir/d_input2" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/d_flags_l.out")" +check_eq "$_got" "$_exp" "flags: -l output matches" + +_got="$($SDIFF -s "$_srcdir/d_input1" "$_srcdir/d_input2" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/d_flags_s.out")" +check_eq "$_got" "$_exp" "flags: -s output matches" + +_got="$($SDIFF -w 125 "$_srcdir/d_input1" "$_srcdir/d_input2" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/d_flags_w.out")" +check_eq "$_got" "$_exp" "flags: -w output matches" + +# --- same file --- +_got="$($SDIFF "$_srcdir/d_input1" "$_srcdir/d_input1" 2>/dev/null)" +_exp="$(cat "$_srcdir/d_same.out")" +check_eq "$_got" "$_exp" "same: identical file output matches" + +# --- tabs --- +_got="$($SDIFF "$_srcdir/d_tabs1.in" "$_srcdir/d_tabs2.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/d_tabs.out")" +check_eq "$_got" "$_exp" "tabs: tab output matches" + +# --- tabends --- +_got="$($SDIFF -w30 "$_srcdir/d_tabends.in" /dev/null 2>/dev/null)" || true +_exp="$(cat "$_srcdir/d_tabends_a.out")" +check_eq "$_got" "$_exp" "tabends: file vs null (a)" + +_got="$($SDIFF -w30 /dev/null "$_srcdir/d_tabends.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/d_tabends_b.out")" +check_eq "$_got" "$_exp" "tabends: null vs file (b)" + +_got="$($SDIFF -w19 "$_srcdir/d_tabends.in" /dev/null 2>/dev/null)" || true +_exp="$(cat "$_srcdir/d_tabends_c.out")" +check_eq "$_got" "$_exp" "tabends: narrow width (c)" + +# --- oneline --- +_got="$($SDIFF /dev/null "$_srcdir/d_oneline.in" 2>/dev/null)" || true +_exp="$(cat "$_srcdir/d_oneline_a.out")" +check_eq "$_got" "$_exp" "oneline: null vs file" + +_got="$($SDIFF "$_srcdir/d_oneline.in" /dev/null 2>/dev/null)" || true +_exp="$(cat "$_srcdir/d_oneline_b.out")" +check_eq "$_got" "$_exp" "oneline: file vs null" + +# --- basic side-by-side --- +printf "A\nB\nC\n" > sby_a +printf "D\nB\nE\n" > sby_b +$SDIFF sby_a sby_b > sby_out 2>&1 || true +check_grep "|" sby_out "side-by-side: contains change marker" +check_grep "B" sby_out "side-by-side: contains common line" + +# --- exit codes --- +printf "a\n" > ex_a +printf "b\n" > ex_b +cp ex_a ex_a2 +check_exit "exit: identical files exit 0" 0 $SDIFF ex_a ex_a2 +check_exit "exit: different files exit 1" 1 $SDIFF ex_a ex_b + +summary diff --git a/tests/test_which.sh b/tests/test_which.sh new file mode 100755 index 0000000..eb293f0 --- /dev/null +++ b/tests/test_which.sh @@ -0,0 +1,77 @@ +#!/bin/sh +# tests/test_which.sh - Tests for which applet +_test_name="which" +. "$(dirname "$0")/harness.sh" + +setup_tmpdir +WHICH="$VX which" + +echo "Running which tests..." + +# --- basic: find a known binary --- +_got="$($WHICH sh 2>/dev/null)" +# sh should resolve to something in PATH +if [ -n "$_got" ] && [ -x "$_got" ]; then + pass "basic: finds sh" +else + fail "basic: finds sh" "got: $_got" +fi + +# --- basic: nonexistent command --- +check_exit "basic: nonexistent exits 1" 1 $WHICH this_command_does_not_exist_xyz + +# --- -s (silent) --- +_got="$($WHICH -s sh 2>/dev/null)" +check_eq "$_got" "" "silent: -s produces no output" +check_exit "silent: -s sh exits 0" 0 $WHICH -s sh +check_exit "silent: -s nonexistent exits 1" 1 $WHICH -s this_command_does_not_exist_xyz + +# --- -a (all matches) --- +# Create a temp dir with a duplicate "testprog" and prepend it to PATH +mkdir -p bin1 bin2 +printf '#!/bin/sh\n' > bin1/testprog +printf '#!/bin/sh\n' > bin2/testprog +chmod +x bin1/testprog bin2/testprog +_old_path="$PATH" +PATH="$(pwd)/bin1:$(pwd)/bin2:$PATH" +export PATH + +# Without -a, should find exactly 1 +_count="$($WHICH testprog 2>/dev/null | wc -l | tr -d ' ')" +check_eq "$_count" "1" "allpaths: default finds 1" + +# With -a, should find at least 2 +_count="$($WHICH -a testprog 2>/dev/null | wc -l | tr -d ' ')" +if [ "$_count" -ge 2 ]; then + pass "allpaths: -a finds multiple" +else + fail "allpaths: -a finds multiple" "got $_count matches" +fi + +PATH="$_old_path" +export PATH + +# --- multiple arguments --- +$WHICH sh cat > multi_out 2>/dev/null +_lines=$(wc -l < multi_out | tr -d ' ') +check_eq "$_lines" "2" "multi: two args produce two lines" + +# --- absolute path passthrough --- +_got="$($WHICH /bin/sh 2>/dev/null)" +if [ "$_got" = "/bin/sh" ] || [ "$_got" = "" ]; then + # FreeBSD which passes through absolute paths if executable + if [ -x /bin/sh ]; then + check_eq "$_got" "/bin/sh" "absolute: /bin/sh passes through" + else + pass "absolute: /bin/sh not executable (skip)" + fi +else + fail "absolute: /bin/sh passes through" "got: $_got" +fi + +# --- no args prints usage and exits 1 --- +$WHICH >/dev/null 2>&1 +_rc=$? +check_eq "$_rc" "1" "usage: no args exits 1" + +summary diff --git a/tests/test_xargs.sh b/tests/test_xargs.sh new file mode 100755 index 0000000..4af06a9 --- /dev/null +++ b/tests/test_xargs.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# tests/test_xargs.sh - Tests for xargs applet +_test_name="xargs" +. "$(dirname "$0")/harness.sh" + +setup_tmpdir +XARGS="$VX xargs" + +echo "Running xargs tests..." + +out="$(printf 'a\nb\n' | $XARGS echo ITEM:)" +check_eq "$out" "ITEM: a b" "basic: appends stdin items" + +out="$(printf 'a\nb\nc\n' | $XARGS -n 2 echo CHUNK:)" +check_eq "$out" "CHUNK: a b +CHUNK: c" "nflag: max args" + +out="$(printf 'one\ntwo\n' | $XARGS -I '{}' echo X{}Y)" +check_eq "$out" "XoneY +XtwoY" "Iflag: replacement" + +out="$(printf 'one\ntwo\n' | $XARGS -J '{}' echo PRE POST '{}')" +check_eq "$out" "PRE POST one two" "Jflag: replacement at custom position" + +printf 'aa\0bb cc\0' > nul +out="$(cat nul | $XARGS -0 echo Z:)" +check_eq "$out" "Z: aa bb cc" "0flag: NUL-delimited input" + +out="$(printf 'a\nb\n' | $XARGS -L 1 echo LINE:)" +check_eq "$out" "LINE: a +LINE: b" "Lflag: one line per command" + +check_exit "exit: bad option exits 1" 1 $XARGS --definitely-not-a-real-option + +summary