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