]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
utils.py
[dak.git] / daklib / utils.py
1 #!/usr/bin/env python
2 # vim:set et ts=4 sw=4:
3
4 # Utility functions
5 # Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
6
7 ################################################################################
8
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
13
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18
19 # You should have received a copy of the GNU General Public License
20 # along with this program; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22
23 ################################################################################
24
25 import codecs, commands, email.Header, os, pwd, re, select, socket, shutil, \
26        sys, tempfile, traceback, stat
27 import apt_pkg
28 import database
29 import time
30 from dak_exceptions import *
31
32 ################################################################################
33
34 re_comments = re.compile(r"\#.*")
35 re_no_epoch = re.compile(r"^\d+\:")
36 re_no_revision = re.compile(r"-[^-]+$")
37 re_arch_from_filename = re.compile(r"/binary-[^/]+/")
38 re_extract_src_version = re.compile (r"(\S+)\s*\((.*)\)")
39 re_isadeb = re.compile (r"(.+?)_(.+?)_(.+)\.u?deb$")
40 re_issource = re.compile (r"(.+)_(.+?)\.(orig\.tar\.gz|diff\.gz|tar\.gz|dsc)$")
41
42 re_single_line_field = re.compile(r"^(\S*)\s*:\s*(.*)")
43 re_multi_line_field = re.compile(r"^\s(.*)")
44 re_taint_free = re.compile(r"^[-+~/\.\w]+$")
45
46 re_parse_maintainer = re.compile(r"^\s*(\S.*\S)\s*\<([^\>]+)\>")
47 re_gpg_uid = re.compile('^uid.*<([^>]*)>')
48
49 re_srchasver = re.compile(r"^(\S+)\s+\((\S+)\)$")
50 re_verwithext = re.compile(r"^(\d+)(?:\.(\d+))(?:\s+\((\S+)\))?$")
51
52 re_srchasver = re.compile(r"^(\S+)\s+\((\S+)\)$")
53
54 html_escaping = {'"':'&quot;', '&':'&amp;', '<':'&lt;', '>':'&gt;'}
55 re_html_escaping = re.compile('|'.join(map(re.escape, html_escaping.keys())))
56
57 default_config = "/etc/dak/dak.conf"
58 default_apt_config = "/etc/dak/apt.conf"
59
60 alias_cache = None
61 key_uid_email_cache = {}
62
63 # (hashname, function, earliest_changes_version)
64 known_hashes = [("sha1", apt_pkg.sha1sum, (1, 8)),
65                 ("sha256", apt_pkg.sha256sum, (1, 8))]
66
67 ################################################################################
68
69 def html_escape(s):
70     return re_html_escaping.sub(lambda x: html_escaping.get(x.group(0)), s)
71
72 ################################################################################
73
74 def open_file(filename, mode='r'):
75     try:
76         f = open(filename, mode)
77     except IOError:
78         raise CantOpenError, filename
79     return f
80
81 ################################################################################
82
83 def our_raw_input(prompt=""):
84     if prompt:
85         sys.stdout.write(prompt)
86     sys.stdout.flush()
87     try:
88         ret = raw_input()
89         return ret
90     except EOFError:
91         sys.stderr.write("\nUser interrupt (^D).\n")
92         raise SystemExit
93
94 ################################################################################
95
96 def extract_component_from_section(section):
97     component = ""
98
99     if section.find('/') != -1:
100         component = section.split('/')[0]
101
102     # Expand default component
103     if component == "":
104         if Cnf.has_key("Component::%s" % section):
105             component = section
106         else:
107             component = "main"
108
109     return (section, component)
110
111 ################################################################################
112
113 def parse_deb822(contents, signing_rules=0):
114     error = ""
115     changes = {}
116
117     # Split the lines in the input, keeping the linebreaks.
118     lines = contents.splitlines(True)
119
120     if len(lines) == 0:
121         raise ParseChangesError, "[Empty changes file]"
122
123     # Reindex by line number so we can easily verify the format of
124     # .dsc files...
125     index = 0
126     indexed_lines = {}
127     for line in lines:
128         index += 1
129         indexed_lines[index] = line[:-1]
130
131     inside_signature = 0
132
133     num_of_lines = len(indexed_lines.keys())
134     index = 0
135     first = -1
136     while index < num_of_lines:
137         index += 1
138         line = indexed_lines[index]
139         if line == "":
140             if signing_rules == 1:
141                 index += 1
142                 if index > num_of_lines:
143                     raise InvalidDscError, index
144                 line = indexed_lines[index]
145                 if not line.startswith("-----BEGIN PGP SIGNATURE"):
146                     raise InvalidDscError, index
147                 inside_signature = 0
148                 break
149             else:
150                 continue
151         if line.startswith("-----BEGIN PGP SIGNATURE"):
152             break
153         if line.startswith("-----BEGIN PGP SIGNED MESSAGE"):
154             inside_signature = 1
155             if signing_rules == 1:
156                 while index < num_of_lines and line != "":
157                     index += 1
158                     line = indexed_lines[index]
159             continue
160         # If we're not inside the signed data, don't process anything
161         if signing_rules >= 0 and not inside_signature:
162             continue
163         slf = re_single_line_field.match(line)
164         if slf:
165             field = slf.groups()[0].lower()
166             changes[field] = slf.groups()[1]
167             first = 1
168             continue
169         if line == " .":
170             changes[field] += '\n'
171             continue
172         mlf = re_multi_line_field.match(line)
173         if mlf:
174             if first == -1:
175                 raise ParseChangesError, "'%s'\n [Multi-line field continuing on from nothing?]" % (line)
176             if first == 1 and changes[field] != "":
177                 changes[field] += '\n'
178             first = 0
179             changes[field] += mlf.groups()[0] + '\n'
180             continue
181         error += line
182
183     if signing_rules == 1 and inside_signature:
184         raise InvalidDscError, index
185
186     changes["filecontents"] = "".join(lines)
187
188     if changes.has_key("source"):
189         # Strip the source version in brackets from the source field,
190         # put it in the "source-version" field instead.
191         srcver = re_srchasver.search(changes["source"])
192         if srcver:
193             changes["source"] = srcver.group(1)
194             changes["source-version"] = srcver.group(2)
195
196     if error:
197         raise ParseChangesError, error
198
199     return changes
200
201 ################################################################################
202
203 def parse_changes(filename, signing_rules=0):
204     """Parses a changes file and returns a dictionary where each field is a
205 key.  The mandatory first argument is the filename of the .changes
206 file.
207
208 signing_rules is an optional argument:
209
210  o If signing_rules == -1, no signature is required.
211  o If signing_rules == 0 (the default), a signature is required.
212  o If signing_rules == 1, it turns on the same strict format checking
213    as dpkg-source.
214
215 The rules for (signing_rules == 1)-mode are:
216
217   o The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
218     followed by any PGP header data and must end with a blank line.
219
220   o The data section must end with a blank line and must be followed by
221     "-----BEGIN PGP SIGNATURE-----".
222 """
223
224     changes_in = open_file(filename)
225     content = changes_in.read()
226     changes_in.close()
227     return parse_deb822(content, signing_rules)
228
229 ################################################################################
230
231 def hash_key(hashname):
232     return '%ssum' % hashname
233
234 ################################################################################
235
236 def create_hash(where, files, hashname, hashfunc):
237     """create_hash extends the passed files dict with the given hash by
238     iterating over all files on disk and passing them to the hashing
239     function given."""
240
241     rejmsg = []
242     for f in files.keys():
243         try:
244             file_handle = open_file(f)
245         except CantOpenError:
246             rejmsg.append("Could not open file %s for checksumming" % (f))
247
248         files[f][hash_key(hashname)] = hashfunc(file_handle)
249
250         file_handle.close()
251     return rejmsg
252
253 ################################################################################
254
255 def check_hash(where, files, hashname, hashfunc):
256     """check_hash checks the given hash in the files dict against the actual
257     files on disk.  The hash values need to be present consistently in
258     all file entries.  It does not modify its input in any way."""
259
260     rejmsg = []
261     for f in files.keys():
262         file_handle = None
263         try:
264             try:
265                 file_handle = open_file(f)
266     
267                 # Check for the hash entry, to not trigger a KeyError.
268                 if not files[f].has_key(hash_key(hashname)):
269                     rejmsg.append("%s: misses %s checksum in %s" % (f, hashname,
270                         where))
271                     continue
272     
273                 # Actually check the hash for correctness.
274                 if hashfunc(file_handle) != files[f][hash_key(hashname)]:
275                     rejmsg.append("%s: %s check failed in %s" % (f, hashname,
276                         where))
277             except CantOpenError:
278                 # TODO: This happens when the file is in the pool.
279                 # warn("Cannot open file %s" % f)
280                 continue
281         finally:
282             if file_handle:
283                 file_handle.close()
284     return rejmsg
285
286 ################################################################################
287
288 def check_size(where, files):
289     """check_size checks the file sizes in the passed files dict against the
290     files on disk."""
291
292     rejmsg = []
293     for f in files.keys():
294         try:
295             entry = os.stat(f)
296         except OSError, exc:
297             if exc.errno == 2:
298                 # TODO: This happens when the file is in the pool.
299                 continue
300             raise
301
302         actual_size = entry[stat.ST_SIZE]
303         size = int(files[f]["size"])
304         if size != actual_size:
305             rejmsg.append("%s: actual file size (%s) does not match size (%s) in %s"
306                    % (f, actual_size, size, where))
307     return rejmsg
308
309 ################################################################################
310
311 def check_hash_fields(what, manifest):
312     """check_hash_fields ensures that there are no checksum fields in the
313     given dict that we do not know about."""
314
315     rejmsg = []
316     hashes = map(lambda x: x[0], known_hashes)
317     for field in manifest:
318         if field.startswith("checksums-"):
319             hashname = field.split("-",1)[1]
320             if hashname not in hashes:
321                 rejmsg.append("Unsupported checksum field for %s "\
322                     "in %s" % (hashname, what))
323     return rejmsg
324
325 ################################################################################
326
327 def _ensure_changes_hash(changes, format, version, files, hashname, hashfunc):
328     if format >= version:
329         # The version should contain the specified hash.
330         func = check_hash
331
332         # Import hashes from the changes
333         rejmsg = parse_checksums(".changes", files, changes, hashname)
334         if len(rejmsg) > 0:
335             return rejmsg
336     else:
337         # We need to calculate the hash because it can't possibly
338         # be in the file.
339         func = create_hash
340     return func(".changes", files, hashname, hashfunc)
341
342 # We could add the orig which might be in the pool to the files dict to
343 # access the checksums easily.
344
345 def _ensure_dsc_hash(dsc, dsc_files, hashname, hashfunc):
346     """ensure_dsc_hashes' task is to ensure that each and every *present* hash
347     in the dsc is correct, i.e. identical to the changes file and if necessary
348     the pool.  The latter task is delegated to check_hash."""
349
350     rejmsg = []
351     if not dsc.has_key('Checksums-%s' % (hashname,)):
352         return rejmsg
353     # Import hashes from the dsc
354     parse_checksums(".dsc", dsc_files, dsc, hashname)
355     # And check it...
356     rejmsg.extend(check_hash(".dsc", dsc_files, hashname, hashfunc))
357     return rejmsg
358
359 ################################################################################
360
361 def ensure_hashes(changes, dsc, files, dsc_files):
362     rejmsg = []
363
364     # Make sure we recognise the format of the Files: field in the .changes
365     format = changes.get("format", "0.0").split(".", 1)
366     if len(format) == 2:
367         format = int(format[0]), int(format[1])
368     else:
369         format = int(float(format[0])), 0
370
371     # We need to deal with the original changes blob, as the fields we need
372     # might not be in the changes dict serialised into the .dak anymore.
373     orig_changes = parse_deb822(changes['filecontents'])
374
375     # Copy the checksums over to the current changes dict.  This will keep
376     # the existing modifications to it intact.
377     for field in orig_changes:
378         if field.startswith('checksums-'):
379             changes[field] = orig_changes[field]
380
381     # Check for unsupported hashes
382     rejmsg.extend(check_hash_fields(".changes", changes))
383     rejmsg.extend(check_hash_fields(".dsc", dsc))
384
385     # We have to calculate the hash if we have an earlier changes version than
386     # the hash appears in rather than require it exist in the changes file
387     for hashname, hashfunc, version in known_hashes:
388         rejmsg.extend(_ensure_changes_hash(changes, format, version, files,
389             hashname, hashfunc))
390         if "source" in changes["architecture"]:
391             rejmsg.extend(_ensure_dsc_hash(dsc, dsc_files, hashname,
392                 hashfunc))
393
394     return rejmsg
395
396 def parse_checksums(where, files, manifest, hashname):
397     rejmsg = []
398     field = 'checksums-%s' % hashname
399     if not field in manifest:
400         return rejmsg
401     for line in manifest[field].split('\n'):
402         if not line:
403             break
404         checksum, size, checkfile = line.strip().split(' ')
405         if not files.has_key(checkfile):
406         # TODO: check for the file's entry in the original files dict, not
407         # the one modified by (auto)byhand and other weird stuff
408         #    rejmsg.append("%s: not present in files but in checksums-%s in %s" %
409         #        (file, hashname, where))
410             continue
411         if not files[checkfile]["size"] == size:
412             rejmsg.append("%s: size differs for files and checksums-%s entry "\
413                 "in %s" % (checkfile, hashname, where))
414             continue
415         files[checkfile][hash_key(hashname)] = checksum
416     for f in files.keys():
417         if not files[f].has_key(hash_key(hashname)):
418             rejmsg.append("%s: no entry in checksums-%s in %s" % (checkfile,
419                 hashname, where))
420     return rejmsg
421
422 ################################################################################
423
424 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
425
426 def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
427     files = {}
428
429     # Make sure we have a Files: field to parse...
430     if not changes.has_key(field):
431         raise NoFilesFieldError
432
433     # Make sure we recognise the format of the Files: field
434     format = re_verwithext.search(changes.get("format", "0.0"))
435     if not format:
436         raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
437
438     format = format.groups()
439     if format[1] == None:
440         format = int(float(format[0])), 0, format[2]
441     else:
442         format = int(format[0]), int(format[1]), format[2]
443     if format[2] == None:
444         format = format[:2]
445
446     if is_a_dsc:
447         # format = (1,0) are the only formats we currently accept,
448         # format = (0,0) are missing format headers of which we still
449         # have some in the archive.
450         if format != (1,0) and format != (0,0):
451             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
452     else:
453         if (format < (1,5) or format > (1,8)):
454             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
455         if field != "files" and format < (1,8):
456             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
457
458     includes_section = (not is_a_dsc) and field == "files"
459
460     # Parse each entry/line:
461     for i in changes[field].split('\n'):
462         if not i:
463             break
464         s = i.split()
465         section = priority = ""
466         try:
467             if includes_section:
468                 (md5, size, section, priority, name) = s
469             else:
470                 (md5, size, name) = s
471         except ValueError:
472             raise ParseChangesError, i
473
474         if section == "":
475             section = "-"
476         if priority == "":
477             priority = "-"
478
479         (section, component) = extract_component_from_section(section)
480
481         files[name] = Dict(size=size, section=section,
482                            priority=priority, component=component)
483         files[name][hashname] = md5
484
485     return files
486
487 ################################################################################
488
489 def force_to_utf8(s):
490     """Forces a string to UTF-8.  If the string isn't already UTF-8,
491 it's assumed to be ISO-8859-1."""
492     try:
493         unicode(s, 'utf-8')
494         return s
495     except UnicodeError:
496         latin1_s = unicode(s,'iso8859-1')
497         return latin1_s.encode('utf-8')
498
499 def rfc2047_encode(s):
500     """Encodes a (header) string per RFC2047 if necessary.  If the
501 string is neither ASCII nor UTF-8, it's assumed to be ISO-8859-1."""
502     try:
503         codecs.lookup('ascii')[1](s)
504         return s
505     except UnicodeError:
506         pass
507     try:
508         codecs.lookup('utf-8')[1](s)
509         h = email.Header.Header(s, 'utf-8', 998)
510         return str(h)
511     except UnicodeError:
512         h = email.Header.Header(s, 'iso-8859-1', 998)
513         return str(h)
514
515 ################################################################################
516
517 # <Culus> 'The standard sucks, but my tool is supposed to interoperate
518 #          with it. I know - I'll fix the suckage and make things
519 #          incompatible!'
520
521 def fix_maintainer (maintainer):
522     """Parses a Maintainer or Changed-By field and returns:
523   (1) an RFC822 compatible version,
524   (2) an RFC2047 compatible version,
525   (3) the name
526   (4) the email
527
528 The name is forced to UTF-8 for both (1) and (3).  If the name field
529 contains '.' or ',' (as allowed by Debian policy), (1) and (2) are
530 switched to 'email (name)' format."""
531     maintainer = maintainer.strip()
532     if not maintainer:
533         return ('', '', '', '')
534
535     if maintainer.find("<") == -1:
536         email = maintainer
537         name = ""
538     elif (maintainer[0] == "<" and maintainer[-1:] == ">"):
539         email = maintainer[1:-1]
540         name = ""
541     else:
542         m = re_parse_maintainer.match(maintainer)
543         if not m:
544             raise ParseMaintError, "Doesn't parse as a valid Maintainer field."
545         name = m.group(1)
546         email = m.group(2)
547
548     # Get an RFC2047 compliant version of the name
549     rfc2047_name = rfc2047_encode(name)
550
551     # Force the name to be UTF-8
552     name = force_to_utf8(name)
553
554     if name.find(',') != -1 or name.find('.') != -1:
555         rfc822_maint = "%s (%s)" % (email, name)
556         rfc2047_maint = "%s (%s)" % (email, rfc2047_name)
557     else:
558         rfc822_maint = "%s <%s>" % (name, email)
559         rfc2047_maint = "%s <%s>" % (rfc2047_name, email)
560
561     if email.find("@") == -1 and email.find("buildd_") != 0:
562         raise ParseMaintError, "No @ found in email address part."
563
564     return (rfc822_maint, rfc2047_maint, name, email)
565
566 ################################################################################
567
568 # sendmail wrapper, takes _either_ a message string or a file as arguments
569 def send_mail (message, filename=""):
570         # If we've been passed a string dump it into a temporary file
571     if message:
572         filename = tempfile.mktemp()
573         fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700)
574         os.write (fd, message)
575         os.close (fd)
576
577     # Invoke sendmail
578     (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
579     if (result != 0):
580         raise SendmailFailedError, output
581
582     # Clean up any temporary files
583     if message:
584         os.unlink (filename)
585
586 ################################################################################
587
588 def poolify (source, component):
589     if component:
590         component += '/'
591     if source[:3] == "lib":
592         return component + source[:4] + '/' + source + '/'
593     else:
594         return component + source[:1] + '/' + source + '/'
595
596 ################################################################################
597
598 def move (src, dest, overwrite = 0, perms = 0664):
599     if os.path.exists(dest) and os.path.isdir(dest):
600         dest_dir = dest
601     else:
602         dest_dir = os.path.dirname(dest)
603     if not os.path.exists(dest_dir):
604         umask = os.umask(00000)
605         os.makedirs(dest_dir, 02775)
606         os.umask(umask)
607     #print "Moving %s to %s..." % (src, dest)
608     if os.path.exists(dest) and os.path.isdir(dest):
609         dest += '/' + os.path.basename(src)
610     # Don't overwrite unless forced to
611     if os.path.exists(dest):
612         if not overwrite:
613             fubar("Can't move %s to %s - file already exists." % (src, dest))
614         else:
615             if not os.access(dest, os.W_OK):
616                 fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
617     shutil.copy2(src, dest)
618     os.chmod(dest, perms)
619     os.unlink(src)
620
621 def copy (src, dest, overwrite = 0, perms = 0664):
622     if os.path.exists(dest) and os.path.isdir(dest):
623         dest_dir = dest
624     else:
625         dest_dir = os.path.dirname(dest)
626     if not os.path.exists(dest_dir):
627         umask = os.umask(00000)
628         os.makedirs(dest_dir, 02775)
629         os.umask(umask)
630     #print "Copying %s to %s..." % (src, dest)
631     if os.path.exists(dest) and os.path.isdir(dest):
632         dest += '/' + os.path.basename(src)
633     # Don't overwrite unless forced to
634     if os.path.exists(dest):
635         if not overwrite:
636             raise FileExistsError
637         else:
638             if not os.access(dest, os.W_OK):
639                 raise CantOverwriteError
640     shutil.copy2(src, dest)
641     os.chmod(dest, perms)
642
643 ################################################################################
644
645 def where_am_i ():
646     res = socket.gethostbyaddr(socket.gethostname())
647     database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname")
648     if database_hostname:
649         return database_hostname
650     else:
651         return res[0]
652
653 def which_conf_file ():
654     res = socket.gethostbyaddr(socket.gethostname())
655     if Cnf.get("Config::" + res[0] + "::DakConfig"):
656         return Cnf["Config::" + res[0] + "::DakConfig"]
657     else:
658         return default_config
659
660 def which_apt_conf_file ():
661     res = socket.gethostbyaddr(socket.gethostname())
662     if Cnf.get("Config::" + res[0] + "::AptConfig"):
663         return Cnf["Config::" + res[0] + "::AptConfig"]
664     else:
665         return default_apt_config
666
667 def which_alias_file():
668     hostname = socket.gethostbyaddr(socket.gethostname())[0]
669     aliasfn = '/var/lib/misc/'+hostname+'/forward-alias'
670     if os.path.exists(aliasfn):
671         return aliasfn
672     else:
673         return None
674
675 ################################################################################
676
677 # Escape characters which have meaning to SQL's regex comparison operator ('~')
678 # (woefully incomplete)
679
680 def regex_safe (s):
681     s = s.replace('+', '\\\\+')
682     s = s.replace('.', '\\\\.')
683     return s
684
685 ################################################################################
686
687 # Perform a substition of template
688 def TemplateSubst(map, filename):
689     file = open_file(filename)
690     template = file.read()
691     for x in map.keys():
692         template = template.replace(x,map[x])
693     file.close()
694     return template
695
696 ################################################################################
697
698 def fubar(msg, exit_code=1):
699     sys.stderr.write("E: %s\n" % (msg))
700     sys.exit(exit_code)
701
702 def warn(msg):
703     sys.stderr.write("W: %s\n" % (msg))
704
705 ################################################################################
706
707 # Returns the user name with a laughable attempt at rfc822 conformancy
708 # (read: removing stray periods).
709 def whoami ():
710     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
711
712 ################################################################################
713
714 def size_type (c):
715     t  = " B"
716     if c > 10240:
717         c = c / 1024
718         t = " KB"
719     if c > 10240:
720         c = c / 1024
721         t = " MB"
722     return ("%d%s" % (c, t))
723
724 ################################################################################
725
726 def cc_fix_changes (changes):
727     o = changes.get("architecture", "")
728     if o:
729         del changes["architecture"]
730     changes["architecture"] = {}
731     for j in o.split():
732         changes["architecture"][j] = 1
733
734 # Sort by source name, source version, 'have source', and then by filename
735 def changes_compare (a, b):
736     try:
737         a_changes = parse_changes(a)
738     except:
739         return -1
740
741     try:
742         b_changes = parse_changes(b)
743     except:
744         return 1
745
746     cc_fix_changes (a_changes)
747     cc_fix_changes (b_changes)
748
749     # Sort by source name
750     a_source = a_changes.get("source")
751     b_source = b_changes.get("source")
752     q = cmp (a_source, b_source)
753     if q:
754         return q
755
756     # Sort by source version
757     a_version = a_changes.get("version", "0")
758     b_version = b_changes.get("version", "0")
759     q = apt_pkg.VersionCompare(a_version, b_version)
760     if q:
761         return q
762
763     # Sort by 'have source'
764     a_has_source = a_changes["architecture"].get("source")
765     b_has_source = b_changes["architecture"].get("source")
766     if a_has_source and not b_has_source:
767         return -1
768     elif b_has_source and not a_has_source:
769         return 1
770
771     # Fall back to sort by filename
772     return cmp(a, b)
773
774 ################################################################################
775
776 def find_next_free (dest, too_many=100):
777     extra = 0
778     orig_dest = dest
779     while os.path.exists(dest) and extra < too_many:
780         dest = orig_dest + '.' + repr(extra)
781         extra += 1
782     if extra >= too_many:
783         raise NoFreeFilenameError
784     return dest
785
786 ################################################################################
787
788 def result_join (original, sep = '\t'):
789     list = []
790     for i in xrange(len(original)):
791         if original[i] == None:
792             list.append("")
793         else:
794             list.append(original[i])
795     return sep.join(list)
796
797 ################################################################################
798
799 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
800     out = ""
801     for line in str.split('\n'):
802         line = line.strip()
803         if line or include_blank_lines:
804             out += "%s%s\n" % (prefix, line)
805     # Strip trailing new line
806     if out:
807         out = out[:-1]
808     return out
809
810 ################################################################################
811
812 def validate_changes_file_arg(filename, require_changes=1):
813     """'filename' is either a .changes or .dak file.  If 'filename' is a
814 .dak file, it's changed to be the corresponding .changes file.  The
815 function then checks if the .changes file a) exists and b) is
816 readable and returns the .changes filename if so.  If there's a
817 problem, the next action depends on the option 'require_changes'
818 argument:
819
820  o If 'require_changes' == -1, errors are ignored and the .changes
821                                filename is returned.
822  o If 'require_changes' == 0, a warning is given and 'None' is returned.
823  o If 'require_changes' == 1, a fatal error is raised.
824 """
825     error = None
826
827     orig_filename = filename
828     if filename.endswith(".dak"):
829         filename = filename[:-4]+".changes"
830
831     if not filename.endswith(".changes"):
832         error = "invalid file type; not a changes file"
833     else:
834         if not os.access(filename,os.R_OK):
835             if os.path.exists(filename):
836                 error = "permission denied"
837             else:
838                 error = "file not found"
839
840     if error:
841         if require_changes == 1:
842             fubar("%s: %s." % (orig_filename, error))
843         elif require_changes == 0:
844             warn("Skipping %s - %s" % (orig_filename, error))
845             return None
846         else: # We only care about the .dak file
847             return filename
848     else:
849         return filename
850
851 ################################################################################
852
853 def real_arch(arch):
854     return (arch != "source" and arch != "all")
855
856 ################################################################################
857
858 def join_with_commas_and(list):
859     if len(list) == 0: return "nothing"
860     if len(list) == 1: return list[0]
861     return ", ".join(list[:-1]) + " and " + list[-1]
862
863 ################################################################################
864
865 def pp_deps (deps):
866     pp_deps = []
867     for atom in deps:
868         (pkg, version, constraint) = atom
869         if constraint:
870             pp_dep = "%s (%s %s)" % (pkg, constraint, version)
871         else:
872             pp_dep = pkg
873         pp_deps.append(pp_dep)
874     return " |".join(pp_deps)
875
876 ################################################################################
877
878 def get_conf():
879     return Cnf
880
881 ################################################################################
882
883 # Handle -a, -c and -s arguments; returns them as SQL constraints
884 def parse_args(Options):
885     # Process suite
886     if Options["Suite"]:
887         suite_ids_list = []
888         for suite in split_args(Options["Suite"]):
889             suite_id = database.get_suite_id(suite)
890             if suite_id == -1:
891                 warn("suite '%s' not recognised." % (suite))
892             else:
893                 suite_ids_list.append(suite_id)
894         if suite_ids_list:
895             con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
896         else:
897             fubar("No valid suite given.")
898     else:
899         con_suites = ""
900
901     # Process component
902     if Options["Component"]:
903         component_ids_list = []
904         for component in split_args(Options["Component"]):
905             component_id = database.get_component_id(component)
906             if component_id == -1:
907                 warn("component '%s' not recognised." % (component))
908             else:
909                 component_ids_list.append(component_id)
910         if component_ids_list:
911             con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
912         else:
913             fubar("No valid component given.")
914     else:
915         con_components = ""
916
917     # Process architecture
918     con_architectures = ""
919     if Options["Architecture"]:
920         arch_ids_list = []
921         check_source = 0
922         for architecture in split_args(Options["Architecture"]):
923             if architecture == "source":
924                 check_source = 1
925             else:
926                 architecture_id = database.get_architecture_id(architecture)
927                 if architecture_id == -1:
928                     warn("architecture '%s' not recognised." % (architecture))
929                 else:
930                     arch_ids_list.append(architecture_id)
931         if arch_ids_list:
932             con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
933         else:
934             if not check_source:
935                 fubar("No valid architecture given.")
936     else:
937         check_source = 1
938
939     return (con_suites, con_architectures, con_components, check_source)
940
941 ################################################################################
942
943 # Inspired(tm) by Bryn Keller's print_exc_plus (See
944 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)
945
946 def print_exc():
947     tb = sys.exc_info()[2]
948     while tb.tb_next:
949         tb = tb.tb_next
950     stack = []
951     frame = tb.tb_frame
952     while frame:
953         stack.append(frame)
954         frame = frame.f_back
955     stack.reverse()
956     traceback.print_exc()
957     for frame in stack:
958         print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
959                                              frame.f_code.co_filename,
960                                              frame.f_lineno)
961         for key, value in frame.f_locals.items():
962             print "\t%20s = " % key,
963             try:
964                 print value
965             except:
966                 print "<unable to print>"
967
968 ################################################################################
969
970 def try_with_debug(function):
971     try:
972         function()
973     except SystemExit:
974         raise
975     except:
976         print_exc()
977
978 ################################################################################
979
980 # Function for use in sorting lists of architectures.
981 # Sorts normally except that 'source' dominates all others.
982
983 def arch_compare_sw (a, b):
984     if a == "source" and b == "source":
985         return 0
986     elif a == "source":
987         return -1
988     elif b == "source":
989         return 1
990
991     return cmp (a, b)
992
993 ################################################################################
994
995 # Split command line arguments which can be separated by either commas
996 # or whitespace.  If dwim is set, it will complain about string ending
997 # in comma since this usually means someone did 'dak ls -a i386, m68k
998 # foo' or something and the inevitable confusion resulting from 'm68k'
999 # being treated as an argument is undesirable.
1000
1001 def split_args (s, dwim=1):
1002     if s.find(",") == -1:
1003         return s.split()
1004     else:
1005         if s[-1:] == "," and dwim:
1006             fubar("split_args: found trailing comma, spurious space maybe?")
1007         return s.split(",")
1008
1009 ################################################################################
1010
1011 def Dict(**dict): return dict
1012
1013 ########################################
1014
1015 # Our very own version of commands.getouputstatus(), hacked to support
1016 # gpgv's status fd.
1017 def gpgv_get_status_output(cmd, status_read, status_write):
1018     cmd = ['/bin/sh', '-c', cmd]
1019     p2cread, p2cwrite = os.pipe()
1020     c2pread, c2pwrite = os.pipe()
1021     errout, errin = os.pipe()
1022     pid = os.fork()
1023     if pid == 0:
1024         # Child
1025         os.close(0)
1026         os.close(1)
1027         os.dup(p2cread)
1028         os.dup(c2pwrite)
1029         os.close(2)
1030         os.dup(errin)
1031         for i in range(3, 256):
1032             if i != status_write:
1033                 try:
1034                     os.close(i)
1035                 except:
1036                     pass
1037         try:
1038             os.execvp(cmd[0], cmd)
1039         finally:
1040             os._exit(1)
1041
1042     # Parent
1043     os.close(p2cread)
1044     os.dup2(c2pread, c2pwrite)
1045     os.dup2(errout, errin)
1046
1047     output = status = ""
1048     while 1:
1049         i, o, e = select.select([c2pwrite, errin, status_read], [], [])
1050         more_data = []
1051         for fd in i:
1052             r = os.read(fd, 8196)
1053             if len(r) > 0:
1054                 more_data.append(fd)
1055                 if fd == c2pwrite or fd == errin:
1056                     output += r
1057                 elif fd == status_read:
1058                     status += r
1059                 else:
1060                     fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
1061         if not more_data:
1062             pid, exit_status = os.waitpid(pid, 0)
1063             try:
1064                 os.close(status_write)
1065                 os.close(status_read)
1066                 os.close(c2pread)
1067                 os.close(c2pwrite)
1068                 os.close(p2cwrite)
1069                 os.close(errin)
1070                 os.close(errout)
1071             except:
1072                 pass
1073             break
1074
1075     return output, status, exit_status
1076
1077 ################################################################################
1078
1079 def process_gpgv_output(status):
1080     # Process the status-fd output
1081     keywords = {}
1082     internal_error = ""
1083     for line in status.split('\n'):
1084         line = line.strip()
1085         if line == "":
1086             continue
1087         split = line.split()
1088         if len(split) < 2:
1089             internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
1090             continue
1091         (gnupg, keyword) = split[:2]
1092         if gnupg != "[GNUPG:]":
1093             internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
1094             continue
1095         args = split[2:]
1096         if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
1097             internal_error += "found duplicate status token ('%s').\n" % (keyword)
1098             continue
1099         else:
1100             keywords[keyword] = args
1101
1102     return (keywords, internal_error)
1103
1104 ################################################################################
1105
1106 def retrieve_key (filename, keyserver=None, keyring=None):
1107     """Retrieve the key that signed 'filename' from 'keyserver' and
1108 add it to 'keyring'.  Returns nothing on success, or an error message
1109 on error."""
1110
1111     # Defaults for keyserver and keyring
1112     if not keyserver:
1113         keyserver = Cnf["Dinstall::KeyServer"]
1114     if not keyring:
1115         keyring = Cnf.ValueList("Dinstall::GPGKeyring")[0]
1116
1117     # Ensure the filename contains no shell meta-characters or other badness
1118     if not re_taint_free.match(filename):
1119         return "%s: tainted filename" % (filename)
1120
1121     # Invoke gpgv on the file
1122     status_read, status_write = os.pipe();
1123     cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
1124     (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
1125
1126     # Process the status-fd output
1127     (keywords, internal_error) = process_gpgv_output(status)
1128     if internal_error:
1129         return internal_error
1130
1131     if not keywords.has_key("NO_PUBKEY"):
1132         return "didn't find expected NO_PUBKEY in gpgv status-fd output"
1133
1134     fingerprint = keywords["NO_PUBKEY"][0]
1135     # XXX - gpg sucks.  You can't use --secret-keyring=/dev/null as
1136     # it'll try to create a lockfile in /dev.  A better solution might
1137     # be a tempfile or something.
1138     cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
1139           % (Cnf["Dinstall::SigningKeyring"])
1140     cmd += " --keyring %s --keyserver %s --recv-key %s" \
1141            % (keyring, keyserver, fingerprint)
1142     (result, output) = commands.getstatusoutput(cmd)
1143     if (result != 0):
1144         return "'%s' failed with exit code %s" % (cmd, result)
1145
1146     return ""
1147
1148 ################################################################################
1149
1150 def gpg_keyring_args(keyrings=None):
1151     if not keyrings:
1152         keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1153
1154     return " ".join(["--keyring %s" % x for x in keyrings])
1155
1156 ################################################################################
1157
1158 def check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
1159     """Check the signature of a file and return the fingerprint if the
1160 signature is valid or 'None' if it's not.  The first argument is the
1161 filename whose signature should be checked.  The second argument is a
1162 reject function and is called when an error is found.  The reject()
1163 function must allow for two arguments: the first is the error message,
1164 the second is an optional prefix string.  It's possible for reject()
1165 to be called more than once during an invocation of check_signature().
1166 The third argument is optional and is the name of the files the
1167 detached signature applies to.  The fourth argument is optional and is
1168 a *list* of keyrings to use.  'autofetch' can either be None, True or
1169 False.  If None, the default behaviour specified in the config will be
1170 used."""
1171
1172     # Ensure the filename contains no shell meta-characters or other badness
1173     if not re_taint_free.match(sig_filename):
1174         reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
1175         return None
1176
1177     if data_filename and not re_taint_free.match(data_filename):
1178         reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
1179         return None
1180
1181     if not keyrings:
1182         keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1183
1184     # Autofetch the signing key if that's enabled
1185     if autofetch == None:
1186         autofetch = Cnf.get("Dinstall::KeyAutoFetch")
1187     if autofetch:
1188         error_msg = retrieve_key(sig_filename)
1189         if error_msg:
1190             reject(error_msg)
1191             return None
1192
1193     # Build the command line
1194     status_read, status_write = os.pipe();
1195     cmd = "gpgv --status-fd %s %s %s %s" % (
1196         status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)
1197
1198     # Invoke gpgv on the file
1199     (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
1200
1201     # Process the status-fd output
1202     (keywords, internal_error) = process_gpgv_output(status)
1203
1204     # If we failed to parse the status-fd output, let's just whine and bail now
1205     if internal_error:
1206         reject("internal error while performing signature check on %s." % (sig_filename))
1207         reject(internal_error, "")
1208         reject("Please report the above errors to the Archive maintainers by replying to this mail.", "")
1209         return None
1210
1211     bad = ""
1212     # Now check for obviously bad things in the processed output
1213     if keywords.has_key("KEYREVOKED"):
1214         reject("The key used to sign %s has been revoked." % (sig_filename))
1215         bad = 1
1216     if keywords.has_key("BADSIG"):
1217         reject("bad signature on %s." % (sig_filename))
1218         bad = 1
1219     if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1220         reject("failed to check signature on %s." % (sig_filename))
1221         bad = 1
1222     if keywords.has_key("NO_PUBKEY"):
1223         args = keywords["NO_PUBKEY"]
1224         if len(args) >= 1:
1225             key = args[0]
1226         reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
1227         bad = 1
1228     if keywords.has_key("BADARMOR"):
1229         reject("ASCII armour of signature was corrupt in %s." % (sig_filename))
1230         bad = 1
1231     if keywords.has_key("NODATA"):
1232         reject("no signature found in %s." % (sig_filename))
1233         bad = 1
1234     if keywords.has_key("EXPKEYSIG"):
1235         args = keywords["EXPKEYSIG"]
1236         if len(args) >= 1:
1237             key = args[0]
1238         reject("Signature made by expired key 0x%s" % (key))
1239         bad = 1
1240     if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1241         args = keywords["KEYEXPIRED"]
1242         expiredate=""
1243         if len(args) >= 1:
1244             timestamp = args[0]
1245             if timestamp.count("T") == 0:
1246                 expiredate = time.strftime("%Y-%m-%d", time.gmtime(timestamp))
1247             else:
1248                 expiredate = timestamp
1249         reject("The key used to sign %s has expired on %s" % (sig_filename, expiredate))
1250         bad = 1
1251
1252     if bad:
1253         return None
1254
1255     # Next check gpgv exited with a zero return code
1256     if exit_status:
1257         reject("gpgv failed while checking %s." % (sig_filename))
1258         if status.strip():
1259             reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1260         else:
1261             reject(prefix_multi_line_string(output, " [GPG output:] "), "")
1262         return None
1263
1264     # Sanity check the good stuff we expect
1265     if not keywords.has_key("VALIDSIG"):
1266         reject("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
1267         bad = 1
1268     else:
1269         args = keywords["VALIDSIG"]
1270         if len(args) < 1:
1271             reject("internal error while checking signature on %s." % (sig_filename))
1272             bad = 1
1273         else:
1274             fingerprint = args[0]
1275     if not keywords.has_key("GOODSIG"):
1276         reject("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
1277         bad = 1
1278     if not keywords.has_key("SIG_ID"):
1279         reject("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
1280         bad = 1
1281
1282     # Finally ensure there's not something we don't recognise
1283     known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
1284                           SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1285                           NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="")
1286
1287     for keyword in keywords.keys():
1288         if not known_keywords.has_key(keyword):
1289             reject("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
1290             bad = 1
1291
1292     if bad:
1293         return None
1294     else:
1295         return fingerprint
1296
1297 ################################################################################
1298
1299 def gpg_get_key_addresses(fingerprint):
1300     """retreive email addresses from gpg key uids for a given fingerprint"""
1301     addresses = key_uid_email_cache.get(fingerprint)
1302     if addresses != None:
1303         return addresses
1304     addresses = set()
1305     cmd = "gpg --no-default-keyring %s --fingerprint %s" \
1306                 % (gpg_keyring_args(), fingerprint)
1307     (result, output) = commands.getstatusoutput(cmd)
1308     if result == 0:
1309         for l in output.split('\n'):
1310             m = re_gpg_uid.match(l)
1311             if m:
1312                 addresses.add(m.group(1))
1313     key_uid_email_cache[fingerprint] = addresses
1314     return addresses
1315
1316 ################################################################################
1317
1318 # Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603
1319
1320 def wrap(paragraph, max_length, prefix=""):
1321     line = ""
1322     s = ""
1323     have_started = 0
1324     words = paragraph.split()
1325
1326     for word in words:
1327         word_size = len(word)
1328         if word_size > max_length:
1329             if have_started:
1330                 s += line + '\n' + prefix
1331             s += word + '\n' + prefix
1332         else:
1333             if have_started:
1334                 new_length = len(line) + word_size + 1
1335                 if new_length > max_length:
1336                     s += line + '\n' + prefix
1337                     line = word
1338                 else:
1339                     line += ' ' + word
1340             else:
1341                 line = word
1342         have_started = 1
1343
1344     if have_started:
1345         s += line
1346
1347     return s
1348
1349 ################################################################################
1350
1351 # Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
1352 # Returns fixed 'src'
1353 def clean_symlink (src, dest, root):
1354     src = src.replace(root, '', 1)
1355     dest = dest.replace(root, '', 1)
1356     dest = os.path.dirname(dest)
1357     new_src = '../' * len(dest.split('/'))
1358     return new_src + src
1359
1360 ################################################################################
1361
1362 def temp_filename(directory=None, dotprefix=None, perms=0700):
1363     """Return a secure and unique filename by pre-creating it.
1364 If 'directory' is non-null, it will be the directory the file is pre-created in.
1365 If 'dotprefix' is non-null, the filename will be prefixed with a '.'."""
1366
1367     if directory:
1368         old_tempdir = tempfile.tempdir
1369         tempfile.tempdir = directory
1370
1371     filename = tempfile.mktemp()
1372
1373     if dotprefix:
1374         filename = "%s/.%s" % (os.path.dirname(filename), os.path.basename(filename))
1375     fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, perms)
1376     os.close(fd)
1377
1378     if directory:
1379         tempfile.tempdir = old_tempdir
1380
1381     return filename
1382
1383 ################################################################################
1384
1385 # checks if the user part of the email is listed in the alias file
1386
1387 def is_email_alias(email):
1388     global alias_cache
1389     if alias_cache == None:
1390         aliasfn = which_alias_file()
1391         alias_cache = set()
1392         if aliasfn:
1393             for l in open(aliasfn):
1394                 alias_cache.add(l.split(':')[0])
1395     uid = email.split('@')[0]
1396     return uid in alias_cache
1397
1398 ################################################################################
1399
1400 apt_pkg.init()
1401
1402 Cnf = apt_pkg.newConfiguration()
1403 apt_pkg.ReadConfigFileISC(Cnf,default_config)
1404
1405 if which_conf_file() != default_config:
1406     apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1407
1408 ################################################################################