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