• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rpm-software-management / rpmlint / 27743531775

18 Jun 2026 07:23AM UTC coverage: 88.143% (+0.2%) from 87.933%
27743531775

push

github

6096 of 6916 relevant lines covered (88.14%)

3.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.02
/rpmlint/pkg.py
1
import bz2
4✔
2
from collections import namedtuple
4✔
3
import contextlib
4✔
4
import gzip
4✔
5
import hashlib
4✔
6
import io
4✔
7
import lzma
4✔
8
import mmap
4✔
9
import os
4✔
10
from pathlib import Path, PurePath
4✔
11
import re
4✔
12
from shlex import quote
4✔
13
import shutil
4✔
14
import stat
4✔
15
import subprocess
4✔
16
import tempfile
4✔
17
import time
4✔
18
from urllib.parse import urljoin
4✔
19

20
try:
4✔
21
    import magic
4✔
22
    has_magic = True
4✔
23
except ImportError:
×
24
    has_magic = False
×
25
import rpm
4✔
26
from rpmlint.helpers import (byte_to_string, ENGLISH_ENVIRONMENT,
4✔
27
                             print_warning, pushd)
28
from rpmlint.pkgfile import PkgFile
4✔
29
import zstandard as zstd
4✔
30

31

32
DepInfo = namedtuple('DepInfo', ('name', 'flags', 'version'))
4✔
33

34
# 64: RPMSENSE_PREREQ is 0 with rpm 4.4..4.7, we want 64 here in order
35
# to do the right thing with those versions and packages built with other
36
# rpm versions
37
PREREQ_FLAG = (rpm.RPMSENSE_PREREQ or 64) | rpm.RPMSENSE_SCRIPT_PRE | \
4✔
38
    rpm.RPMSENSE_SCRIPT_POST | rpm.RPMSENSE_SCRIPT_PREUN | \
39
    rpm.RPMSENSE_SCRIPT_POSTUN
40

41
SCRIPT_TAGS = [
4✔
42
    (rpm.RPMTAG_PREIN, rpm.RPMTAG_PREINPROG, '%pre'),
43
    (rpm.RPMTAG_POSTIN, rpm.RPMTAG_POSTINPROG, '%post'),
44
    (rpm.RPMTAG_PREUN, rpm.RPMTAG_PREUNPROG, '%preun'),
45
    (rpm.RPMTAG_POSTUN, rpm.RPMTAG_POSTUNPROG, '%postun'),
46
    (rpm.RPMTAG_TRIGGERSCRIPTS, rpm.RPMTAG_TRIGGERSCRIPTPROG, '%trigger'),
47
    (rpm.RPMTAG_PRETRANS, rpm.RPMTAG_PRETRANSPROG, '%pretrans'),
48
    (rpm.RPMTAG_POSTTRANS, rpm.RPMTAG_POSTTRANSPROG, '%posttrans'),
49
    (rpm.RPMTAG_VERIFYSCRIPT, rpm.RPMTAG_VERIFYSCRIPTPROG, '%verifyscript'),
50
    # file triggers: rpm >= 4.12.90
51
    (getattr(rpm, 'RPMTAG_FILETRIGGERSCRIPTS', 5066),
52
     getattr(rpm, 'RPMTAG_FILETRIGGERSCRIPTPROG', 5067),
53
     '%filetrigger'),
54
    (getattr(rpm, 'RPMTAG_TRANSFILETRIGGERSCRIPTS', 5076),
55
     getattr(rpm, 'RPMTAG_TRANSFILETRIGGERSCRIPTPROG', 5077),
56
     '%transfiletrigger'),
57
]
58

59
RPM_SCRIPTLETS = ('pre', 'post', 'preun', 'postun', 'pretrans', 'posttrans',
4✔
60
                  'trigger', 'triggerin', 'triggerprein', 'triggerun',
61
                  'triggerpostun', 'verifyscript', 'filetriggerin',
62
                  'filetrigger', 'filetriggerun', 'filetriggerpostun',
63
                  'transfiletriggerin', 'transfiletrigger',
64
                  'transfiletriggerun', 'transfiletriggerun',
65
                  'transfiletriggerpostun')
66

67

68
gzip_regex = re.compile(r'\.t?gz?$')
4✔
69
bz2_regex = re.compile(r'\.t?bz2?$')
4✔
70
xz_regex = re.compile(r'\.(t[xl]z|xz|lzma)$')
4✔
71
zst_regex = re.compile(r'\.zst$')
4✔
72

73

74
def catcmd(fname):
4✔
75
    """Get a 'cat' command that handles possibly compressed files."""
76
    fname = str(fname)
×
77
    cat = 'gzip -dcf'
×
78
    if bz2_regex.search(fname):
×
79
        cat = 'bzip2 -dcf'
×
80
    elif xz_regex.search(fname):
×
81
        cat = 'xz -dc'
×
82
    elif zst_regex.search(fname):
×
83
        cat = 'zstd -dc'
×
84
    return cat
×
85

86

87
def compression_algorithm(fname):
4✔
88
    """Return compression algorithm based on filename if known, None otherwise."""
89
    fname = str(fname)
4✔
90
    if gzip_regex.search(fname):
4✔
91
        return gzip
4✔
92
    if bz2_regex.search(fname):
4✔
93
        return bz2
4✔
94
    if xz_regex.search(fname):
4✔
95
        return lzma
4✔
96
    if zst_regex.search(fname):
4✔
97
        return zstd
×
98
    return None
4✔
99

100

101
def is_utf8(fname):
4✔
102
    compression = compression_algorithm(fname)
4✔
103
    if compression is None:
4✔
104
        with open(fname, 'rb') as f:
4✔
105
            return is_utf8_bytestr(f.read())
4✔
106

107
    with compression.open(fname, 'rb') as f:
4✔
108
        try:
4✔
109
            return is_utf8_bytestr(f.read())
4✔
110
        except OSError:
4✔
111
            return True
4✔
112

113

114
def is_utf8_bytestr(s):
4✔
115
    """Returns True whether the given text is UTF-8.
116
    Due to changes in rpm, needs to handle both bytes and unicode."""
