1#!/usr/bin/env python3
2
3"""
4Utility for inspecting files in various ways.
5
6This tool is like the collection of tools found in a unix environment but are
7cross platform and stable and suitable for our needs in the test suite.
8
9This can be used instead of tools like:
10  [
11  dd
12  find
13  head
14  hexdump
15  ls
16  md5sum
17  readlink
18  sha1sum
19  stat
20  tail
21  test
22  readlink.py
23  md5sum.py
24"""
25
26from __future__ import absolute_import
27
28import binascii
29import glob
30import hashlib
31import optparse
32import os
33import re
34import sys
35
36# Python 3 adapters
37ispy3 = sys.version_info[0] >= 3
38if ispy3:
39
40    def iterbytes(s):
41        for i in range(len(s)):
42            yield s[i : i + 1]
43
44
45else:
46    iterbytes = iter
47
48
49def visit(opts, filenames, outfile):
50    """Process filenames in the way specified in opts, writing output to
51    outfile."""
52    for f in sorted(filenames):
53        isstdin = f == '-'
54        if not isstdin and not os.path.lexists(f):
55            outfile.write(b'%s: file not found\n' % f.encode('utf-8'))
56            continue
57        quiet = opts.quiet and not opts.recurse or isstdin
58        isdir = os.path.isdir(f)
59        islink = os.path.islink(f)
60        isfile = os.path.isfile(f) and not islink
61        dirfiles = None
62        content = None
63        facts = []
64        if isfile:
65            if opts.type:
66                facts.append(b'file')
67            if any((opts.hexdump, opts.dump, opts.md5, opts.sha1, opts.sha256)):
68                with open(f, 'rb') as fobj:
69                    content = fobj.read()
70        elif islink:
71            if opts.type:
72                facts.append(b'link')
73            content = os.readlink(f).encode('utf8')
74        elif isstdin:
75            content = getattr(sys.stdin, 'buffer', sys.stdin).read()
76            if opts.size:
77                facts.append(b'size=%d' % len(content))
78        elif isdir:
79            if opts.recurse or opts.type:
80                dirfiles = glob.glob(f + '/*')
81                facts.append(b'directory with %d files' % len(dirfiles))
82        elif opts.type:
83            facts.append(b'type unknown')
84        if not isstdin:
85            stat = os.lstat(f)
86            if opts.size and not isdir:
87                facts.append(b'size=%d' % stat.st_size)
88            if opts.mode and not islink:
89                facts.append(b'mode=%o' % (stat.st_mode & 0o777))
90            if opts.links:
91                facts.append(b'links=%d' % stat.st_nlink)
92            if opts.newer:
93                # mtime might be in whole seconds so newer file might be same
94                if stat.st_mtime >= os.stat(opts.newer).st_mtime:
95                    facts.append(
96                        b'newer than %s' % opts.newer.encode('utf8', 'replace')
97                    )
98                else:
99                    facts.append(
100                        b'older than %s' % opts.newer.encode('utf8', 'replace')
101                    )
102        if opts.md5 and content is not None:
103            h = hashlib.md5(content)
104            facts.append(b'md5=%s' % binascii.hexlify(h.digest())[: opts.bytes])
105        if opts.sha1 and content is not None:
106            h = hashlib.sha1(content)
107            facts.append(
108                b'sha1=%s' % binascii.hexlify(h.digest())[: opts.bytes]
109            )
110        if opts.sha256 and content is not None:
111            h = hashlib.sha256(content)
112            facts.append(
113                b'sha256=%s' % binascii.hexlify(h.digest())[: opts.bytes]
114            )
115        if isstdin:
116            outfile.write(b', '.join(facts) + b'\n')
117        elif facts:
118            outfile.write(b'%s: %s\n' % (f.encode('utf-8'), b', '.join(facts)))
119        elif not quiet:
120            outfile.write(b'%s:\n' % f.encode('utf-8'))
121        if content is not None:
122            chunk = content
123            if not islink:
124                if opts.lines:
125                    if opts.lines >= 0:
126                        chunk = b''.join(chunk.splitlines(True)[: opts.lines])
127                    else:
128                        chunk = b''.join(chunk.splitlines(True)[opts.lines :])
129                if opts.bytes:
130                    if opts.bytes >= 0:
131                        chunk = chunk[: opts.bytes]
132                    else:
133                        chunk = chunk[opts.bytes :]
134            if opts.hexdump:
135                for i in range(0, len(chunk), 16):
136                    s = chunk[i : i + 16]
137                    outfile.write(
138                        b'%04x: %-47s |%s|\n'
139                        % (
140                            i,
141                            b' '.join(b'%02x' % ord(c) for c in iterbytes(s)),
142                            re.sub(b'[^ -~]', b'.', s),
143                        )
144                    )
145            if opts.dump:
146                if not quiet:
147                    outfile.write(b'>>>\n')
148                outfile.write(chunk)
149                if not quiet:
150                    if chunk.endswith(b'\n'):
151                        outfile.write(b'<<<\n')
152                    else:
153                        outfile.write(b'\n<<< no trailing newline\n')
154        if opts.recurse and dirfiles:
155            assert not isstdin
156            visit(opts, dirfiles, outfile)
157
158
159if __name__ == "__main__":
160    parser = optparse.OptionParser("%prog [options] [filenames]")
161    parser.add_option(
162        "-t",
163        "--type",
164        action="store_true",
165        help="show file type (file or directory)",
166    )
167    parser.add_option(
168        "-m", "--mode", action="store_true", help="show file mode"
169    )
170    parser.add_option(
171        "-l", "--links", action="store_true", help="show number of links"
172    )
173    parser.add_option(
174        "-s", "--size", action="store_true", help="show size of file"
175    )
176    parser.add_option(
177        "-n", "--newer", action="store", help="check if file is newer (or same)"
178    )
179    parser.add_option(
180        "-r", "--recurse", action="store_true", help="recurse into directories"
181    )
182    parser.add_option(
183        "-S",
184        "--sha1",
185        action="store_true",
186        help="show sha1 hash of the content",
187    )
188    parser.add_option(
189        "",
190        "--sha256",
191        action="store_true",
192        help="show sha256 hash of the content",
193    )
194    parser.add_option(
195        "-M", "--md5", action="store_true", help="show md5 hash of the content"
196    )
197    parser.add_option(
198        "-D", "--dump", action="store_true", help="dump file content"
199    )
200    parser.add_option(
201        "-H", "--hexdump", action="store_true", help="hexdump file content"
202    )
203    parser.add_option(
204        "-B", "--bytes", type="int", help="number of characters to dump"
205    )
206    parser.add_option(
207        "-L", "--lines", type="int", help="number of lines to dump"
208    )
209    parser.add_option(
210        "-q", "--quiet", action="store_true", help="no default output"
211    )
212    (opts, filenames) = parser.parse_args(sys.argv[1:])
213    if not filenames:
214        filenames = ['-']
215
216    visit(opts, filenames, getattr(sys.stdout, 'buffer', sys.stdout))
217