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