117
    if not isinstance(s, (bytes, str)):
4✔
118
        unexpected = type(s).__name__
×
119
        raise TypeError(f'Expected str/bytes, not {unexpected}')
×
120

121
    try:
4✔
122
        if isinstance(s, bytes):
4✔
123
            s.decode('utf-8')
4✔
124
    except UnicodeError:
4✔
125
        return False
4✔
126

127
    return True
4✔
128

129

130
def has_forbidden_controlchars(val):
4✔
131
    if isinstance(val, (str, bytes)):
4✔
132
        string = val
4✔
133
        if isinstance(val, bytes):
4✔
134
            val = memoryview(val)
×
135
        for c in val:
4✔
136
            if isinstance(c, str):
4✔
137
                c = ord(c)
4✔
138
            if c < 32 and (c not in (9, 10, 13)):
4✔
139
                return string
4✔
140
    if isinstance(val, (tuple, list)):
4✔
141
        for item in val:
4✔
142
            return has_forbidden_controlchars(item)
4✔
143
    return False
4✔
144

145

146
# from yum 3.2.27, rpmUtils.miscutils, with rpmlint modifications
147
def compareEVR(evr1, evr2):
4✔
148
    (e1, v1, r1) = evr1
4✔
149
    (e2, v2, r2) = evr2
4✔
150
    # return 1: a is newer than b
151
    # 0: a and b are the same version
152
    # -1: b is newer than a
153
    # rpmlint mod: don't stringify None epochs to 'None' strings
154
    if e1 is not None:
4✔
155
        e1 = str(e1)
4✔
156
    v1 = str(v1)
4✔
157
    r1 = str(r1)
4✔
158
    if e2 is not None:
4✔
159
        e2 = str(e2)
4✔
160
    v2 = str(v2)
4✔
161
    r2 = str(r2)
4✔
162
    rc = rpm.labelCompare((e1, v1, r1), (e2, v2, r2))
4✔
163
    return rc
4✔
164

165

166
# from yum 3.2.27, rpmUtils.miscutils, with rpmlint modifications
167
def rangeCompare(reqtuple, provtuple):
4✔
168
    """returns true if provtuple satisfies reqtuple"""
169
    (reqn, reqf, (reqe, reqv, reqr)) = reqtuple
4✔
170
    (n, f, (e, v, r)) = provtuple
4✔
171
    if reqn != n:
4✔
172
        return 0
4✔
173

174
    # unversioned satisfies everything
175
    if not f or not reqf:
4✔
176
        return 1
4✔
177

178
    # and you thought we were done having fun
179
    # if the requested release is left out then we have
180
    # to remove release from the package prco to make sure the match
181
    # is a success - ie: if the request is EQ foo 1:3.0.0 and we have
182
    # foo 1:3.0.0-15 then we have to drop the 15 so we can match
183
    if reqr is None:
4✔
184
        r = None
4✔
185
    # rpmlint mod: don't mess with provided Epoch, doing so breaks e.g.
186
    # 'Requires: foo < 1.0' should not be satisfied by 'Provides: foo = 1:0.5'
187
    # if reqe is None:
188
    #    e = None
189
    if reqv is None:  # just for the record if ver is None then we're going to segfault
4✔
190
        v = None
×
191

192
    # if we just require foo-version, then foo-version-* will match
193
    if r is None:
4✔
194
        reqr = None
4✔
195

196
    rc = compareEVR((e, v, r), (reqe, reqv, reqr))
4✔
197

198
    # does not match unless
199
    if rc >= 1:
4✔
200
        if reqf in ['GT', 'GE', 4, 12]:
4✔
201
            return 1
×
202
        if reqf in ['EQ', 8] and f in ['LE', 10, 'LT', 2]:
4✔
203
            return 1
×
204
        if reqf in ['LE', 'LT', 'EQ', 10, 2, 8] and f in ['LE', 'LT', 10, 2]:
4✔
205
            return 1
×
206

207
    if rc == 0:
4✔
208
        if reqf in ['GT', 4] and f in ['GT', 'GE', 4, 12]:
4✔
209
            return 1
×
210
        if reqf in ['GE', 12] and f in ['GT', 'GE', 'EQ', 'LE', 4, 12, 8, 10]:
4✔
211
            return 1
×
212
        if reqf in ['EQ', 8] and f in ['EQ', 'GE', 'LE', 8, 12, 10]:
4✔
213
            return 1
×
214
        if reqf in ['LE', 10] and f in ['EQ', 'LE', 'LT', 'GE', 8, 10, 2, 12]:
4✔
215
            return 1
×
216
        if reqf in ['LT', 2] and f in ['LE', 'LT', 10, 2]:
4✔
217
            return 1
×
218
    if rc <= -1:
4✔
219
        if reqf in ['GT', 'GE', 'EQ', 4, 12, 8] and f in ['GT', 'GE', 4, 12]:
×
220
            return 1
×
221
        if reqf in ['LE', 'LT', 10, 2]:
×
222
            return 1
×
223
#                if rc >= 1:
224
#                    if reqf in ['GT', 'GE', 4, 12]:
225
#                        return 1
226
#                if rc == 0:
227
#                    if reqf in ['GE', 'LE', 'EQ', 8, 10, 12]:
228
#                        return 1
229
#                if rc <= -1:
230
#                    if reqf in ['LT', 'LE', 2, 10]:
231
#                        return 1
232

233
    return 0
4✔
234

235

236
# from yum 3.2.23, rpmUtils.miscutils, with rpmlint modifications
237
def formatRequire(name, flags, evr):
4✔
238
    s = name
4✔
239

240
    if flags and flags & (rpm.RPMSENSE_LESS | rpm.RPMSENSE_GREATER |
4✔
241
                          rpm.RPMSENSE_EQUAL):
242
        s = s + ' '
4✔
243
        if flags & rpm.RPMSENSE_LESS:
4✔
244
            s = s + '<'
4✔
245
        if flags & rpm.RPMSENSE_GREATER:
4✔
246
            s = s + '>'
