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