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