1✔
247
        if flags & rpm.RPMSENSE_EQUAL:
4✔
248
            s = s + '='
4✔
249
        s = f'{s} {versionToString(evr)}'
4✔
250
    return s
4✔
251

252

253
def versionToString(evr):
4✔
254
    if not isinstance(evr, (list, tuple)):
4✔
255
        # assume string
256
        return evr
×
257
    ret = ''
4✔
258
    if evr[0] is not None and evr[0] != '':
4✔
259
        ret += str(evr[0]) + ':'
4✔
260
    if evr[1] is not None:
4✔
261
        ret += evr[1]
4✔
262
        if evr[2] is not None and evr[2] != '':
4✔
263
            ret += '-' + evr[2]
4✔
264
    return ret
4✔
265

266

267
# from yum 3.2.23, rpmUtils.miscutils, with some rpmlint modifications
268
def stringToVersion(verstring):
4✔
269
    if verstring in (None, ''):
4✔
270
        return (None, None, None)
4✔
271
    epoch = None
4✔
272
    i = verstring.find(':')
4✔
273
    if i != -1:
4✔
274
        with contextlib.suppress(ValueError):
4✔
275
            # garbage in epoch, ignore it
276
            epoch = int(verstring[:i])
4✔
277
    i += 1
4✔
278
    j = verstring.find('-', i)
4✔
279
    if j != -1:
4✔
280
        if verstring[i:j] == '':
4✔
281
            version = None
×
282
        else:
283
            version = verstring[i:j]
4✔
284
        release = verstring[j + 1:]
4✔
285
    else:
286
        if verstring[i:] == '':
4✔
287
            version = None
×
288
        else:
289
            version = verstring[i:]
4✔
290
        release = None
4✔
291
    return (epoch, version, release)
4✔
292

293

294
def parse_deps(line):
4✔
295
    """
296
    Parse provides/requires/conflicts/obsoletes line to list of
297
    (name, flags, (epoch, version, release)) tuples.
298
    """
299

300
    prcos = []
4✔
301
    tokens = re.split(r'[\s,]+', line.strip())
4✔
302

303
    # Drop line continuation backslash in multiline macro definition (for
304
    # spec file parsing), e.g.
305
    # [...] \
306
    # Obsoletes: foo-%1 <= 1.0.0 \
307
    # [...] \
308
    # (yes, this is an ugly hack and we probably have other problems with
309
    #  multiline macro definitions elsewhere...)
310
    if tokens[-1] == '\\':
4✔
311
        del tokens[-1]
×
312

313
    prco = []
4✔
314
    while tokens:
4✔
315
        token = tokens.pop(0)
4✔
316
        if not token:
4✔
317
            # skip empty tokens
318
            continue
4✔
319

320
        plen = len(prco)
4✔
321

322
        if plen == 0:
4✔
323
            prco.append(token)
4✔
324

325
        elif plen == 1:
4✔
326
            flags = 0
4✔
327
            if token[0] in ('=', '<', '<=', '>', '>='):
4✔
328
                # versioned, flags
329
                if '=' in token:
4✔
330
                    flags |= rpm.RPMSENSE_EQUAL
4✔
331
                if '<' in token:
4✔
332
                    flags |= rpm.RPMSENSE_LESS
4✔
333
                if '>' in token:
4✔
334
                    flags |= rpm.RPMSENSE_GREATER
4✔
335
                prco.append(flags)
4✔
336
            else:
337
                # no flags following name, treat as unversioned, add and reset
338
                prco.extend((flags, (None, None, None)))
4✔
339
                prcos.append(tuple(prco))
4✔
340
                prco = [token]
4✔
341

342
        elif plen == 2:
4✔
343
            # last token of versioned one, add and reset
344
            prco.append(stringToVersion(token))
4✔
345
            prcos.append(tuple(prco))
4✔
346
            prco = []
4✔
347

348
    plen = len(prco)
4✔
349
    if plen:
4✔
350
        if plen == 1:
4✔
351
            prco.extend((0, (None, None, None)))
4✔
352
        elif plen == 2:
4✔
353
            prco.append((None, None, None))
4✔
354
        prcos.append(tuple(prco))
4✔
355

356
    return prcos
4✔
357

358

359
def _get_magic_libmagic(path):
4✔
360
    return magic.detect_from_filename(path).name
×
361

362

363
def _get_magic_python_magic(path):
4✔
364
    return magic.from_file(path)
4✔
365

366

367
def get_magic(path):
4✔
368
    # python-magic & libmagic compatibility code
369
    # https://github.com/ahupp/python-magic/blob/master/COMPAT.md
370
    detect_magic = _get_magic_python_magic
4✔
371
    if not hasattr(magic, 'from_file'):
4✔
372
        # libmagic python bindings
373
        detect_magic = _get_magic_libmagic
×
374

375
    try:
4✔
376
        return detect_magic(path)
4✔
377
    except (ValueError, FileNotFoundError):
4✔
378
        return ''
4✔
379

380

381
# classes representing package
382

383
class AbstractPkg:
4✔
384
    def cleanup(self):
4✔
385
        pass
×
386

387
    def _calc_magic(self, pkgfile):
4✔
388
        magic_description = pkgfile.magic
4✔
389
        if not magic_description:
4✔
390
            if stat.S_ISDIR(pkgfile.mode):
4✔
391
                magic_description = 'directory'
×
392
            elif stat.S_ISLNK(pkgfile.mode):
4✔
393
                magic_description = "symbolic link to `%s'" % pkgfile.linkto
×
394
            elif not pkgfile.size:
4✔
395
                magic_description = 'empty'
4✔
396
        if not magic_description and not pkgfile.is_ghost and has_magic:
4✔
397
            start = time.monotonic()
4✔
398
            magic_description = get_magic(pkgfile.path)
4✔
399
            self.timers['libmagic'] += time.monotonic() - start
4✔
400
        if magic_description is None or Pkg._magic_from_compressed_re.search(magic_description):
4✔
401
            # Discard magic from inside compressed files ('file -z')
402
            # until PkgFile gets decompression support.  We may get
403
            # such magic strings from package headers already now;
