Fix EXECUTABLES symlink display.
[pkgusr] / usr / bin / list_package
1 #!/bin/bash
2 # Copyright © 2021 Steve Youngs. All rights reserved.
3 # SPDX-License-Identifier: BSD-3-Clause
4
5 # Author:     Steve Youngs <steve@sxemacs.org>
6 # Maintainer: Steve Youngs <steve@sxemacs.org>
7 # Created:    <2021-05-04>
8 # Time-stamp: <Thursday 27 May 2021 19:36:09 (steve)>
9 # Homepage:   https://git.sxemacs.org/pkgusr
10 # Keywords:   pkgusr package-management tools
11
12 ## This file is part of pkgusr
13
14 ### Commentary:
15 #
16 #     This script outputs the contents (filelist) of a package.  Along
17 #     with the raw list of files and directories, there are also sectioned
18 #     lists of binaries, libraries, man page summaries, and texinfo doc
19 #     top node entries.  If the package is holding some deprecated old
20 #     libs from a previous install they are listed separately, as are any
21 #     "suspicious" files/directories.  By "suspicious" we mean weird-arsed
22 #     permissions, broken symlinks, hardlinks, that sort of thing.
23 #
24 #     The output is suitable to redirect to the pkgusr's .project file.
25 #
26 #     The idea for this script comes from Matthias S. Benkmann's
27 #     script 'list_package' in his set of pkgusr tools.  In fact,
28 #     this script uses some of Matthias' other scripts.  So, to be
29 #     fair, I'm going to include the copyright notice from Matthias'
30 #     list_package script...
31
32 ###[Matthias' list_package]
33 # Copyright (c) 2004 Matthias S. Benkmann <article AT winterdrache DOT de>
34 # You may do everything with this code except misrepresent its origin.
35 # PROVIDED `AS IS' WITH ABSOLUTELY NO WARRANTY OF ANY KIND!
36 ###
37
38 ### User-facing differences from Matthias' list_package
39 #
40 #     o Man pages:
41 #       This script lists summaries for all man pages of a package,
42 #       whereas Matt's script only lists summaries for the binaries.
43 #       Matt's script lists binaries that don't have man pages (many
44 #       false-positives), this script doesn't.  Matt's script claims
45 #       to have a bug where it can't list multiple pages that exist
46 #       with the same name in different topic sections.  No such bug
47 #       in this script.
48 #
49 #     o Texinfo docs:
50 #       This script -- yep; Matt's script -- nope.
51 #
52 #     o Group support:
53 #       Matt's script supports using a group name/gid, this script
54 #       doesn't.  The rationale is that it was too much of a PITA to
55 #       determine whether a specific group belonged to a pkgusr,
56 #       especially doing so without escalating privileges.  Instead
57 #       this script will mark any file that has a different group
58 #       from the pkgusr's primary group, but only if the script is
59 #       called with '--subgroups'. (when using group name/gid with
60 #       Matt's script no distinction is made in the output)
61 #
62 #     o Scary warnings:
63 #       Quoting part of the help output from Matt's script...
64 #       ,----
65 #       | WARNING! This program is for listing files from package users only!
66 #       |     Do NOT use it to list files from untrusted users!
67 #       |     An untrusted user could set up a manipulated manpage to exploit
68 #       |     a bug in man when it is used to extract the summary!
69 #       `----
70 #       This script does away with that by simply refusing to run if
71 #       asked to list a non-pkgusr's files.
72
73 #     o Deprecated libs:
74 #       This script -- yep; Matt's script -- nope.
75 #
76 #     Other than that the output is pretty much the same and all other
77 #     differences are behind the scenes, hidden from the user.
78
79 ### Todo:
80 #
81 #     o Write it.
82 #     o Make it work without the bugs.
83 #
84
85
86 ### Code:
87 PKG="$1"
88 SGRP="$2"
89
90 # User help
91 usage()
92 {
93     less<<EOF
94
95 Synopsis:
96 --------
97
98     ${0##*/} --help
99     ${0##*/} PKGUSR
100     ${0##*/} PKGUSR --subgroups
101
102 In the first form, display this help and exit.
103 In the second, output filelist from PKGUSR package.
104 In the last, do the same, but mark any files belonging to a "sub-group"
105
106 Description:
107 -----------
108
109 ${0##*/} is a script to list an installed package's content in a
110 formatted, human-readable manner.
111
112 Along with a complete file list with full pathnames, it also lists
113 executable files, libraries, manpage summaries, info document
114 summaries, and any extra executables (those residing outside of
115 /**/{,s}bin/).  It will then list anything odd or suspicious
116 (setuid/gid, world/group writable, sticky, broken links, hardlinks,
117 non-printable chars in filenames, etc.  basically, anything funky).
118 This is done by calling out to 'list_suspicious_files_from'.  Finally,
119 it lists anything from /usr/lib/deprecated (see "Deprecating
120 Libraries" below).
121
122 This script can take a very long time to complete, often several minutes,
123 and, depending on the size of the package, output many thousands of lines
124 (e.g. the output for a full KDE install is well over 60K lines).  Be
125 smart, redirect to a file.  Ideally ~pkgusr/.project, that way you can
126 view it later with 'pinky -l PKGUSR|less'.
127
128 Requirements:
129 ------------
130
131 Nothing out of the ordinary, pretty much just basic UNIX commands.  This
132 script calls 'forall_direntries_from' and 'list_suspicious_files_from',
133 so anything they need which you would already have.  We do need to take
134 into account dealing with compressed .info files which could be gzip,
135 bzip2, lzip, lzma, or xz (I personally have never seen any compressed
136 with anything other than gzip, but the stand-alone reader supports all
137 of those so, yeah... there is that)
138
139 The script will run fine if you do not have those compression tools
140 installed, it will just skip the file if it cannot be decompressed.
141 Having said that, do yourself a favour and install lzip and zutils,
142 especially zutils (and then get rid of zcat from gzip, it blows)
143
144 Deprecating Libraries:
145 ---------------------
146
147 The way I deal with library upgrades where the so version changes is
148 to put the old lib into /usr/lib/deprecated which is listed _LAST_ in
149 /etc/ld.so.conf.  The way it works is...
150
151 /usr/lib/deprecated is obviously not in ld search path so libs in
152 there would never be used at compile/link time, but dlopen() will
153 still find them because the directory is listed in /etc/ld.so.conf.
154 Stuff that is already linked will still be able to find what it needs.
155 Then it is simply a matter of rebuilding the packages that depend on
156 the upgraded library at your leisure.  Nuke the stuff in deprecated
157 once nothing needs it any longer.
158
159 EOF
160 exit 0
161 }
162
163 # This interesting construct will return true if $PKG is any of:
164 #  ? -? --? h -h --h help -help --help usage -usage --usage
165 [[ "$PKG" =~ ^-?-?(h(elp)?|usage|\?)$ ]] && usage
166
167 # Bail if $PKG isn't a real package.
168 if [[ -z ${PKG} || ! "$(id -Gn ${PKG})" =~ "install" ]]; then
169     echo 1>&2 'Invalid or missing PKGUSR'
170     echo 1>&2 Usage: ${0##*/} '[--help | PKGUSR [ --subgroups ] ]'
171     exit 1
172 fi
173
174 # We should definitely try to traverse the filesystem as few times as
175 # possible to save on time and CPU cycles.  So lets store the complete
176 # filelist right from the get-go, but only if we need to.
177 if [ -z "${PKGASSETS[*]}" ]; then
178     # Massage $IFS to cope with filenames with spaces
179     _oldifs="${IFS}"
180     IFS=$'\n'
181     PKGASSETS=($(forall_direntries_from $PKG|sort))
182     IFS="${_oldifs}"
183 fi
184
185 # We are going to want extended globbing
186 shopt -s extglob
187
188 typeset -a INFOS MANS LIBS UBINS EBINS AEXEC
189 categorise()
190 {
191     # yeah, you can use multiple counters in a for-loop
192     for ((i=0,I=0,M=0,L=0,U=0,E=0,A=0; i<${#PKGASSETS[@]}; i++)); do
193         # texinfo (collect full pathnames)
194         if [[ ${PKGASSETS[$i]} =~ /share/info/.*\.info(-[[:digit:]]+)? ]]; then
195             INFOS[$I]=${PKGASSETS[$i]}
196             I=$((($I + 1)))
197             # done with this file, skip to the next
198             continue
199         fi
200         # man (collect filenames sans extensions)
201         if [[ ${PKGASSETS[$i]} =~ /share/man/ ]]; then
202             MANS[$M]=$(basename ${PKGASSETS[$i]%%\.?(man|n|url|[[:digit:]]*)})
203             M=$((($M + 1)))
204             # done with this file, skip to the next
205             continue
206         fi
207         # libraries (collect filenames sans extensions, not symlinks)
208         if [[ ${PKGASSETS[$i]} =~ /lib(64)?/lib ]]; then
209             if [[ ! -h ${PKGASSETS[$i]} && -f ${PKGASSETS[$i]} ]]; then
210                 LIBS[$L]=$(basename ${PKGASSETS[$i]%%\.?(a|so*)})
211                 L=$((($L + 1)))
212                 # add to the list of all executables (for later mining
213                 # for dependencies) skip static libs
214                 if [[ $(file -i ${PKGASSETS[$i]}) =~ x-sharedlib ]]; then
215                     AEXEC[$A]=${PKGASSETS[$i]}
216                     A=$((($A + 1)))
217                 fi
218                 # done with this file, skip to the next
219                 continue
220             fi
221         fi
222         # binaries in */{,s}bin/
223         if [[ ${PKGASSETS[$i]} =~ /s?bin/ ]]; then
224             # look at symlinks first, we'll be massaging them a bit
225             if [[ -h ${PKGASSETS[$i]} && -f ${PKGASSETS[$i]} ]]; then
226                 LTGT=$(stat --printf "%N" "${PKGASSETS[$i]}" |
227                     awk -F\' '{print "(->"$4")"}')
228                 UBINS[$U]=$(basename ${PKGASSETS[$i]})${LTGT}
229                 U=$((($U + 1)))
230                 # done with this file, skip to the next
231                 continue
232             fi
233             # regular files
234             if [[ -f ${PKGASSETS[$i]} && -x ${PKGASSETS[$i]} ]]; then
235                 UBINS[$U]=$(basename ${PKGASSETS[$i]})
236                 # add to the list of all execs (not scripts)
237                 if [[ $(file -i ${PKGASSETS[$i]}) =~ x-executable ]]; then
238                     AEXEC[$A]=${PKGASSETS[$i]}
239                     A=$((($A + 1)))
240                 fi
241                 U=$((($U + 1)))
242                 # done with this file, skip to the next
243                 continue
244             fi
245         fi
246         # binaries outside of */{,s}bin/ (not symlinks)
247         if [[ ! -h ${PKGASSETS[$i]} && -f ${PKGASSETS[$i]} &&
248                     -x ${PKGASSETS[$i]} ]]; then
249             EBINS[$E]=${PKGASSETS[$i]}
250             # add to the list of all execs (not scripts)
251             if [[ $(file -i ${EBINS[$E]}) =~ x-executable ]]; then
252                 AEXEC[$A]=${EBINS[$E]}
253                 A=$((($A + 1)))
254             fi
255             E=$((($E + 1)))
256         fi
257     done
258
259     # sort the libs and binaries
260     oldifs="${IFS}"
261     IFS=$'\n'
262     UBINS=($(sort <<<"${UBINS[*]}"))
263     LIBS=($(sort -u <<<"${LIBS[*]}"))
264     IFS="${oldifs}"
265
266     # export AEXEC for further processing
267     export AEXEC
268 }
269
270 ## Process the docs
271 _have_docs=0                    # Keep note if the pkg has docs
272 # texinfo
273 procinfos()
274 {
275     local cmd
276     _have_docs=1
277     echo -e '\nDOCUMENTATION (texinfo):'
278     if [[ $(file -i $(type -Pp zcat)) =~ x-executable ]]; then
279         # Sweet! we have zcat from zutils
280         #
281         # This will fail if it hits a .lzma or .zst but texinfo 6.7's
282         # stand-alone info reader doesn't support zstd, and I have
283         # never in my life ever seen an info file compressed with
284         # lzma. tl;dr I'm not fixing it because you'll never see it
285         for ((i=0; i<${#INFOS[@]}; i++)); do
286             zcat ${INFOS[$i]}|sed -n /START-INFO-DIR-ENTRY/,/END-INFO-DIR-ENTRY/p |
287                 sed /DIR-ENTRY/d
288         done | sort -u | sed 's/^/  /'
289     else # fine, do it the hard way then
290         for ((i=0; i<${#INFOS[@]}; i++)); do
291             # Um, yeah.  Slow AF.  Install zutils.
292             case $(file -Lb --mime-type ${INFOS[$i]}) in
293                 application/gzip)    cmd=(gzip -dc)  ;;
294                 application/x-bzip2) cmd=(bzip2 -dc) ;;
295                 application/x-xz)    cmd=(xz -dc)    ;;
296                 application/x-lzip)  cmd=(lzip -dc)  ;;
297                 application/x-lzma)  cmd=(lzma -dc)  ;;
298                 application/zstd)    cmd=(zstd -dc)  ;;
299                 *)                   cmd=(cat)
300             esac
301             type -Pp ${cmd[0]} > /dev/null || continue
302             ${cmd[@]} ${INFOS[$i]}|sed -n /START-INFO-DIR-ENTRY/,/END-INFO-DIR-ENTRY/p |
303                 sed /DIR-ENTRY/d
304         done | sort -u | sed 's/^/  /'
305     fi
306 }
307
308 # man
309 procmans()
310 {
311     _have_docs=1
312     echo -e '\nDOCUMENTATION (man):'
313     for ((i=0; i<${#MANS[@]}; i++)); do
314         man --whatis ${MANS[$i]} 2>/dev/null
315     done | sort -u | sed 's/^/  /'
316 }
317
318 # We ain't got no steenkin docs
319 nodocs()
320 {
321     echo -e '\nDOCUMENTATION (lol, wut?):'
322     echo No docs here, maybe try Google...
323 }
324
325 ## Process the binaries
326 # bins in {,s}bin
327 procubins()
328 {
329     echo -e '\nEXECUTABLES (in */{,s}bin/):'
330     echo ${UBINS[@]}|sed -e 's/ /, /g' -e 's/^/  /'|fmt
331 }
332
333 # bins outside of {,s}bin
334 procebins()
335 {
336     echo -e '\nEXTRA EXECUTABLES (outside of */{,s}bin/):'
337     for ((i=0; i<${#EBINS[@]}; i++)); do
338         echo "  ${EBINS[$i]}"
339     done
340 }
341
342 # libs
343 proclibs()
344 {
345     echo -e '\nLIBRARIES:'
346     echo ${LIBS[@]}|sed -e 's/ /, /g' -e 's/^/  /'|fmt
347 }
348
349 # We need to do one more find, but it is short and sweet so won't add
350 # any noticable time to the process...
351 listdeprecated()
352 {
353     DEPRECATED=$(find /usr/lib/deprecated -maxdepth 1 -type f \
354         -user $PKG -printf "  %p\n")
355     if [ -n "${DEPRECATED}" ]; then
356         echo -e '\nDEPRECATED LIBRARIES (remember to audit):'
357         echo ${DEPRECATED}
358     fi
359 }
360
361 ## The complete file list
362 filelist()
363 {
364     local GRP
365     echo -e '\nFILE LIST:'
366     if [[ -n ${SGRP} && ${SGRP} =~ ^--?subgroups$ ]]; then
367         for ((i=0; i<${#PKGASSETS[@]}; i++)); do
368             GRP=$(stat --printf "%G" "${PKGASSETS[$i]}")
369             if [[ ${GRP} != ${PKG} ]]; then
370                 echo "* ${PKGASSETS[$i]} [${GRP}]"
371             else
372                 echo "  ${PKGASSETS[$i]}"
373             fi
374         done
375     else
376         for ((i=0; i<${#PKGASSETS[@]}; i++)); do
377             echo "  ${PKGASSETS[$i]}"
378         done
379     fi
380 }
381
382 ## Rock and Roll
383 categorise
384 # binaries and libraries
385 [[ ${#UBINS[@]} -gt 0 ]] && procubins
386 [[ ${#EBINS[@]} -gt 0 ]] && procebins
387 [[ ${#LIBS[@]} -gt 0 ]] && proclibs
388 # docs
389 [[ ${#MANS[@]} -gt 0 ]] && procmans
390 [[ ${#INFOS[@]} -gt 0 ]] && procinfos
391 [[ ${_have_docs} -eq 0 ]] && nodocs
392 # main file list
393 filelist
394 # sus files
395 list_suspicious_files_from ${PKG}
396 # deprecated
397 listdeprecated
398
399 ## Return cleanly via return and not exit so any calling script won't
400 ## exit
401 return 0
402
403 ### list_package ends here
404 # Local variables:
405 # coding: utf-8
406 # end: