1
2from __future__ import print_function
3
4import os.path
5import re
6import tarfile
7from . import packagequery
8import subprocess
9
10class ArchError(packagequery.PackageError):
11    pass
12
13class ArchQuery(packagequery.PackageQuery, packagequery.PackageQueryResult):
14    def __init__(self, fh):
15        self.__file = fh
16        self.__path = os.path.abspath(fh.name)
17        self.fields = {}
18        #self.magic = None
19        #self.pkgsuffix = 'pkg.tar.gz'
20        self.pkgsuffix = b'arch'
21
22    def read(self, all_tags=True, self_provides=True, *extra_tags):
23        # all_tags and *extra_tags are currently ignored
24        f = open(self.__path, 'rb')
25        #self.magic = f.read(5)
26        #if self.magic == '\375\067zXZ':
27        #    self.pkgsuffix = 'pkg.tar.xz'
28        fn = open('/dev/null', 'wb')
29        pipe = subprocess.Popen(['tar', '-O', '-xf', self.__path, '.PKGINFO'], stdout=subprocess.PIPE, stderr=fn).stdout
30        for line in pipe.readlines():
31            line = line.rstrip().split(b' = ', 2)
32            if len(line) == 2:
33                field, value = line[0].decode('ascii'), line[1]
34                self.fields.setdefault(field, []).append(value)
35        if self_provides:
36            prv = b'%s = %s' % (self.name(), self.fields['pkgver'][0])
37            self.fields.setdefault('provides', []).append(prv)
38        return self
39
40    def vercmp(self, archq):
41        res = packagequery.cmp(int(self.epoch()), int(archq.epoch()))
42        if res != 0:
43            return res
44        res = ArchQuery.rpmvercmp(self.version(), archq.version())
45        if res != 0:
46            return res
47        res = ArchQuery.rpmvercmp(self.release(), archq.release())
48        return res
49
50    def name(self):
51        return self.fields['pkgname'][0] if 'pkgname' in self.fields else None
52
53    def version(self):
54        pkgver = self.fields['pkgver'][0] if 'pkgver' in self.fields else None
55        if pkgver != None:
56            pkgver = re.sub(br'[0-9]+:', b'', pkgver, 1)
57            pkgver = re.sub(br'-[^-]*$', b'', pkgver)
58        return pkgver
59
60    def release(self):
61        pkgver = self.fields['pkgver'][0] if 'pkgver' in self.fields else None
62        if pkgver != None:
63            m = re.search(br'-([^-])*$', pkgver)
64            if m:
65                return m.group(1)
66        return None
67
68    def _epoch(self):
69        pkgver = self.fields.get('pkgver', [b''])[0]
70        if pkgver:
71            m = re.match(br'([0-9])+:', pkgver)
72            if m:
73                return m.group(1)
74        return b''
75
76    def epoch(self):
77        epoch = self._epoch()
78        if epoch:
79            return epoch
80        return b'0'
81
82    def arch(self):
83        return self.fields['arch'][0] if 'arch' in self.fields else None
84
85    def description(self):
86        return self.fields['pkgdesc'][0] if 'pkgdesc' in self.fields else None
87
88    def path(self):
89        return self.__path
90
91    def provides(self):
92        return self.fields['provides'] if 'provides' in self.fields else []
93
94    def requires(self):
95        return self.fields['depend'] if 'depend' in self.fields else []
96
97    def conflicts(self):
98        return self.fields['conflict'] if 'conflict' in self.fields else []
99
100    def obsoletes(self):
101        return self.fields['replaces'] if 'replaces' in self.fields else []
102
103    def recommends(self):
104        # a .PKGINFO has no notion of "recommends"
105        return []
106
107    def suggests(self):
108        # libsolv treats an optdepend as a "suggests", hence we do the same
109        if 'optdepend' not in self.fields:
110            return []
111        return [re.sub(b':.*', b'', entry) for entry in self.fields['optdepend']]
112
113    def supplements(self):
114        # a .PKGINFO has no notion of "recommends"
115        return []
116
117    def enhances(self):
118        # a .PKGINFO has no notion of "enhances"
119        return []
120
121    def canonname(self):
122        name = self.name()
123        if name is None:
124            raise ArchError(self.path(), 'package has no name')
125        version = self.version()
126        if version is None:
127            raise ArchError(self.path(), 'package has no version')
128        arch = self.arch()
129        if arch is None:
130            raise ArchError(self.path(), 'package has no arch')
131        return ArchQuery.filename(name, self._epoch(), version, self.release(),
132                                  arch)
133
134    def gettag(self, tag):
135        # implement me, if needed
136        return None
137
138    @staticmethod
139    def query(filename, all_tags = False, *extra_tags):
140        f = open(filename, 'rb')
141        archq = ArchQuery(f)
142        archq.read(all_tags, *extra_tags)
143        f.close()
144        return archq
145
146    @staticmethod
147    def rpmvercmp(ver1, ver2):
148        """
149        implementation of RPM's version comparison algorithm
150        (as described in lib/rpmvercmp.c)
151        """
152        if ver1 == ver2:
153            return 0
154        elif ver1 is None:
155            return -1
156        elif ver2 is None:
157            return 1
158        res = 0
159        while res == 0:
160            # remove all leading non alphanumeric chars
161            ver1 = re.sub(b'^[^a-zA-Z0-9]*', b'', ver1)
162            ver2 = re.sub(b'^[^a-zA-Z0-9]*', b'', ver2)
163            if not (len(ver1) and len(ver2)):
164                break
165            # check if we have a digits segment
166            mo1 = re.match(b'(\d+)', ver1)
167            mo2 = re.match(b'(\d+)', ver2)
168            numeric = True
169            if mo1 is None:
170                mo1 = re.match(b'([a-zA-Z]+)', ver1)
171                mo2 = re.match(b'([a-zA-Z]+)', ver2)
172                numeric = False
173            # check for different types: alpha and numeric
174            if mo2 is None:
175                if numeric:
176                    return 1
177                return -1
178            seg1 = mo1.group(0)
179            ver1 = ver1[mo1.end(0):]
180            seg2 = mo2.group(1)
181            ver2 = ver2[mo2.end(1):]
182            if numeric:
183                # remove leading zeros
184                seg1 = re.sub(b'^0+', b'', seg1)
185                seg2 = re.sub(b'^0+', b'', seg2)
186                # longer digit segment wins - if both have the same length
187                # a simple ascii compare decides
188                res = len(seg1) - len(seg2) or packagequery.cmp(seg1, seg2)
189            else:
190                res = packagequery.cmp(seg1, seg2)
191        if res > 0:
192            return 1
193        elif res < 0:
194            return -1
195        return packagequery.cmp(ver1, ver2)
196
197    @staticmethod
198    def filename(name, epoch, version, release, arch):
199        if epoch:
200            if release:
201                return b'%s-%s:%s-%s-%s.arch' % (name, epoch, version, release, arch)
202            else:
203                return b'%s-%s:%s-%s.arch' % (name, epoch, version, arch)
204        if release:
205            return b'%s-%s-%s-%s.arch' % (name, version, release, arch)
206        else:
207            return b'%s-%s-%s.arch' % (name, version, arch)
208
209
210if __name__ == '__main__':
211    import sys
212    archq = ArchQuery.query(sys.argv[1])
213    print(archq.name(), archq.version(), archq.release(), archq.arch())
214    try:
215        print(archq.canonname())
216    except ArchError as e:
217        print(e.msg)
218    print(archq.description())
219    print('##########')
220    print(b'\n'.join(archq.provides()))
221    print('##########')
222    print(b'\n'.join(archq.requires()))
223