404
            # for example Fedora's rpmbuild as of F-11's 4.7.1 is
405
            # patched so it generates them.
406
            magic_description = ''
4✔
407
        return magic_description
4✔
408

409
    # internal function to gather dependency info used by the above ones
410
    def _gather_aux(self, header, xs, nametag, flagstag, versiontag,
4✔
411
                    prereq=None):
412
        versions = header[versiontag]
4✔
413

414
        if versions:
4✔
415
            names = header[nametag]
4✔
416
            flags = header[flagstag]
4✔
417
            for version, name_bytes, flag in zip(versions, names, flags):
4✔
418
                name = byte_to_string(name_bytes)
4✔
419
                evr = stringToVersion(byte_to_string(version))
4✔
420
                if prereq is not None and flag & PREREQ_FLAG:
4✔
421
                    prereq.append((name, flag & (~PREREQ_FLAG), evr))
4✔
422
                else:
423
                    xs.append(DepInfo(name, flag, evr))
4✔
424
        return xs, prereq
4✔
425

426
    def _gather_dep_info(self):
4✔
427
        _requires = []
4✔
428
        _prereq = []
4✔
429
        _provides = []
4✔
430
        _conflicts = []
4✔
431
        _obsoletes = []
4✔
432
        _recommends = []
4✔
433
        _suggests = []
4✔
434
        _enhances = []
4✔
435
        _supplements = []
4✔
436

437
        _requires, _prereq = self._gather_aux(self.header, _requires,
4✔
438
                                              rpm.RPMTAG_REQUIRENAME,
439
                                              rpm.RPMTAG_REQUIREFLAGS,
440
                                              rpm.RPMTAG_REQUIREVERSION,
441
                                              _prereq)
442
        _conflits, _ = self._gather_aux(self.header, _conflicts,
4✔
443
                                        rpm.RPMTAG_CONFLICTNAME,
444
                                        rpm.RPMTAG_CONFLICTFLAGS,
445
                                        rpm.RPMTAG_CONFLICTVERSION)
446
        _provides, _ = self._gather_aux(self.header, _provides,
4✔
447
                                        rpm.RPMTAG_PROVIDENAME,
448
                                        rpm.RPMTAG_PROVIDEFLAGS,
449
                                        rpm.RPMTAG_PROVIDEVERSION)
450
        _obsoletes, _ = self._gather_aux(self.header, _obsoletes,
4✔
451
                                         rpm.RPMTAG_OBSOLETENAME,
452
                                         rpm.RPMTAG_OBSOLETEFLAGS,
453
                                         rpm.RPMTAG_OBSOLETEVERSION)
454
        _recommends, _ = self._gather_aux(self.header, _recommends,
4✔
455
                                          rpm.RPMTAG_RECOMMENDNAME,
456
                                          rpm.RPMTAG_RECOMMENDFLAGS,
457
                                          rpm.RPMTAG_RECOMMENDVERSION)
458
        _suggests, _ = self._gather_aux(self.header, _suggests,
4✔
459
                                        rpm.RPMTAG_SUGGESTNAME,
460
                                        rpm.RPMTAG_SUGGESTFLAGS,
461
                                        rpm.RPMTAG_SUGGESTVERSION)
462
        _enhances, _ = self._gather_aux(self.header, _enhances,
4✔
463
                                        rpm.RPMTAG_ENHANCENAME,
464
                                        rpm.RPMTAG_ENHANCEFLAGS,
465
                                        rpm.RPMTAG_ENHANCEVERSION)
466
        _supplements, _ = self._gather_aux(self.header, _supplements,
4✔
467
                                           rpm.RPMTAG_SUPPLEMENTNAME,
468
                                           rpm.RPMTAG_SUPPLEMENTFLAGS,
469
                                           rpm.RPMTAG_SUPPLEMENTVERSION)
470

471
        return (_requires, _prereq, _provides, _conflicts, _obsoletes, _recommends,
4✔
472
                _suggests, _enhances, _supplements)
473

474
    def scriptprog(self, which):
4✔
475
        """
476
        Get the specified script interpreter as a string.
477
        Depending on rpm-python version, the string may or may not include
478
        interpreter arguments, if any.
479
        """
480
        if which is None:
4✔
481
            return ''
×
482
        prog = self[which]
4✔
483
        if prog is None:
4✔
484
            prog = ''
4✔
485
        elif isinstance(prog, (list, tuple)):
2✔
486
            # http://rpm.org/ticket/847#comment:2
487
            prog = ''.join(prog)
2✔
488
        return prog
4✔
489

490
    def __enter__(self):
4✔
491
        return self
4✔
492

493
    def __exit__(self, exc_type, exc_val, exc_tb):
4✔
494
        self.cleanup()
4✔
495

496
    def check_versioned_dep(self, name, version):
4✔
497
        # try to match name%_isa as well (e.g. 'foo(x86-64)', 'foo(x86-32)')
498
        name_re = re.compile(r'^%s(\(\w+-\d+\))?$' % re.escape(name))
4✔
499
        for d in self.requires + self.prereq:
4✔
500
            if name_re.match(d[0]):
4✔
501
                if d[1] & rpm.RPMSENSE_EQUAL != rpm.RPMSENSE_EQUAL \
4✔
502
                        or d[2][1] != version:
503
                    return False
×
504
                return True
4✔
505
        return False
4✔
506

507
    def read_with_mmap(self, filename):
4✔
508
        """Mmap a file, return it's content decoded."""
509
        try:
4✔
510
            with open(Path(self.dir_name() or '/', filename.lstrip('/'))) as in_file:
4✔
511
                return mmap.mmap(in_file.fileno(), 0, mmap.MAP_SHARED, mmap.PROT_READ).read().decode()
4✔
512
        except Exception:
2✔
513
            return ''
2✔
514

515
    def grep(self, regex, filename):
4✔
516
        """Grep regex from a file, return first matching line number (starting with 1)."""
517
        data = self.read_with_mmap(filename)
4✔
518
        match = regex.search(data)
4✔
519
        if match:
4✔
520
            return data.count('\n', 0, match.start()) + 1
