]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
52b902f9ffe84e298fce9fb4c127b935b5376f6a
[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     input = manifest[field]
402     for line in input.split('\n'):
403         if not line:
404             break
405         hash, size, file = line.strip().split(' ')
406         if not files.has_key(file):
407         # TODO: check for the file's entry in the original files dict, not
408         # the one modified by (auto)byhand and other weird stuff
409         #    rejmsg.append("%s: not present in files but in checksums-%s in %s" %
410         #        (file, hashname, where))
411             continue
412         if not files[file]["size"] == size:
413             rejmsg.append("%s: size differs for files and checksums-%s entry "\
414                 "in %s" % (file, hashname, where))
415             continue
416         files[file][hash_key(hashname)] = hash
417     for f in files.keys():
418         if not files[f].has_key(hash_key(hashname)):
419             rejmsg.append("%s: no entry in checksums-%s in %s" % (file,
420                 hashname, where))
421     return rejmsg
422
423 ################################################################################
424
425 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
426
427 def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
428     files = {}
429
430     # Make sure we have a Files: field to parse...
431     if not changes.has_key(field):
432         raise NoFilesFieldError
433
434     # Make sure we recognise the format of the Files: field
435     format = re_verwithext.search(changes.get("format", "0.0"))
436     if not format:
437         raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
438
439     format = format.groups()
440     if format[1] == None:
441         format = int(float(format[0])), 0, format[2]
442     else:
443         format = int(format[0]), int(format[1]), format[2]
444     if format[2] == None:
445         format = format[:2]
446
447     if is_a_dsc:
448         # format = (1,0) are the only formats we currently accept,
449         # format = (0,0) are missing format headers of which we still
450         # have some in the archive.
451         if format != (1,0) and format != (0,0):
452             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
453     else:
454         if (format < (1,5) or format > (1,8)):
455             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
456         if field != "files" and format < (1,8):
457             raise UnknownFormatError, "%s" % (changes.get("format","0.0"))
458
459     includes_section = (not is_a_dsc) and field == "files"
460
461     # Parse each entry/line:
462     for i in changes[field].split('\n'):
463         if not i:
464             break
465         s = i.split()
466         section = priority = ""
467         try:
468             if includes_section:
469                 (md5, size, section, priority, name) = s
470             else:
471                 (md5, size, name) = s
472         except ValueError:
473             raise ParseChangesError, i
474
475         if section == "":
476             section = "-"
477         if priority == "":
478             priority = "-"
479
480         (section, component) = extract_component_from_section(section)
481
482         files[name] = Dict(size=size, section=section,
483                            priority=priority, component=component)
484         files[name][hashname] = md5
485
486     return files
487
488 ################################################################################
489
490 def force_to_utf8(s):
491     """Forces a string to UTF-8.  If the string isn't already UTF-8,
492 it's assumed to be ISO-8859-1."""
493     try:
494         unicode(s, 'utf-8')
495         return s
496     except UnicodeError:
497         latin1_s = unicode(s,'iso8859-1')
498         return latin1_s.encode('utf-8')
499
500 def rfc2047_encode(s):
501     """Encodes a (header) string per RFC2047 if necessary.  If the
502 string is neither ASCII nor UTF-8, it's assumed to be ISO-8859-1."""
503     try:
504         codecs.lookup('ascii')[1](s)
505         return s
506     except UnicodeError:
507         pass
508     try:
509         codecs.lookup('utf-8')[1](s)
510         h = email.Header.Header(s, 'utf-8', 998)
511         return str(h)
512     except UnicodeError:
513         h = email.Header.Header(s, 'iso-8859-1', 998)
514         return str(h)
515
516 ################################################################################
517
518 # <Culus> 'The standard sucks, but my tool is supposed to interoperate
519 #          with it. I know - I'll fix the suckage and make things
520 #          incompatible!'
521
522 def fix_maintainer (maintainer):
523     """Parses a Maintainer or Changed-By field and returns:
524   (1) an RFC822 compatible version,
525   (2) an RFC2047 compatible version,
526   (3) the name
527   (4) the email
528
529 The name is forced to UTF-8 for both (1) and (3).  If the name field
530 contains '.' or ',' (as allowed by Debian policy), (1) and (2) are
531 switched to 'email (name)' format."""
532     maintainer = maintainer.strip()
533     if not maintainer:
534         return ('', '', '', '')
535
536     if maintainer.find("<") == -1:
537         email = maintainer
538         name = ""
539     elif (maintainer[0] == "<" and maintainer[-1:] == ">"):
540         email = maintainer[1:-1]
541         name = ""
542     else:
543         m = re_parse_maintainer.match(maintainer)
544         if not m:
545             raise ParseMaintError, "Doesn't parse as a valid Maintainer field."
546         name = m.group(1)
547         email = m.group(2)
548
549     # Get an RFC2047 compliant version of the name
550     rfc2047_name = rfc2047_encode(name)
551
552     # Force the name to be UTF-8
553     name = force_to_utf8(name)
554
555     if name.find(',') != -1 or name.find('.') != -1:
556         rfc822_maint = "%s (%s)" % (email, name)
557         rfc2047_maint = "%s (%s)" % (email, rfc2047_name)
558     else:
559         rfc822_maint = "%s <%s>" % (name, email)
560         rfc2047_maint = "%s <%s>" % (rfc2047_name, email)
561
562     if email.find("@") == -1 and email.find("buildd_") != 0:
563         raise ParseMaintError, "No @ found in email address part."
564
565     return (rfc822_maint, rfc2047_maint, name, email)
566
567 ################################################################################
568
569 # sendmail wrapper, takes _either_ a message string or a file as arguments
570 def send_mail (message, filename=""):
571         # If we've been passed a string dump it into a temporary file
572     if message:
573         filename = tempfile.mktemp()
574         fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700)
575         os.write (fd, message)
576         os.close (fd)
577
578     # Invoke sendmail
579     (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
580     if (result != 0):
581         raise SendmailFailedError, output
582
583     # Clean up any temporary files
584     if message:
585         os.unlink (filename)
586
587 ################################################################################
588
589 def poolify (source, component):
590     if component:
591         component += '/'
592     if source[:3] == "lib":
593         return component + source[:4] + '/' + source + '/'
594     else:
595         return component + source[:1] + '/' + source + '/'
596
597 ################################################################################
598
599 def move (src, dest, overwrite = 0, perms = 0664):
600     if os.path.exists(dest) and os.path.isdir(dest):
601         dest_dir = dest
602     else:
603         dest_dir = os.path.dirname(dest)
604     if not os.path.exists(dest_dir):
605         umask = os.umask(00000)
606         os.makedirs(dest_dir, 02775)
607         os.umask(umask)
608     #print "Moving %s to %s..." % (src, dest)
609     if os.path.exists(dest) and os.path.isdir(dest):
610         dest += '/' + os.path.basename(src)
611     # Don't overwrite unless forced to
612     if os.path.exists(dest):
613         if not overwrite:
614             fubar("Can't move %s to %s - file already exists." % (src, dest))
615         else:
616             if not os.access(dest, os.W_OK):
617                 fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
618     shutil.copy2(src, dest)
619     os.chmod(dest, perms)
620     os.unlink(src)
621
622 def copy (src, dest, overwrite = 0, perms = 0664):
623     if os.path.exists(dest) and os.path.isdir(dest):
624         dest_dir = dest
625     else:
626         dest_dir = os.path.dirname(dest)
627     if not os.path.exists(dest_dir):
628         umask = os.umask(00000)
629         os.makedirs(dest_dir, 02775)
630         os.umask(umask)
631     #print "Copying %s to %s..." % (src, dest)
632     if os.path.exists(dest) and os.path.isdir(dest):
633         dest += '/' + os.path.basename(src)
634     # Don't overwrite unless forced to
635     if os.path.exists(dest):
636         if not overwrite:
637             raise FileExistsError
638         else:
639             if not os.access(dest, os.W_OK):
640                 raise CantOverwriteError
641     shutil.copy2(src, dest)
642     os.chmod(dest, perms)
643
644 ################################################################################
645
646 def where_am_i ():
647     res = socket.gethostbyaddr(socket.gethostname())
648     database_hostname = Cnf.get("Config::" + res[0] + "::DatabaseHostname")
649     if database_hostname:
650         return database_hostname
651     else:
652         return res[0]
653
654 def which_conf_file ():
655     res = socket.gethostbyaddr(socket.gethostname())
656     if Cnf.get("Config::" + res[0] + "::DakConfig"):
657         return Cnf["Config::" + res[0] + "::DakConfig"]
658     else:
659         return default_config
660
661 def which_apt_conf_file ():
662     res = socket.gethostbyaddr(socket.gethostname())
663     if Cnf.get("Config::" + res[0] + "::AptConfig"):
664         return Cnf["Config::" + res[0] + "::AptConfig"]
665     else:
666         return default_apt_config
667
668 def which_alias_file():
669     hostname = socket.gethostbyaddr(socket.gethostname())[0]
670     aliasfn = '/var/lib/misc/'+hostname+'/forward-alias'
671     if os.path.exists(aliasfn):
672         return aliasfn
673     else:
674         return None
675
676 ################################################################################
677
678 # Escape characters which have meaning to SQL's regex comparison operator ('~')
679 # (woefully incomplete)
680
681 def regex_safe (s):
682     s = s.replace('+', '\\\\+')
683     s = s.replace('.', '\\\\.')
684     return s
685
686 ################################################################################
687
688 # Perform a substition of template
689 def TemplateSubst(map, filename):
690     file = open_file(filename)
691     template = file.read()
692     for x in map.keys():
693         template = template.replace(x,map[x])
694     file.close()
695     return template
696
697 ################################################################################
698
699 def fubar(msg, exit_code=1):
700     sys.stderr.write("E: %s\n" % (msg))
701     sys.exit(exit_code)
702
703 def warn(msg):
704     sys.stderr.write("W: %s\n" % (msg))
705
706 ################################################################################
707
708 # Returns the user name with a laughable attempt at rfc822 conformancy
709 # (read: removing stray periods).
710 def whoami ():
711     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
712
713 ################################################################################
714
715 def size_type (c):
716     t  = " B"
717     if c > 10240:
718         c = c / 1024
719         t = " KB"
720     if c > 10240:
721         c = c / 1024
722         t = " MB"
723     return ("%d%s" % (c, t))
724
725 ################################################################################
726
727 def cc_fix_changes (changes):
728     o = changes.get("architecture", "")
729     if o:
730         del changes["architecture"]
731     changes["architecture"] = {}
732     for j in o.split():
733         changes["architecture"][j] = 1
734
735 # Sort by source name, source version, 'have source', and then by filename
736 def changes_compare (a, b):
737     try:
738         a_changes = parse_changes(a)
739     except:
740         return -1
741
742     try:
743         b_changes = parse_changes(b)
744     except:
745         return 1
746
747     cc_fix_changes (a_changes)
748     cc_fix_changes (b_changes)
749
750     # Sort by source name
751     a_source = a_changes.get("source")
752     b_source = b_changes.get("source")
753     q = cmp (a_source, b_source)
754     if q:
755         return q
756
757     # Sort by source version
758     a_version = a_changes.get("version", "0")
759     b_version = b_changes.get("version", "0")
760     q = apt_pkg.VersionCompare(a_version, b_version)
761     if q:
762         return q
763
764     # Sort by 'have source'
765     a_has_source = a_changes["architecture"].get("source")
766     b_has_source = b_changes["architecture"].get("source")
767     if a_has_source and not b_has_source:
768         return -1
769     elif b_has_source and not a_has_source:
770         return 1
771
772     # Fall back to sort by filename
773     return cmp(a, b)
774
775 ################################################################################
776
777 def find_next_free (dest, too_many=100):
778     extra = 0
779     orig_dest = dest
780     while os.path.exists(dest) and extra < too_many:
781         dest = orig_dest + '.' + repr(extra)
782         extra += 1
783     if extra >= too_many:
784         raise NoFreeFilenameError
785     return dest
786
787 ################################################################################
788
789 def result_join (original, sep = '\t'):
790     list = []
791     for i in xrange(len(original)):
792         if original[i] == None:
793             list.append("")
794         else:
795             list.append(original[i])
796     return sep.join(list)
797
798 ################################################################################
799
800 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
801     out = ""
802     for line in str.split('\n'):
803         line = line.strip()
804         if line or include_blank_lines:
805             out += "%s%s\n" % (prefix, line)
806     # Strip trailing new line
807     if out:
808         out = out[:-1]
809     return out
810
811 ################################################################################
812
813 def validate_changes_file_arg(filename, require_changes=1):
814     """'filename' is either a .changes or .dak file.  If 'filename' is a
815 .dak file, it's changed to be the corresponding .changes file.  The
816 function then checks if the .changes file a) exists and b) is
817 readable and returns the .changes filename if so.  If there's a
818 problem, the next action depends on the option 'require_changes'
819 argument:
820
821  o If 'require_changes' == -1, errors are ignored and the .changes
822                                filename is returned.
823  o If 'require_changes' == 0, a warning is given and 'None' is returned.
824  o If 'require_changes' == 1, a fatal error is raised.
825 """
826     error = None
827
828     orig_filename = filename
829     if filename.endswith(".dak"):
830         filename = filename[:-4]+".changes"
831
832     if not filename.endswith(".changes"):
833         error = "invalid file type; not a changes file"
834     else:
835         if not os.access(filename,os.R_OK):
836             if os.path.exists(filename):
837                 error = "permission denied"
838             else:
839                 error = "file not found"
840
841     if error:
842         if require_changes == 1:
843             fubar("%s: %s." % (orig_filename, error))
844         elif require_changes == 0:
845             warn("Skipping %s - %s" % (orig_filename, error))
846             return None
847         else: # We only care about the .dak file
848             return filename
849     else:
850         return filename
851
852 ################################################################################
853
854 def real_arch(arch):
855     return (arch != "source" and arch != "all")
856
857 ################################################################################
858
859 def join_with_commas_and(list):
860     if len(list) == 0: return "nothing"
861     if len(list) == 1: return list[0]
862     return ", ".join(list[:-1]) + " and " + list[-1]
863
864 ################################################################################
865
866 def pp_deps (deps):
867     pp_deps = []
868     for atom in deps:
869         (pkg, version, constraint) = atom
870         if constraint:
871             pp_dep = "%s (%s %s)" % (pkg, constraint, version)
872         else:
873             pp_dep = pkg
874         pp_deps.append(pp_dep)
875     return " |".join(pp_deps)
876
877 ################################################################################
878
879 def get_conf():
880     return Cnf
881
882 ################################################################################
883
884 # Handle -a, -c and -s arguments; returns them as SQL constraints
885 def parse_args(Options):
886     # Process suite
887     if Options["Suite"]:
888         suite_ids_list = []
889         for suite in split_args(Options["Suite"]):
890             suite_id = database.get_suite_id(suite)
891             if suite_id == -1:
892                 warn("suite '%s' not recognised." % (suite))
893             else:
894                 suite_ids_list.append(suite_id)
895         if suite_ids_list:
896             con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
897         else:
898             fubar("No valid suite given.")
899     else:
900         con_suites = ""
901
902     # Process component
903     if Options["Component"]:
904         component_ids_list = []
905         for component in split_args(Options["Component"]):
906             component_id = database.get_component_id(component)
907             if component_id == -1:
908                 warn("component '%s' not recognised." % (component))
909             else:
910                 component_ids_list.append(component_id)
911         if component_ids_list:
912             con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
913         else:
914             fubar("No valid component given.")
915     else:
916         con_components = ""
917
918     # Process architecture
919     con_architectures = ""
920     if Options["Architecture"]:
921         arch_ids_list = []
922         check_source = 0
923         for architecture in split_args(Options["Architecture"]):
924             if architecture == "source":
925                 check_source = 1
926             else:
927                 architecture_id = database.get_architecture_id(architecture)
928                 if architecture_id == -1:
929                     warn("architecture '%s' not recognised." % (architecture))
930                 else:
931                     arch_ids_list.append(architecture_id)
932         if arch_ids_list:
933             con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
934         else:
935             if not check_source:
936                 fubar("No valid architecture given.")
937     else:
938         check_source = 1
939
940     return (con_suites, con_architectures, con_components, check_source)
941
942 ################################################################################
943
944 # Inspired(tm) by Bryn Keller's print_exc_plus (See
945 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)
946
947 def print_exc():
948     tb = sys.exc_info()[2]
949     while tb.tb_next:
950         tb = tb.tb_next
951     stack = []
952     frame = tb.tb_frame
953     while frame:
954         stack.append(frame)
955         frame = frame.f_back
956     stack.reverse()
957     traceback.print_exc()
958     for frame in stack:
959         print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
960                                              frame.f_code.co_filename,
961                                              frame.f_lineno)
962         for key, value in frame.f_locals.items():
963             print "\t%20s = " % key,
964             try:
965                 print value
966             except:
967                 print "<unable to print>"
968
969 ################################################################################
970
971 def try_with_debug(function):
972     try:
973         function()
974     except SystemExit:
975         raise
976     except:
977         print_exc()
978
979 ################################################################################
980
981 # Function for use in sorting lists of architectures.
982 # Sorts normally except that 'source' dominates all others.
983
984 def arch_compare_sw (a, b):
985     if a == "source" and b == "source":
986         return 0
987     elif a == "source":
988         return -1
989     elif b == "source":
990         return 1
991
992     return cmp (a, b)
993
994 ################################################################################
995
996 # Split command line arguments which can be separated by either commas
997 # or whitespace.  If dwim is set, it will complain about string ending
998 # in comma since this usually means someone did 'dak ls -a i386, m68k
999 # foo' or something and the inevitable confusion resulting from 'm68k'
1000 # being treated as an argument is undesirable.
1001
1002 def split_args (s, dwim=1):
1003     if s.find(",") == -1:
1004         return s.split()
1005     else:
1006         if s[-1:] == "," and dwim:
1007             fubar("split_args: found trailing comma, spurious space maybe?")
1008         return s.split(",")
1009
1010 ################################################################################
1011
1012 def Dict(**dict): return dict
1013
1014 ########################################
1015
1016 # Our very own version of commands.getouputstatus(), hacked to support
1017 # gpgv's status fd.
1018 def gpgv_get_status_output(cmd, status_read, status_write):
1019     cmd = ['/bin/sh', '-c', cmd]
1020     p2cread, p2cwrite = os.pipe()
1021     c2pread, c2pwrite = os.pipe()
1022     errout, errin = os.pipe()
1023     pid = os.fork()
1024     if pid == 0:
1025         # Child
1026         os.close(0)
1027         os.close(1)
1028         os.dup(p2cread)
1029         os.dup(c2pwrite)
1030         os.close(2)
1031         os.dup(errin)
1032         for i in range(3, 256):
1033             if i != status_write:
1034                 try:
1035                     os.close(i)
1036                 except:
1037                     pass
1038         try:
1039             os.execvp(cmd[0], cmd)
1040         finally:
1041             os._exit(1)
1042
1043     # Parent
1044     os.close(p2cread)
1045     os.dup2(c2pread, c2pwrite)
1046     os.dup2(errout, errin)
1047
1048     output = status = ""
1049     while 1:
1050         i, o, e = select.select([c2pwrite, errin, status_read], [], [])
1051         more_data = []
1052         for fd in i:
1053             r = os.read(fd, 8196)
1054             if len(r) > 0:
1055                 more_data.append(fd)
1056                 if fd == c2pwrite or fd == errin:
1057                     output += r
1058                 elif fd == status_read:
1059                     status += r
1060                 else:
1061                     fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
1062         if not more_data:
1063             pid, exit_status = os.waitpid(pid, 0)
1064             try:
1065                 os.close(status_write)
1066                 os.close(status_read)
1067                 os.close(c2pread)
1068                 os.close(c2pwrite)
1069                 os.close(p2cwrite)
1070                 os.close(errin)
1071                 os.close(errout)
1072             except:
1073                 pass
1074             break
1075
1076     return output, status, exit_status
1077
1078 ################################################################################
1079
1080 def process_gpgv_output(status):
1081     # Process the status-fd output
1082     keywords = {}
1083     internal_error = ""
1084     for line in status.split('\n'):
1085         line = line.strip()
1086         if line == "":
1087             continue
1088         split = line.split()
1089         if len(split) < 2:
1090             internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
1091             continue
1092         (gnupg, keyword) = split[:2]
1093         if gnupg != "[GNUPG:]":
1094             internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
1095             continue
1096         args = split[2:]
1097         if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
1098             internal_error += "found duplicate status token ('%s').\n" % (keyword)
1099             continue
1100         else:
1101             keywords[keyword] = args
1102
1103     return (keywords, internal_error)
1104
1105 ################################################################################
1106
1107 def retrieve_key (filename, keyserver=None, keyring=None):
1108     """Retrieve the key that signed 'filename' from 'keyserver' and
1109 add it to 'keyring'.  Returns nothing on success, or an error message
1110 on error."""
1111
1112     # Defaults for keyserver and keyring
1113     if not keyserver:
1114         keyserver = Cnf["Dinstall::KeyServer"]
1115     if not keyring:
1116         keyring = Cnf.ValueList("Dinstall::GPGKeyring")[0]
1117
1118     # Ensure the filename contains no shell meta-characters or other badness
1119     if not re_taint_free.match(filename):
1120         return "%s: tainted filename" % (filename)
1121
1122     # Invoke gpgv on the file
1123     status_read, status_write = os.pipe();
1124     cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
1125     (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
1126
1127     # Process the status-fd output
1128     (keywords, internal_error) = process_gpgv_output(status)
1129     if internal_error:
1130         return internal_error
1131
1132     if not keywords.has_key("NO_PUBKEY"):
1133         return "didn't find expected NO_PUBKEY in gpgv status-fd output"
1134
1135     fingerprint = keywords["NO_PUBKEY"][0]
1136     # XXX - gpg sucks.  You can't use --secret-keyring=/dev/null as
1137     # it'll try to create a lockfile in /dev.  A better solution might
1138     # be a tempfile or something.
1139     cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
1140           % (Cnf["Dinstall::SigningKeyring"])
1141     cmd += " --keyring %s --keyserver %s --recv-key %s" \
1142            % (keyring, keyserver, fingerprint)
1143     (result, output) = commands.getstatusoutput(cmd)
1144     if (result != 0):
1145         return "'%s' failed with exit code %s" % (cmd, result)
1146
1147     return ""
1148
1149 ################################################################################
1150
1151 def gpg_keyring_args(keyrings=None):
1152     if not keyrings:
1153         keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1154
1155     return " ".join(["--keyring %s" % x for x in keyrings])
1156
1157 ################################################################################
1158
1159 def check_signature (sig_filename, reject, data_filename="", keyrings=None, autofetch=None):
1160     """Check the signature of a file and return the fingerprint if the
1161 signature is valid or 'None' if it's not.  The first argument is the
1162 filename whose signature should be checked.  The second argument is a
1163 reject function and is called when an error is found.  The reject()
1164 function must allow for two arguments: the first is the error message,
1165 the second is an optional prefix string.  It's possible for reject()
1166 to be called more than once during an invocation of check_signature().
1167 The third argument is optional and is the name of the files the
1168 detached signature applies to.  The fourth argument is optional and is
1169 a *list* of keyrings to use.  'autofetch' can either be None, True or
1170 False.  If None, the default behaviour specified in the config will be
1171 used."""
1172
1173     # Ensure the filename contains no shell meta-characters or other badness
1174     if not re_taint_free.match(sig_filename):
1175         reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
1176         return None
1177
1178     if data_filename and not re_taint_free.match(data_filename):
1179         reject("!!WARNING!! tainted data filename: '%s'." % (data_filename))
1180         return None
1181
1182     if not keyrings:
1183         keyrings = Cnf.ValueList("Dinstall::GPGKeyring")
1184
1185     # Autofetch the signing key if that's enabled
1186     if autofetch == None:
1187         autofetch = Cnf.get("Dinstall::KeyAutoFetch")
1188     if autofetch:
1189         error_msg = retrieve_key(sig_filename)
1190         if error_msg:
1191             reject(error_msg)
1192             return None
1193
1194     # Build the command line
1195     status_read, status_write = os.pipe();
1196     cmd = "gpgv --status-fd %s %s %s %s" % (
1197         status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)
1198
1199     # Invoke gpgv on the file
1200     (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
1201
1202     # Process the status-fd output
1203     (keywords, internal_error) = process_gpgv_output(status)
1204
1205     # If we failed to parse the status-fd output, let's just whine and bail now
1206     if internal_error:
1207         reject("internal error while performing signature check on %s." % (sig_filename))
1208         reject(internal_error, "")
1209         reject("Please report the above errors to the Archive maintainers by replying to this mail.", "")
1210         return None
1211
1212     bad = ""
1213     # Now check for obviously bad things in the processed output
1214     if keywords.has_key("KEYREVOKED"):
1215         reject("The key used to sign %s has been revoked." % (sig_filename))
1216         bad = 1
1217     if keywords.has_key("BADSIG"):
1218         reject("bad signature on %s." % (sig_filename))
1219         bad = 1
1220     if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1221         reject("failed to check signature on %s." % (sig_filename))
1222         bad = 1
1223     if keywords.has_key("NO_PUBKEY"):
1224         args = keywords["NO_PUBKEY"]
1225         if len(args) >= 1:
1226             key = args[0]
1227         reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
1228         bad = 1
1229     if keywords.has_key("BADARMOR"):
1230         reject("ASCII armour of signature was corrupt in %s." % (sig_filename))
1231         bad = 1
1232     if keywords.has_key("NODATA"):
1233         reject("no signature found in %s." % (sig_filename))
1234         bad = 1
1235     if keywords.has_key("EXPKEYSIG"):
1236         args = keywords["EXPKEYSIG"]
1237         if len(args) >= 1:
1238             key = args[0]
1239         reject("Signature made by expired key 0x%s" % (key))
1240         bad = 1
1241     if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1242         args = keywords["KEYEXPIRED"]
1243         expiredate=""
1244         if len(args) >= 1:
1245             timestamp = args[0]
1246             if timestamp.count("T") == 0:
1247                 expiredate = time.strftime("%Y-%m-%d", time.gmtime(timestamp))
1248             else:
1249                 expiredate = timestamp
1250         reject("The key used to sign %s has expired on %s" % (sig_filename, expiredate))
1251         bad = 1
1252
1253     if bad:
1254         return None
1255
1256     # Next check gpgv exited with a zero return code
1257     if exit_status:
1258         reject("gpgv failed while checking %s." % (sig_filename))
1259         if status.strip():
1260             reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), "")
1261         else:
1262             reject(prefix_multi_line_string(output, " [GPG output:] "), "")
1263         return None
1264
1265     # Sanity check the good stuff we expect
1266     if not keywords.has_key("VALIDSIG"):
1267         reject("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
1268         bad = 1
1269     else:
1270         args = keywords["VALIDSIG"]
1271         if len(args) < 1:
1272             reject("internal error while checking signature on %s." % (sig_filename))
1273             bad = 1
1274         else:
1275             fingerprint = args[0]
1276     if not keywords.has_key("GOODSIG"):
1277         reject("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
1278         bad = 1
1279     if not keywords.has_key("SIG_ID"):
1280         reject("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
1281         bad = 1
1282
1283     # Finally ensure there's not something we don't recognise
1284     known_keywords = Dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
1285                           SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1286                           NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="")
1287
1288     for keyword in keywords.keys():
1289         if not known_keywords.has_key(keyword):
1290             reject("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
1291             bad = 1
1292
1293     if bad:
1294         return None
1295     else:
1296         return fingerprint
1297
1298 ################################################################################
1299
1300 def gpg_get_key_addresses(fingerprint):
1301     """retreive email addresses from gpg key uids for a given fingerprint"""
1302     addresses = key_uid_email_cache.get(fingerprint)
1303     if addresses != None:
1304         return addresses
1305     addresses = set()
1306     cmd = "gpg --no-default-keyring %s --fingerprint %s" \
1307                 % (gpg_keyring_args(), fingerprint)
1308     (result, output) = commands.getstatusoutput(cmd)
1309     if result == 0:
1310         for l in output.split('\n'):
1311             m = re_gpg_uid.match(l)
1312             if m:
1313                 addresses.add(m.group(1))
1314     key_uid_email_cache[fingerprint] = addresses
1315     return addresses
1316
1317 ################################################################################
1318
1319 # Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603
1320
1321 def wrap(paragraph, max_length, prefix=""):
1322     line = ""
1323     s = ""
1324     have_started = 0
1325     words = paragraph.split()
1326
1327     for word in words:
1328         word_size = len(word)
1329         if word_size > max_length:
1330             if have_started:
1331                 s += line + '\n' + prefix
1332             s += word + '\n' + prefix
1333         else:
1334             if have_started:
1335                 new_length = len(line) + word_size + 1
1336                 if new_length > max_length:
1337                     s += line + '\n' + prefix
1338                     line = word
1339                 else:
1340                     line += ' ' + word
1341             else:
1342                 line = word
1343         have_started = 1
1344
1345     if have_started:
1346         s += line
1347
1348     return s
1349
1350 ################################################################################
1351
1352 # Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
1353 # Returns fixed 'src'
1354 def clean_symlink (src, dest, root):
1355     src = src.replace(root, '', 1)
1356     dest = dest.replace(root, '', 1)
1357     dest = os.path.dirname(dest)
1358     new_src = '../' * len(dest.split('/'))
1359     return new_src + src
1360
1361 ################################################################################
1362
1363 def temp_filename(directory=None, dotprefix=None, perms=0700):
1364     """Return a secure and unique filename by pre-creating it.
1365 If 'directory' is non-null, it will be the directory the file is pre-created in.
1366 If 'dotprefix' is non-null, the filename will be prefixed with a '.'."""
1367
1368     if directory:
1369         old_tempdir = tempfile.tempdir
1370         tempfile.tempdir = directory
1371
1372     filename = tempfile.mktemp()
1373
1374     if dotprefix:
1375         filename = "%s/.%s" % (os.path.dirname(filename), os.path.basename(filename))
1376     fd = os.open(filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, perms)
1377     os.close(fd)
1378
1379     if directory:
1380         tempfile.tempdir = old_tempdir
1381
1382     return filename
1383
1384 ################################################################################
1385
1386 # checks if the user part of the email is listed in the alias file
1387
1388 def is_email_alias(email):
1389     global alias_cache
1390     if alias_cache == None:
1391         aliasfn = which_alias_file()
1392         alias_cache = set()
1393         if aliasfn:
1394             for l in open(aliasfn):
1395                 alias_cache.add(l.split(':')[0])
1396     uid = email.split('@')[0]
1397     return uid in alias_cache
1398
1399 ################################################################################
1400
1401 apt_pkg.init()
1402
1403 Cnf = apt_pkg.newConfiguration()
1404 apt_pkg.ReadConfigFileISC(Cnf,default_config)
1405
1406 if which_conf_file() != default_config:
1407     apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1408
1409 ################################################################################
1410
1411 def generate_contents_information(filename):
1412     """
1413     Generate a list of flies contained in a .deb
1414
1415     @type filename: string
1416     @param filename: the path to a .deb
1417
1418     @rtype: list
1419     @return: a list of files in the data.tar.* portion of the .deb
1420     """
1421     cmd = "ar t %s" % (filename)
1422     (result, output) = commands.getstatusoutput(cmd)
1423     if result != 0:
1424         reject("%s: 'ar t' invocation failed." % (filename))
1425         reject(utils.prefix_multi_line_string(output, " [ar output:] "), "")
1426
1427     # Ugh ... this is ugly ... Code ripped from process_unchecked.py
1428     chunks = output.split('\n')
1429
1430     contents = []
1431     try:
1432         cmd = "ar x %s %s" % (filename, chunks[2])
1433         (result, output) = commands.getstatusoutput(cmd)
1434         if result != 0:
1435             reject("%s: '%s' invocation failed." % (filename, cmd))
1436             reject(utils.prefix_multi_line_string(output, " [ar output:] "), "")
1437
1438         # Got deb tarballs, now lets go through and determine what bits
1439         # and pieces the deb had ...
1440         if chunks[2] == "data.tar.gz":
1441             data = tarfile.open("data.tar.gz", "r:gz")
1442         elif data_tar == "data.tar.bz2":
1443             data = tarfile.open("data.tar.bz2", "r:bz2")
1444         else:
1445             os.remove(chunks[2])
1446             reject("couldn't find data.tar.*")
1447
1448         for tarinfo in data:
1449             if not tarinfo.isdir():
1450                 contents.append(tarinfo.name[2:])
1451
1452     finally:
1453         if os.path.exists( chunks[2] ):
1454             os.remove( chunks[2] )
1455
1456     return contents
1457
1458 ###############################################################################