suse-filter-exception.diff
[platform/upstream/rpmlint.git] / Pkg.py
1 # -*- coding: utf-8 -*-
2 #############################################################################
3 # File          : Pkg.py
4 # Package       : rpmlint
5 # Author        : Frederic Lepied
6 # Created on    : Tue Sep 28 07:18:06 1999
7 # Version       : $Id: Pkg.py 1892 2011-11-23 20:21:05Z scop $
8 # Purpose       : provide an API to handle a rpm package either by accessing
9 #                 the rpm file or by accessing the files contained inside.
10 #############################################################################
11
12 import commands
13 import os
14 import re
15 import stat
16 import subprocess
17 import sys
18 import tempfile
19 import types
20 import urlparse
21
22 try:
23     import magic
24     # TODO: magic.MAGIC_COMPRESS when PkgFile gets decompress support.
25     _magic = magic.open(magic.MAGIC_NONE)
26     _magic.load()
27 except:
28     _magic = None
29 import rpm
30
31 import Filter
32
33 # Python 2/3 compatibility/convenience wrapper for printing to stderr with
34 # less concerns of UnicodeErrors than plain sys.stderr.write.
35 if sys.version_info[0] > 2:
36     # Blows up with Python < 3 without the exec() hack
37     exec('def warn(s): print (s, file=sys.stderr)')
38 else:
39     def warn(s): print >> sys.stderr, s
40
41
42 # utilities
43
44 # 64: RPMSENSE_PREREQ is 0 with recent rpm versions, we want 64 here in order
45 # to do the right thing with packages built with older rpm versions
46 PREREQ_FLAG = (rpm.RPMSENSE_PREREQ or 64) | \
47               rpm.RPMSENSE_SCRIPT_PRE | \
48               rpm.RPMSENSE_SCRIPT_POST | \
49               rpm.RPMSENSE_SCRIPT_PREUN | \
50               rpm.RPMSENSE_SCRIPT_POSTUN
51
52 var_regex = re.compile('^(.*)\${?(\w+)}?(.*)$')
53
54 def shell_var_value(var, script):
55     assign_regex = re.compile('\\b' + re.escape(var) + '\s*=\s*(.+)\s*(#.*)*$',
56                               re.MULTILINE)
57     res = assign_regex.search(script)
58     if res:
59         res2 = var_regex.search(res.group(1))
60         if res2:
61             if res2.group(2) == var: # infinite loop
62                 return None
63         return substitute_shell_vars(res.group(1), script)
64     else:
65         return None
66
67 def substitute_shell_vars(val, script):
68     res = var_regex.search(val)
69     if res:
70         value = shell_var_value(res.group(2), script)
71         if not value:
72             value = ''
73         return res.group(1) + value + \
74             substitute_shell_vars(res.group(3), script)
75     else:
76         return val
77
78 def getstatusoutput(cmd, stdoutonly = False):
79     '''A version of commands.getstatusoutput() which can take cmd as a
80        sequence, thus making it potentially more secure.'''
81     if stdoutonly:
82         proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
83                                 stdout=subprocess.PIPE, close_fds=True)
84     else:
85         proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
86                                 stdout=subprocess.PIPE,
87                                 stderr=subprocess.STDOUT, close_fds=True)
88     proc.stdin.close()
89     text = proc.stdout.read()
90     sts = proc.wait()
91     if sts is None:
92         sts = 0
93     if text.endswith('\n'):
94         text = text[:-1]
95     return sts, text
96
97 bz2_regex = re.compile('\.t?bz2?$')
98 xz_regex = re.compile('\.(t[xl]z|xz|lzma)$')
99
100 def catcmd(fname):
101     """Get a 'cat' command that handles possibly compressed files."""
102     cat = 'gzip -dcf'
103     if bz2_regex.search(fname):
104         cat = 'bzip2 -dcf'
105     elif xz_regex.search(fname):
106         cat = 'xz -dc'
107     return cat
108
109 def is_utf8(fname):
110     (sts, text) = getstatusoutput(catcmd(fname).split() + [fname])
111     return not sts and is_utf8_str(text)
112
113 def is_utf8_str(s):
114     try:
115         s.decode('UTF-8')
116     except:
117         return False
118     return True
119
120 def to_utf8(string):
121     if string is None:
122         return ''
123     elif isinstance(string, unicode):
124         return string
125     try:
126         x = unicode(string, 'ascii')
127         return string
128     except UnicodeError:
129         encodings = ['utf-8', 'iso-8859-1', 'iso-8859-15', 'iso-8859-2']
130         for enc in encodings:
131             try:
132                 x = unicode(string, enc)
133             except UnicodeError:
134                 pass
135             else:
136                 if x.encode(enc) == string:
137                     return x.encode('utf-8')
138     newstring = ''
139     for char in string:
140         if ord(char) > 127:
141             newstring = newstring + '?'
142         else:
143             newstring = newstring + char
144     return newstring
145
146 def readlines(path):
147     fobj = open(path, "r")
148     try:
149         return fobj.readlines()
150     finally:
151         fobj.close()
152
153 def mktemp():
154     tmpfd, tmpname = tempfile.mkstemp(prefix = 'rpmlint.')
155     tmpfile = os.fdopen(tmpfd, 'w')
156     return tmpfile, tmpname
157
158 slash_regex = re.compile('/+')
159 slashdot_regex = re.compile('/(\.(/|$))+')
160 slashend_regex = re.compile('([^/])/+$')
161
162 def safe_normpath(path):
163     """Like os.path.normpath but normalizes less aggressively thus being
164     potentially safer for paths containing symlinks."""
165     ret = slash_regex.sub('/', path)
166     ret = slashdot_regex.sub('\\2', ret)
167     ret = slashend_regex.sub('\\1', ret)
168     return ret
169
170 def get_default_valid_rpmgroups(filename = None):
171     """Get default rpm groups from filename, or try to look them up from
172     the rpm package (if installed) if no filename is given"""
173     groups = []
174     if not filename:
175         try:
176             p = InstalledPkg('rpm')
177         except:
178             pass
179         else:
180             groupsfiles = [x for x in p.files() if x.endswith('/GROUPS')]
181             if groupsfiles:
182                 filename = groupsfiles[0]
183     if filename and os.path.exists(filename):
184         fobj = open(filename)
185         try:
186             groups = fobj.read().strip().splitlines()
187         finally:
188             fobj.close()
189         if 'Development/Debug' not in groups:
190             groups.append('Development/Debug')
191         if 'Unspecified' not in groups: # auto-added by rpm >= 4.6.0
192             groups.append('Unspecified')
193         groups.sort()
194     return groups
195
196 # from yum 3.2.27, rpmUtils.miscutils, with rpmlint modifications
197 def compareEVR((e1, v1, r1), (e2, v2, r2)):
198     # return 1: a is newer than b
199     # 0: a and b are the same version
200     # -1: b is newer than a
201     # rpmlint mod: don't stringify None epochs to 'None' strings
202     if e1 is not None:
203         e1 = str(e1)
204     v1 = str(v1)
205     r1 = str(r1)
206     if e2 is not None:
207         e2 = str(e2)
208     v2 = str(v2)
209     r2 = str(r2)
210     rc = rpm.labelCompare((e1, v1, r1), (e2, v2, r2))
211     return rc
212
213 # from yum 3.2.27, rpmUtils.miscutils, with rpmlint modifications
214 def rangeCompare(reqtuple, provtuple):
215     """returns true if provtuple satisfies reqtuple"""
216     (reqn, reqf, (reqe, reqv, reqr)) = reqtuple
217     (n, f, (e, v, r)) = provtuple
218     if reqn != n:
219         return 0
220
221     # unversioned satisfies everything
222     if not f or not reqf:
223         return 1
224
225     # and you thought we were done having fun
226     # if the requested release is left out then we have
227     # to remove release from the package prco to make sure the match
228     # is a success - ie: if the request is EQ foo 1:3.0.0 and we have
229     # foo 1:3.0.0-15 then we have to drop the 15 so we can match
230     if reqr is None:
231         r = None
232     # rpmlint mod: don't mess with provided Epoch, doing so breaks e.g.
233     # "Requires: foo < 1.0" should not be satisfied by "Provides: foo = 1:0.5"
234     #if reqe is None:
235     #    e = None
236     if reqv is None: # just for the record if ver is None then we're going to segfault
237         v = None
238
239     # if we just require foo-version, then foo-version-* will match
240     if r is None:
241         reqr = None
242
243     rc = compareEVR((e, v, r), (reqe, reqv, reqr))
244
245     # does not match unless
246     if rc >= 1:
247         if reqf in ['GT', 'GE', 4, 12]:
248             return 1
249         if reqf in ['EQ', 8]:
250             if f in ['LE', 10, 'LT', 2]:
251                 return 1
252         if reqf in ['LE', 'LT', 'EQ', 10, 2, 8]:
253             if f in ['LE', 'LT', 10, 2]:
254                 return 1
255
256     if rc == 0:
257         if reqf in ['GT', 4]:
258             if f in ['GT', 'GE', 4, 12]:
259                 return 1
260         if reqf in ['GE', 12]:
261             if f in ['GT', 'GE', 'EQ', 'LE', 4, 12, 8, 10]:
262                 return 1
263         if reqf in ['EQ', 8]:
264             if f in ['EQ', 'GE', 'LE', 8, 12, 10]:
265                 return 1
266         if reqf in ['LE', 10]:
267             if f in ['EQ', 'LE', 'LT', 'GE', 8, 10, 2, 12]:
268                 return 1
269         if reqf in ['LT', 2]:
270             if f in ['LE', 'LT', 10, 2]:
271                 return 1
272     if rc <= -1:
273         if reqf in ['GT', 'GE', 'EQ', 4, 12, 8]:
274             if f in ['GT', 'GE', 4, 12]:
275                 return 1
276         if reqf in ['LE', 'LT', 10, 2]:
277             return 1
278 #                if rc >= 1:
279 #                    if reqf in ['GT', 'GE', 4, 12]:
280 #                        return 1
281 #                if rc == 0:
282 #                    if reqf in ['GE', 'LE', 'EQ', 8, 10, 12]:
283 #                        return 1
284 #                if rc <= -1:
285 #                    if reqf in ['LT', 'LE', 2, 10]:
286 #                        return 1
287
288     return 0
289
290 # from yum 3.2.23, rpmUtils.miscutils, with rpmlint modifications
291 def formatRequire(name, flags, evr):
292     s = name
293
294     if flags:
295         if flags & (rpm.RPMSENSE_LESS | rpm.RPMSENSE_GREATER |
296                     rpm.RPMSENSE_EQUAL):
297             s = s + " "
298             if flags & rpm.RPMSENSE_LESS:
299                 s = s + "<"
300             if flags & rpm.RPMSENSE_GREATER:
301                 s = s + ">"
302             if flags & rpm.RPMSENSE_EQUAL:
303                 s = s + "="
304             s = "%s %s" % (s, versionToString(evr))
305     return s
306
307 def versionToString(evr):
308     if not isinstance(evr, tuple) and not isinstance(evr, list):
309         # assume string
310         return evr
311     ret = ""
312     if evr[0] is not None and evr[0] != "":
313         ret += str(evr[0]) + ":"
314     if evr[1] is not None:
315         ret += evr[1]
316         if evr[2] is not None and evr[2] != "":
317             ret += "-" + evr[2]
318     return ret
319
320 # from yum 3.2.23, rpmUtils.miscutils, with some rpmlint modifications
321 def stringToVersion(verstring):
322     if verstring in (None, ''):
323         return (None, None, None)
324     epoch = None
325     i = verstring.find(':')
326     if i != -1:
327         try:
328             epoch = str(long(verstring[:i]))
329         except ValueError:
330             # garbage in epoch, ignore it
331             pass
332     i += 1
333     j = verstring.find('-', i)
334     if j != -1:
335         if verstring[i:j] == '':
336             version = None
337         else:
338             version = verstring[i:j]
339         release = verstring[j+1:]
340     else:
341         if verstring[i:] == '':
342             version = None
343         else:
344             version = verstring[i:]
345         release = None
346     return (epoch, version, release)
347
348 def parse_deps(line):
349     '''Parse provides/requires/conflicts/obsoletes line to list of
350        (name, flags, (epoch, version, release)) tuples.'''
351
352     prcos = []
353     tokens = re.split('[\s,]+', line.strip())
354
355     # Drop line continuation backslash in multiline macro definition (for
356     # spec file parsing), e.g.
357     # [...] \
358     # Obsoletes: foo-%1 <= 1.0.0 \
359     # [...] \
360     # (yes, this is an ugly hack and we probably have other problems with
361     #  multiline macro definitions elsewhere...)
362     if tokens[-1] == '\\':
363         del tokens[-1]
364
365     prco = []
366     while tokens:
367         token = tokens.pop(0)
368         if not token:
369             # skip empty tokens
370             continue
371
372         plen = len(prco)
373
374         if plen == 0:
375             prco.append(token)
376
377         elif plen == 1:
378             flags = 0
379             if token[0] in ("=", "<", "<=", ">", ">="):
380                 # versioned, flags
381                 if "=" in token:
382                     flags |= rpm.RPMSENSE_EQUAL
383                 if "<" in token:
384                     flags |= rpm.RPMSENSE_LESS
385                 if ">" in token:
386                     flags |= rpm.RPMSENSE_GREATER
387                 prco.append(flags)
388             else:
389                 # no flags following name, treat as unversioned, add and reset
390                 prco.extend((flags, (None, None, None)))
391                 prcos.append(tuple(prco))
392                 prco = [token]
393
394         elif plen == 2:
395             # last token of versioned one, add and reset
396             prco.append(stringToVersion(token))
397             prcos.append(tuple(prco))
398             prco = []
399
400     plen = len(prco)
401     if plen:
402         if plen == 1:
403             prco.extend((0, (None, None, None)))
404         elif plen == 2:
405             prco.append((None, None, None))
406         prcos.append(tuple(prco))
407
408     return prcos
409
410
411 # classes representing package
412
413 class Pkg:
414
415     _magic_from_compressed_re = re.compile('\([^)]+\s+compressed\s+data\\b')
416
417     def __init__(self, filename, dirname, header = None, is_source = False):
418         self.filename = filename
419         self.extracted = False
420         self.dirname = dirname
421         self.current_linenum = None
422         self._config_files = None
423         self._doc_files = None
424         self._noreplace_files = None
425         self._ghost_files = None
426         self._missingok_files = None
427         self._files = None
428         self._requires = None
429         self._req_names = -1
430
431         if header:
432             self.header = header
433             self.is_source = is_source
434         else:
435             # Create a package object from the file name
436             ts = rpm.TransactionSet()
437             # Don't check signatures here...
438             ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
439             fd = os.open(filename, os.O_RDONLY)
440             try:
441                 self.header = ts.hdrFromFdno(fd)
442             finally:
443                 os.close(fd)
444             self.is_source = not self.header[rpm.RPMTAG_SOURCERPM]
445
446         self.name = self.header[rpm.RPMTAG_NAME]
447         if self.isNoSource():
448             self.arch = 'nosrc'
449         elif self.isSource():
450             self.arch = 'src'
451         else:
452             self.arch = self.header[rpm.RPMTAG_ARCH]
453
454     # Return true if the package is a source package
455     def isSource(self):
456         return self.is_source
457
458     # Return true if the package is a nosource package.
459     # NoSource files are ghosts in source packages.
460     def isNoSource(self):
461         return self.is_source and self.ghostFiles()
462
463     # access the tags like an array
464     def __getitem__(self, key):
465         try:
466             val = self.header[key]
467         except:
468             val = []
469         if val == []:
470             return None
471         else:
472             return val
473
474     # return the name of the directory where the package is extracted
475     def dirName(self):
476         if not self.extracted:
477             self._extract()
478         return self.dirname
479
480     # extract rpm contents
481     def _extract(self):
482         s = os.stat(self.dirname)
483         if not stat.S_ISDIR(s[stat.ST_MODE]):
484             warn('Unable to access dir %s' % self.dirname)
485             return None
486         else:
487             self.dirname = tempfile.mkdtemp(
488                 prefix = 'rpmlint.%s.' % os.path.basename(self.filename),
489                 dir = self.dirname)
490             # TODO: better shell escaping or sequence based command invocation
491             command_str = \
492                 'rpm2cpio "%s" | (cd "%s"; cpio -id); chmod -R +rX "%s"' % \
493                 (self.filename, self.dirname, self.dirname)
494             cmd = commands.getstatusoutput(command_str)
495             self.extracted = True
496             return cmd
497
498     def checkSignature(self):
499         return getstatusoutput(('env', 'LC_ALL=C', 'rpm', '-K', self.filename))
500
501     # remove the extracted files from the package
502     def cleanup(self):
503         if self.extracted and self.dirname:
504             getstatusoutput(('rm', '-rf', self.dirname))
505
506     def grep(self, regex, filename):
507         """Grep regex from a file, return matching line numbers."""
508         ret = []
509         lineno = 0
510         in_file = None
511         try:
512             try:
513                 in_file = open(self.dirName() + '/' + filename)
514                 for line in in_file:
515                     lineno += 1
516                     if regex.search(line):
517                         ret.append(str(lineno))
518                         break
519             except Exception, e:
520                 Filter.printWarning(self, 'read-error', filename, e)
521         finally:
522             if in_file:
523                 in_file.close()
524         return ret
525
526     def langtag(self, tag, lang):
527         """Get value of tag in the given language."""
528         # LANGUAGE trumps other env vars per GNU gettext docs, see also #166
529         orig = os.environ.get('LANGUAGE')
530         os.environ['LANGUAGE'] = lang
531         ret = self[tag]
532         if orig is not None:
533             os.environ['LANGUAGE'] = orig
534         return ret
535
536     # return the associative array indexed on file names with
537     # the values as: (file perm, file owner, file group, file link to)
538     def files(self):
539         if self._files is not None:
540             return self._files
541
542         self._gatherFilesInfo()
543         return self._files
544
545     # return the list of config files
546     def configFiles(self):
547         if self._config_files is not None:
548             return self._config_files
549
550         self._config_files = [x.name for x in self.files().values()
551                               if x.is_config]
552         return self._config_files
553
554     # return the list of noreplace files
555     def noreplaceFiles(self):
556         if self._noreplace_files is not None:
557             return self._noreplace_files
558
559         self._noreplace_files = [x.name for x in self.files().values()
560                                  if x.is_noreplace]
561         return self._noreplace_files
562
563     # return the list of documentation files
564     def docFiles(self):
565         if self._doc_files is not None:
566             return self._doc_files
567
568         self._doc_files = [x.name for x in self.files().values() if x.is_doc]
569         return self._doc_files
570
571     # return the list of ghost files
572     def ghostFiles(self):
573         if self._ghost_files is not None:
574             return self._ghost_files
575
576         self._ghost_files = [x.name for x in self.files().values()
577                              if x.is_ghost]
578         return self._ghost_files
579
580     def missingOkFiles(self):
581         if self._missingok_files is not None:
582             return self._missingok_files
583
584         self._missingok_files = [x.name for x in self.files().values()
585                                  if x.is_missingok]
586         return self._missingok_files
587
588     # extract information about the files
589     def _gatherFilesInfo(self):
590
591         self._files = {}
592         flags = self.header[rpm.RPMTAG_FILEFLAGS]
593         modes = self.header[rpm.RPMTAG_FILEMODES]
594         users = self.header[rpm.RPMTAG_FILEUSERNAME]
595         groups = self.header[rpm.RPMTAG_FILEGROUPNAME]
596         links = self.header[rpm.RPMTAG_FILELINKTOS]
597         sizes = self.header[rpm.RPMTAG_FILESIZES]
598         md5s = self.header[rpm.RPMTAG_FILEMD5S]
599         mtimes = self.header[rpm.RPMTAG_FILEMTIMES]
600         rdevs = self.header[rpm.RPMTAG_FILERDEVS]
601         langs = self.header[rpm.RPMTAG_FILELANGS]
602         inodes = self.header[rpm.RPMTAG_FILEINODES]
603         requires = self.header[rpm.RPMTAG_FILEREQUIRE]
604         provides = self.header[rpm.RPMTAG_FILEPROVIDE]
605         files = self.header[rpm.RPMTAG_FILENAMES]
606         magics = self.header[rpm.RPMTAG_FILECLASS]
607         try: # rpm >= 4.7.0
608             filecaps = self.header[rpm.RPMTAG_FILECAPS]
609         except:
610             filecaps = None
611
612         # rpm-python < 4.6 does not return a list for this (or FILEDEVICES,
613         # FWIW) for packages containing exactly one file
614         if not isinstance(inodes, types.ListType):
615             inodes = [inodes]
616
617         if files:
618             for idx in range(0, len(files)):
619                 pkgfile = PkgFile(files[idx])
620                 # Do not use os.path.join here, pkgfile.name can start with a
621                 # / which would result in self.dirName being ignored
622                 pkgfile.path = os.path.normpath(
623                     self.dirName() + '/' + pkgfile.name)
624                 pkgfile.flags = flags[idx]
625                 pkgfile.mode = modes[idx]
626                 pkgfile.user = users[idx]
627                 pkgfile.group = groups[idx]
628                 pkgfile.linkto = links[idx] and safe_normpath(links[idx])
629                 pkgfile.size = sizes[idx]
630                 pkgfile.md5 = md5s[idx]
631                 pkgfile.mtime = mtimes[idx]
632                 pkgfile.rdev = rdevs[idx]
633                 pkgfile.inode = inodes[idx]
634                 pkgfile.requires = parse_deps(requires[idx])
635                 pkgfile.provides = parse_deps(provides[idx])
636                 pkgfile.lang = langs[idx]
637                 pkgfile.magic = magics[idx]
638                 if not pkgfile.magic and _magic:
639                     pkgfile.magic = _magic.file(pkgfile.path)
640                 if pkgfile.magic is None:
641                     pkgfile.magic = ''
642                 elif Pkg._magic_from_compressed_re.search(pkgfile.magic):
643                     # Discard magic from inside compressed files ("file -z")
644                     # until PkgFile gets decompression support.  We may get
645                     # such magic strings from package headers already now;
646                     # for example Fedora's rpmbuild as of F-11's 4.7.1 is
647                     # patched so it generates them.
648                     pkgfile.magic = ''
649                 if filecaps:
650                     pkgfile.filecaps = filecaps[idx]
651                 self._files[pkgfile.name] = pkgfile
652
653     def readlink(self, pkgfile):
654         """Resolve symlinks for the given PkgFile, return the dereferenced
655            PkgFile if it is found in this package, None if not."""
656         result = pkgfile
657         while result and result.linkto:
658             linkpath = urlparse.urljoin(result.name, result.linkto)
659             linkpath = safe_normpath(linkpath)
660             result = self.files().get(linkpath)
661         return result
662
663     # API to access dependency information
664     def obsoletes(self):
665         """Get package Obsoletes as list of
666            (name, flags, (epoch, version, release)) tuples."""
667         self._gatherDepInfo()
668         return self._obsoletes
669
670     def requires(self):
671         """Get package Requires as list of
672            (name, flags, (epoch, version, release)) tuples."""
673         self._gatherDepInfo()
674         return self._requires
675
676     def prereq(self):
677         """Get package PreReqs as list of
678            (name, flags, (epoch, version, release)) tuples."""
679         self._gatherDepInfo()
680         return self._prereq
681
682     def req_names(self):
683         if self._req_names == -1:
684             self._req_names = [x[0] for x in self.requires() + self.prereq()]
685         return self._req_names
686
687     def check_versioned_dep(self, name, version):
688         # try to match name%_isa as well (e.g. "foo(x86-64)", "foo(x86-32)")
689         name_re = re.compile('^%s(\(\w+-\d+\))?$' % re.escape(name))
690         for d in self.requires() + self.prereq():
691             if name_re.match(d[0]):
692                 if d[1] & rpm.RPMSENSE_EQUAL != rpm.RPMSENSE_EQUAL \
693                         or d[2][1] != version:
694                     return False
695                 return True
696         return False
697
698     def conflicts(self):
699         """Get package Conflicts as list of
700            (name, flags, (epoch, version, release)) tuples."""
701         self._gatherDepInfo()
702         return self._conflicts
703
704     def provides(self):
705         """Get package Provides as list of
706            (name, flags, (epoch, version, release)) tuples."""
707         self._gatherDepInfo()
708         return self._provides
709
710     # internal function to gather dependency info used by the above ones
711     def _gather_aux(self, header, list, nametag, flagstag, versiontag,
712                     prereq = None):
713         names = header[nametag]
714         flags = header[flagstag]
715         versions = header[versiontag]
716
717         if versions:
718             for loop in range(len(versions)):
719                 evr = stringToVersion(versions[loop])
720                 if prereq is not None and flags[loop] & PREREQ_FLAG:
721                     prereq.append((names[loop], flags[loop] & (~PREREQ_FLAG),
722                                    evr))
723                 else:
724                     list.append((names[loop], flags[loop], evr))
725
726     def _gatherDepInfo(self):
727         if self._requires is None:
728             self._requires = []
729             self._prereq = []
730             self._provides = []
731             self._conflicts = []
732             self._obsoletes = []
733
734             self._gather_aux(self.header, self._requires,
735                              rpm.RPMTAG_REQUIRENAME,
736                              rpm.RPMTAG_REQUIREFLAGS,
737                              rpm.RPMTAG_REQUIREVERSION,
738                              self._prereq)
739             self._gather_aux(self.header, self._conflicts,
740                              rpm.RPMTAG_CONFLICTNAME,
741                              rpm.RPMTAG_CONFLICTFLAGS,
742                              rpm.RPMTAG_CONFLICTVERSION)
743             self._gather_aux(self.header, self._provides,
744                              rpm.RPMTAG_PROVIDENAME,
745                              rpm.RPMTAG_PROVIDEFLAGS,
746                              rpm.RPMTAG_PROVIDEVERSION)
747             self._gather_aux(self.header, self._obsoletes,
748                              rpm.RPMTAG_OBSOLETENAME,
749                              rpm.RPMTAG_OBSOLETEFLAGS,
750                              rpm.RPMTAG_OBSOLETEVERSION)
751
752     def scriptprog(self, which):
753         """Get the specified script interpreter as a string.
754            Depending on rpm-python version, the string may or may not include
755            interpreter arguments, if any."""
756         prog = self[which]
757         if prog is None:
758             prog = ""
759         elif not isinstance(prog, basestring):
760             # http://rpm.org/ticket/847#comment:2
761             prog = " ".join(prog)
762         return prog
763
764 def getInstalledPkgs(name):
765     """Get list of installed package objects by name."""
766
767     pkgs = []
768     ts = rpm.TransactionSet()
769     if re.search('[?*]|\[.+\]', name):
770         mi = ts.dbMatch()
771         mi.pattern("name", rpm.RPMMIRE_GLOB, name)
772     else:
773         mi = ts.dbMatch("name", name)
774
775     for hdr in mi:
776         pkgs.append(InstalledPkg(name, hdr))
777
778     return pkgs
779
780 # Class to provide an API to an installed package
781 class InstalledPkg(Pkg):
782     def __init__(self, name, hdr = None):
783         if not hdr:
784             ts = rpm.TransactionSet()
785             mi = ts.dbMatch('name', name)
786             if not mi:
787                 raise KeyError(name)
788             try:
789                 hdr = mi.next()
790             except StopIteration:
791                 raise KeyError(name)
792
793         Pkg.__init__(self, name, '/', hdr)
794
795         self.extracted = True
796         # create a fake filename to satisfy some checks on the filename
797         self.filename = '%s-%s-%s.%s.rpm' % \
798             (self.name, self[rpm.RPMTAG_VERSION], self[rpm.RPMTAG_RELEASE],
799              self[rpm.RPMTAG_ARCH])
800
801     def cleanup(self):
802         pass
803
804     def checkSignature(self):
805         return (0, 'fake: pgp md5 OK')
806
807 # Class to provide an API to a "fake" package, eg. for specfile-only checks
808 class FakePkg:
809     def __init__(self, name):
810         self.name = name
811         self.arch = None
812         self.current_linenum = None
813
814     def cleanup(self):
815         pass
816
817 # Class for files in packages
818 class PkgFile(object):
819
820     def __init__(self, name):
821         self.name = name
822         # Real path to the file (taking extract dir into account)
823         self.path = name
824         self.flags = 0
825         self.mode = 0
826         self.user = None
827         self.group = None
828         self.linkto = ''
829         self.size = None
830         self.md5 = None
831         self.mtime = 0
832         self.rdev = ''
833         self.inode = 0
834         self.requires = []
835         self.provides = []
836         self.lang = ''
837         self.magic = ''
838         self.filecaps = None
839
840     # TODO: decompression support
841
842     is_config    = property(lambda self: self.flags & rpm.RPMFILE_CONFIG)
843     is_doc       = property(lambda self: self.flags & rpm.RPMFILE_DOC)
844     is_noreplace = property(lambda self: self.flags & rpm.RPMFILE_NOREPLACE)
845     is_ghost     = property(lambda self: self.flags & rpm.RPMFILE_GHOST)
846     is_missingok = property(lambda self: self.flags & rpm.RPMFILE_MISSINGOK)
847
848
849 if __name__ == '__main__':
850     for p in sys.argv[1:]:
851         pkg = Pkg(sys.argv[1], tempfile.gettempdir())
852         print ('Requires: %s' % pkg.requires())
853         print ('Prereq: %s' % pkg.prereq())
854         print ('Conflicts: %s' % pkg.conflicts())
855         print ('Provides: %s' % pkg.provides())
856         print ('Obsoletes: %s' % pkg.obsoletes())
857         pkg.cleanup()
858
859 # Pkg.py ends here
860
861 # Local variables:
862 # indent-tabs-mode: nil
863 # py-indent-offset: 4
864 # End:
865 # ex: ts=4 sw=4 et