4✔
521
        return None
4✔
522

523

524
class Pkg(AbstractPkg):
4✔
525
    _magic_from_compressed_re = re.compile(r'\([^)]+\s+compressed\s+data\b')
4✔
526

527
    def __init__(self, filename, dirname, header=None, is_source=False, extracted=False, verbose=False):
4✔
528
        self.filename = filename
4✔
529
        self.extracted = extracted
4✔
530

531
        # record decompression and extraction time
532
        start = time.monotonic()
4✔
533
        self.dirname = self._extract_rpm(dirname, verbose)
4✔
534
        self.timers = {'ExtractRpm': time.monotonic() - start, 'libmagic': 0}
4✔
535
        self.current_linenum = None
4✔
536

537
        self._req_names = -1
4✔
538

539
        if header:
4✔
540
            self.header = header
2✔
541
            self.is_source = is_source
2✔
542
        else:
543
            # Create a package object from the file name
544
            ts = rpm.TransactionSet()
4✔
545
            # Don't check signatures here...
546
            ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
4✔
547
            fd = os.open(filename, os.O_RDONLY)
4✔
548
            try:
4✔
549
                self.header = ts.hdrFromFdno(fd)
4✔
550
            finally:
551
                os.close(fd)
4✔
552
            self.is_source = not self.header[rpm.RPMTAG_SOURCERPM]
4✔
553

554
        self.name = self[rpm.RPMTAG_NAME]
4✔
555

556
        (self.requires, self.prereq, self.provides, self.conflicts,
4✔
557
         self.obsoletes, self.recommends, self.suggests, self.enhances,
558
         self.supplements) = self._gather_dep_info()
559

560
        self.req_names = [x[0] for x in self.requires + self.prereq]
4✔
561

562
        self.files = self._gather_files_info()
4✔
563
        self.config_files = [x.name for x in self.files.values() if x.is_config]
4✔
564
        self.doc_files = [x.name for x in self.files.values() if x.is_doc]
4✔
565
        self.ghost_files = [x.name for x in self.files.values() if x.is_ghost]
4✔
566
        self.noreplace_files = [x.name for x in self.files.values() if x.is_noreplace]
4✔
567
        self.missingok_files = [x.name for x in self.files.values() if x.is_missingok]
4✔
568

569
        if self.is_no_source:
4✔
570
            self.arch = 'nosrc'
×
571
        elif self.is_source:
4✔
572
            self.arch = 'src'
4✔
573
        else:
574
            self.arch = self.header.format('%{ARCH}')
4✔
575

576
    # Return true if the package is a nosource package.
577
    # NoSource files are ghosts in source packages.
578
    @property
4✔
579
    def is_no_source(self):
4✔
580
        return self.is_source and self.ghost_files
4✔
581

582
    # access the tags like an array
583
    def __getitem__(self, key):
4✔
584
        try:
4✔
585
            val = self.header[key]
4✔
586
        except KeyError:
×
587
            val = []
×
588
        if val == []:
4✔
589
            return None
4✔
590
        # Note that text tags we want to try decoding for real in TagsCheck
591
        # such as summary, description and changelog are not here.
592
        if key in (rpm.RPMTAG_NAME, rpm.RPMTAG_VERSION, rpm.RPMTAG_RELEASE,
4✔
593
                   rpm.RPMTAG_ARCH, rpm.RPMTAG_GROUP, rpm.RPMTAG_BUILDHOST,
594
                   rpm.RPMTAG_LICENSE, rpm.RPMTAG_HEADERI18NTABLE,
595
                   rpm.RPMTAG_PACKAGER, rpm.RPMTAG_SOURCERPM,
596
                   rpm.RPMTAG_DISTRIBUTION, rpm.RPMTAG_VENDOR) \
597
        or key in (x[0] for x in SCRIPT_TAGS) \
598
        or key in (x[1] for x in SCRIPT_TAGS):
599
            val = byte_to_string(val)
4✔
600
            if key == rpm.RPMTAG_GROUP and val == 'Unspecified':
4✔
601
                val = None
4✔
602
        return val
4✔
603

604
    # return the name of the directory where the package is extracted
605
    def dir_name(self):
4✔
606
        return self.dirname
4✔
607

608
    def _extract_rpm(self, dirname, verbose):
4✔
609
        if not Path(dirname).is_dir():
4✔
610
            print_warning('Unable to access dir %s' % dirname)
×
611
        elif dirname == '/':
4✔
612
            # it is an InstalledPkg
613
            pass
2✔
614
        else:
615
            self.__tmpdir = tempfile.TemporaryDirectory(
4✔
616
                prefix='rpmlint.%s.' % Path(self.filename).name, dir=dirname
617
            )
618
            dirname = self.__tmpdir.name
4✔
619

620
            # BusyBox' cpio does not support '-D' argument and the only safe
621
            # usage is doing chdir before invocation.
622
            filename = Path(self.filename).resolve()
4✔
623
            with pushd(dirname):
4✔
624
                stderr = None if verbose else subprocess.DEVNULL
4✔
625
                if shutil.which('rpm2archive'):
626
                    with open(filename, 'rb') as rpm_data:
4✔
627
                        subprocess.check_output('rpm2archive - | tar -xz && chmod -R +rX .', shell=True, env=ENGLISH_ENVIRONMENT,
4✔
628
                                                stderr=stderr, stdin=rpm_data)
4✔
629
                else:
4✔
630
                    command_str = f'rpm2cpio {quote(str(filename))} | cpio -id && chmod -R +rX .'
631
                    subprocess.check_output(command_str, shell=True, env=ENGLISH_ENVIRONMENT, stderr=stderr)
632
            self.extracted = True
×
633
        return dirname
×
634

4✔
635
    def check_signature(self):
4✔
636
        ret = subprocess.run(('rpm', '-Kv', self.filename),
637
                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
4✔
638
                             env=ENGLISH_ENVIRONMENT, text=True)
4✔
639
        text = ret.stdout
640
        if text.endswith('\n'):
641
            text = text[:-1]
4✔
642
        return ret.returncode, text
