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