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