4✔
643

4✔
644
    # remove the extracted files from the package
4✔
645
    def cleanup(self):
646
        if self.extracted and self.dirname:
647
            self.__tmpdir.cleanup()
4✔
648

4✔
649
    def langtag(self, tag, lang):
4✔
650
        """Get value of tag in the given language."""
651
        # LANGUAGE trumps other env vars per GNU gettext docs, see also #166
4✔
652
        orig = os.environ.get('LANGUAGE')
653
        os.environ['LANGUAGE'] = lang
654
        ret = self[tag]
4✔
655
        if orig is not None:
4✔
656
            os.environ['LANGUAGE'] = orig
4✔
657
        return ret
4✔
658

4✔
659
    # extract information about the files
4✔
660
    def _gather_files_info(self):
661
        ret = {}
662
        flags = self.header[rpm.RPMTAG_FILEFLAGS]
4✔
663
        modes = self.header[rpm.RPMTAG_FILEMODES]
4✔
664
        users = self.header[rpm.RPMTAG_FILEUSERNAME]
4✔
665
        groups = self.header[rpm.RPMTAG_FILEGROUPNAME]
4✔
666
        links = [byte_to_string(x) for x in self.header[rpm.RPMTAG_FILELINKTOS]]
4✔
667
        sizes = self.header[rpm.RPMTAG_FILESIZES]
4✔
668
        if len(sizes) != len(flags):
4✔
669
            sizes = self.header[rpm.RPMTAG_LONGFILESIZES]
4✔
670
        md5s = self.header[rpm.RPMTAG_FILEMD5S]
4✔
671
        mtimes = self.header[rpm.RPMTAG_FILEMTIMES]
×
672
        rdevs = self.header[rpm.RPMTAG_FILERDEVS]
4✔
673
        langs = self.header[rpm.RPMTAG_FILELANGS]
4✔
674
        inodes = self.header[rpm.RPMTAG_FILEINODES]
4✔
675
        requires = [byte_to_string(x) for x in self.header[rpm.RPMTAG_FILEREQUIRE]]
4✔
676
        provides = [byte_to_string(x) for x in self.header[rpm.RPMTAG_FILEPROVIDE]]
4✔
677
        files = [byte_to_string(x) for x in self.header[rpm.RPMTAG_FILENAMES]]
4✔
678
        magics = [byte_to_string(x) for x in self.header[rpm.RPMTAG_FILECLASS]]
4✔
679
        try:  # rpm >= 4.7.0
4✔
680
            filecaps = self.header[rpm.RPMTAG_FILECAPS]
4✔
681
        except AttributeError:
4✔
682
            filecaps = None
4✔
683

×
684
        # rpm-python < 4.6 does not return a list for this (or FILEDEVICES,
×
685
        # FWIW) for packages containing exactly one file
686
        if not isinstance(inodes, list):
687
            inodes = [inodes]
688

4✔
689
        if files:
×
690
            for idx, file in enumerate(files):
691
                pkgfile = PkgFile(file)
4✔
692
                pkgfile.path = os.path.normpath(os.path.join(
4✔
693
                    self.dir_name() or '/', pkgfile.name.lstrip('/')))
4✔
694
                pkgfile.flags = flags[idx]
4✔
695
                pkgfile.mode = modes[idx]
696
                pkgfile.user = byte_to_string(users[idx])
4✔
697
                pkgfile.group = byte_to_string(groups[idx])
4✔
698
                pkgfile.linkto = links[idx] and os.path.normpath(links[idx])
4✔
699
                pkgfile.size = sizes[idx]
4✔
700
                pkgfile.md5 = md5s[idx]
4✔
701
                pkgfile.mtime = mtimes[idx]
4✔
702
                pkgfile.rdev = rdevs[idx]
4✔
703
                pkgfile.inode = inodes[idx]
4✔
704
                pkgfile.requires = parse_deps(requires[idx])
4✔
705
                pkgfile.provides = parse_deps(provides[idx])
4✔
706
                pkgfile.lang = byte_to_string(langs[idx])
4✔
707
                pkgfile.magic = magics[idx]
4✔
708
                pkgfile.magic = self._calc_magic(pkgfile)
4✔
709
                if filecaps:
4✔
710
                    pkgfile.filecaps = byte_to_string(filecaps[idx])
4✔
711
                ret[pkgfile.name] = pkgfile
4✔
712
        return ret
4✔
713

4✔
714
    def readlink(self, pkgfile):
4✔
715
        """
716
        Resolve symlinks for the given PkgFile, return the dereferenced
4✔
717
        PkgFile if it is found in this package, None if not.
718
        """
719
        result = pkgfile
720
        while result and result.linkto:
721
            linkpath = urljoin(result.name, result.linkto)
4✔
722
            linkpath = os.path.normpath(linkpath)
4✔
723
            result = self.files.get(linkpath)
4✔
724
        return result
4✔
725

4✔
726
    def get_core_reqs(self):
4✔
727
        """
728
        Return the list of dependencies that are not found by find-requires
4✔
729
        withouth the flag RPM
730
        """
731
        core_reqs = []
732

733
        for dep in rpm.ds(self.header, 'requires'):
2✔
734
            # skip deps which were found by find-requires
735
            if dep.Flags() & rpm.RPMSENSE_FIND_REQUIRES != 0:
2✔
736
                continue
737
            core_reqs.append(dep.N())
2✔
738

2✔
739
        return core_reqs
2✔
740

741

2✔
742
def get_installed_pkgs(name):
743
    """Get list of installed package objects by name."""
744

4✔
745
    ts = rpm.TransactionSet()
746
    if re.search(r'[?*]|\[.+\]', name):
747
        mi = ts.dbMatch()
2✔
748
        mi.pattern('name', rpm.RPMMIRE_GLOB, name)
2✔
749
    else:
×
750
        mi = ts.dbMatch('name', name)
×
751

752
    return [InstalledPkg(name, hdr) for hdr in mi]
2✔
753

754

2✔
755
# Class to provide an API to an installed package
756
class InstalledPkg(Pkg):
757
    def __init__(self, name, hdr=None):
758
        if not hdr:
4✔
759
            ts = rpm.TransactionSet()
4✔
760
            mi = ts.dbMatch('name', name)
2✔
761
            if not mi:
×
762
                raise KeyError(name)
×
763
            try:
×
764
                hdr = next(mi)
×
765
            except StopIteration:
×
766
                raise KeyError(name)
×
767

×
768
        super().__init__(name, '/', hdr, extracted=True)
×
769
        # create a fake filename to satisfy some checks on the filename
770
        self.filename = '%s-%s-%s.%s.rpm' % \
2✔
771
            (self.name, self[rpm.RPMTAG_VERSION], self[rpm.RPMTAG_RELEASE],
772
             self[rpm.RPMTAG_ARCH])
2✔
773

774
    def cleanup(self):
775
        pass
776

4✔
777
    def check_signature(self):
×
778
        return (0, 'fake: pgp md5 OK')
779

4✔
780

2✔
781
class FakeHeader(dict):
782
    def sprintf(self, expr):
783
        """
4✔
784
        Replaces expressions like %{} with actual package
4✔
785
        """
786

787
        tagre = re.compile(r'%{([^}]*)}')
788
        for tag in tagre.findall(expr):
789
            expr = expr.replace(f'%{tag}', self[f'RPMTAG_{tag}'])
4✔
790
        return expr
4✔
791

4✔
792
    def __missing__(self, key):
4✔
793
        try:
794
            key = getattr(rpm, key)
4✔
795
        except (TypeError, KeyError):
4✔
796
            raise KeyError
4✔
797

×
798
        if key not in self:
×
799
            raise KeyError
800

4✔
801
        return self[key]
×
802

803

4✔
804
# Class to provide an API to a 'fake' package, eg. for specfile-only checks
805
class FakePkg(AbstractPkg):
806
    _autoheaders = [
807
        'requires',
4✔
808
        'conflicts',
4✔
809
        'provides',
810
        'obsoletes',
811
        'recommends',
812
        'suggests',
813
        'enhances',
814
        'supplements',
815
    ]
816

817
    def __init__(self, name, is_source=False):
818
        self.timers = {'ExtractRpm': 0, 'libmagic': 0}
819
        self.name = str(name)
4✔
820
        self.filename = f'{name}.rpm'
4✔
821
        self.arch = None
4✔
822
        self.current_linenum = None
4✔
823
        self.dirname = None
4✔
824
        self.is_source = False
4✔
825

4✔
826
        # files are dictionary where key is name of a file
4✔
827
        self.files = {}
828
        self.ghost_files = {}
829

4✔
830
        # header is a dictionary to mock rpm metadata
4✔
831
        self.header = FakeHeader()
832
        for i in self._autoheaders:
833
            # the header name wihtout the ending 's'
4✔
834
            tagname = i[:-1].upper()
4✔
835
            self.header[getattr(rpm, f'RPMTAG_{tagname}NAME')] = []
836
            self.header[getattr(rpm, f'RPMTAG_{tagname}FLAGS')] = []
4✔
837
            self.header[getattr(rpm, f'RPMTAG_{tagname}VERSION')] = []
4✔
838
        self.header[rpm.RPMTAG_FILENAMES] = []
4✔
839

4✔
840
    def add_file(self, path, name):
4✔
841
        pkgfile = PkgFile(name)
842
        pkgfile.path = path
4✔
843
        self.files[name] = pkgfile
4✔
844
        return pkgfile
4✔
845

4✔
846
    def _mock_file(self, path, attrs):
4✔
847
        metadata = None
848
        if attrs.get('create_dirs', False):
4✔
849
            for i in PurePath(path).parents[:attrs.get('include_dirs', -1)]:
4✔
850
                self.add_dir(str(i))
4✔
851
        metadata = attrs.get('metadata', None)
4✔
852

4✔
853
        if attrs.get('is_dir', False):
4✔
854
            self.add_dir(path, metadata=metadata)
855
            return
4✔
856

4✔
857
        content = ''
4✔
858
        if 'content-path' in attrs:
859
            content = open(attrs['content-path'], 'rb')
4✔
860
        elif 'content' in attrs:
4✔
861
            content = attrs['content']
4✔
862

4✔
863
        if 'linkto' in attrs:
4✔
864
            self.add_symlink_to(path, attrs['linkto'])
865
        else:
4✔
866
            self.add_file_with_content(path, content, metadata=metadata)
4✔
867
        self.header[rpm.RPMTAG_FILENAMES].append(path)
868

4✔
869
        if 'content-path' in attrs:
4✔
870
            content.close()
871

4✔
872
    def create_files(self, files):
4✔
873
        """
874
        This is a helper method to create files(real files); not PkgFile
4✔
875
        objects.
876
        """
877

878
        # files can be just a list
879
        if isinstance(files, (list, tuple)):
880
            for path in files:
881
                self._mock_file(path, {})
4✔
882
        # list of files with attributes and content
4✔
883
        elif isinstance(files, dict):
4✔
884
            for path, file in files.items():
885
                self._mock_file(path, file)
4✔
886

4✔
887
    def add_dir(self, path, metadata=None):
4✔
888
        name = path
889
        pkgdir = PkgFile(name)
4✔
890
        pkgdir.magic = 'directory'
4✔
891

4✔
892
        path = os.path.join(self.dir_name(), path.lstrip('/'))
4✔
893
        os.makedirs(Path(path), exist_ok=True)
894
        pkgdir.inode = os.stat(Path(path)).st_ino
4✔
895

4✔
896
        pkgdir.path = path
4✔
897
        self.files[name] = pkgdir
898

4✔
899
        if metadata:
4✔
900
            for k, v in metadata.items():
901
                setattr(pkgdir, k, v)
4✔
902

4✔
903
        return pkgdir
4✔
904

905
    def add_file_with_content(self, name, content, metadata=None, **flags):
4✔
906
        """
907
        Add file to the FakePkg and fill the file with provided
4✔
908
        string content.
909
        """
910
        path = os.path.join(self.dir_name(), name.lstrip('/'))
911
        pkg_file = PkgFile(name)
912
        pkg_file.path = path
4✔
913
        pkg_file.mode = stat.S_IFREG | 0o0644
4✔
914
        pkg_file.user = 'root'
4✔
915
        pkg_file.group = 'root'
4✔
916
        self.files[name] = pkg_file
4✔
917

4✔
918
        # create files in filesystem
4✔
919
        os.makedirs(Path(path).parent, exist_ok=True)
920

921
        if isinstance(content, str):
4✔
922
            content = content.encode('utf-8', errors='ignore')
923

4✔
924
        with open(Path(path), 'wb') as out:
4✔
925
            # file like content
926
            if isinstance(content, io.IOBase):
4✔
927
                shutil.copyfileobj(content, out)
928
            else:
4✔
929
                out.write(content)
4✔
930

931
        # Generating md5 hash values for real files:
4✔
932
        pkg_file.md5 = self.md5_checksum(Path(path))
933
        pkg_file.size = os.path.getsize(Path(path))
934
        pkg_file.inode = os.stat(Path(path)).st_ino
4✔
935
        pkg_file.magic = self._calc_magic(pkg_file)
4✔
936

4✔
937
        if metadata:
4✔
938
            for k, v in metadata.items():
939
                setattr(pkg_file, k, v)
4✔
940
        for key, value in flags.items():
4✔
941
            setattr(pkg_file, key, value)
4✔
942

4✔
943
    def initiate_files_base_data(self):
4✔
944
        """ This method is called after adding metadata of each file """
945
        self.config_files = [x.name for x in self.files.values() if x.is_config]
4✔
946
        self.doc_files = [x.name for x in self.files.values() if x.is_doc]
947
        self.ghost_files = [x.name for x in self.files.values() if x.is_ghost]
4✔
948
        self.noreplace_files = [x.name for x in self.files.values() if x.is_noreplace]
4✔
949
        self.missingok_files = [x.name for x in self.files.values() if x.is_missingok]
4✔
950

4✔
951
    def add_header(self, header):
4✔
952
        for k, v in header.items():
953
            if k in self._autoheaders:
4✔
954
                # the header name wihtout the ending 's'
4✔
955
                tagname = k[:-1].upper()
4✔
956
                for i in v:
957
                    name, flags, version = parse_deps(i)[0]
4✔
958
                    version = versionToString(version)
4✔
959
                    self.header[getattr(rpm, f'RPMTAG_{tagname}NAME')].append(name)
4✔
960
                    self.header[getattr(rpm, f'RPMTAG_{tagname}FLAGS')].append(flags)
4✔
961
                    self.header[getattr(rpm, f'RPMTAG_{tagname}VERSION')].append(version)
4✔
962
                continue
4✔
963

4✔
964
            key = getattr(rpm, f'RPMTAG_{k}'.upper())
4✔
965
            self.header[key] = v
966

4✔
967
            if key == rpm.RPMTAG_ARCH:
4✔
968
                self.arch = v
969

4✔
970
        (self.requires, self.prereq, self.provides, self.conflicts,
4✔
971
         self.obsoletes, self.recommends, self.suggests, self.enhances,
972
         self.supplements) = self._gather_dep_info()
4✔
973

974
        self.req_names = [x[0] for x in self.requires + self.prereq]
975

976
    def add_dependency(self, dep):
4✔
977
        name, flags, version = parse_deps(dep)[0]
978
        version = versionToString(version)
4✔
979
        self.header[rpm.RPMTAG_REQUIRESNAME].append(name)
×
980
        self.header[rpm.RPMTAG_REQUIRESFLAGS].append(flags)
×
981
        self.header[rpm.RPMTAG_REQUIRESVERSION].append(version)
×
982

×
983
        _requires = []
×
984
        _prereq = []
985
        self.requires, self.prereq = self._gather_aux(self.header, _requires,
×
986
                                                      rpm.RPMTAG_REQUIRENAME,
×
987
                                                      rpm.RPMTAG_REQUIREFLAGS,
×
988
                                                      rpm.RPMTAG_REQUIREVERSION,
989
                                                      _prereq)
990

991
        self.req_names = [x[0] for x in self.requires + self.prereq]
992

993
    def add_symlink_to(self, name, target):
×
994
        """
995
        Add symlink to name file which path is related to name.
4✔
996
        Eg. name == '/etc/foo' and target == '../bar' creates a symlink file
997
        /etc/bar that points to /etc/foo.
998
        """
999
        pkg_file = PkgFile(name)
1000
        pkg_file.mode = stat.S_IFLNK
1001
        pkg_file.linkto = target
4✔
1002
        pkg_file.user = 'root'
4✔
1003
        pkg_file.group = 'root'
4✔
1004
        self.files[name] = pkg_file
4✔
1005

4✔
1006
    def readlink(self, pkgfile):
4✔
1007
        # HACK: reuse the real Pkg's logic
1008
        return Pkg.readlink(self, pkgfile)
4✔
1009

4✔
1010
    def dir_name(self):
4✔
1011
        if not self.dirname:
4✔
1012
            self.__tmpdir = tempfile.TemporaryDirectory(prefix='rpmlint.%s.' % Path(self.name).name)
4✔
1013
            self.dirname = self.__tmpdir.name
1014
        return self.dirname
4✔
1015

1016
    def md5_checksum(self, file_name):
4✔
1017
        md5_hash = hashlib.md5()
1018
        with open(file_name, 'rb') as f:
4✔
1019
            for byte_block in iter(lambda: f.read(4096), b''):
4✔
1020
                md5_hash.update(byte_block)
4✔
1021
        return md5_hash.hexdigest()
4✔
1022

4✔
1023
    def cleanup(self):
1024
        if self.dirname:
4✔
1025
            self.__tmpdir.cleanup()
4✔
1026

4✔
1027
    def get_core_reqs(self):
4✔
1028
        core_reqs = []
4✔
1029
        return core_reqs
4✔
1030

1031
    # access the tags like an array
4✔
1032
    def __getitem__(self, key):
4✔
1033
        return self.header.get(key, None)
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc