]> git.decadent.org.uk Git - dak.git/blob - dak/process_unchecked.py
merge with master
[dak.git] / dak / process_unchecked.py
1 #!/usr/bin/env python
2
3 """ Checks Debian packages from Incoming """
4 # Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 # Originally based on dinstall by Guy Maor <maor@debian.org>
21
22 ################################################################################
23
24 # Computer games don't affect kids. I mean if Pacman affected our generation as
25 # kids, we'd all run around in a darkened room munching pills and listening to
26 # repetitive music.
27 #         -- Unknown
28
29 ################################################################################
30
31 import commands
32 import errno
33 import fcntl
34 import os
35 import re
36 import shutil
37 import stat
38 import sys
39 import time
40 import traceback
41 import tarfile
42 import apt_inst
43 import apt_pkg
44 from debian_bundle import deb822
45 from daklib.dbconn import DBConn
46 from daklib.binary import Binary
47 from daklib import logging
48 from daklib import queue
49 from daklib import utils
50 from daklib.dak_exceptions import *
51 from daklib.regexes import re_valid_version, re_valid_pkg_name, re_changelog_versions, \
52                            re_strip_revision, re_strip_srcver, re_spacestrip, \
53                            re_isanum, re_no_epoch, re_no_revision, re_taint_free, \
54                            re_isadeb, re_extract_src_version, re_issource, re_default_answer
55
56 from types import *
57
58 ################################################################################
59
60
61 ################################################################################
62
63 # Globals
64 Cnf = None
65 Options = None
66 Logger = None
67 Upload = None
68
69 reprocess = 0
70 in_holding = {}
71
72 # Aliases to the real vars in the Upload class; hysterical raisins.
73 reject_message = ""
74 changes = {}
75 dsc = {}
76 dsc_files = {}
77 files = {}
78 pkg = {}
79
80 ###############################################################################
81
82 def init():
83     global Cnf, Options, Upload, changes, dsc, dsc_files, files, pkg
84
85     apt_pkg.init()
86
87     Cnf = apt_pkg.newConfiguration()
88     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file())
89
90     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
91                  ('h',"help","Dinstall::Options::Help"),
92                  ('n',"no-action","Dinstall::Options::No-Action"),
93                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
94                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
95                  ('d',"directory", "Dinstall::Options::Directory", "HasArg")]
96
97     for i in ["automatic", "help", "no-action", "no-lock", "no-mail",
98               "override-distribution", "version", "directory"]:
99         Cnf["Dinstall::Options::%s" % (i)] = ""
100
101     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv)
102     Options = Cnf.SubTree("Dinstall::Options")
103
104     if Options["Help"]:
105         usage()
106
107     # If we have a directory flag, use it to find our files
108     if Cnf["Dinstall::Options::Directory"] != "":
109         # Note that we clobber the list of files we were given in this case
110         # so warn if the user has done both
111         if len(changes_files) > 0:
112             utils.warn("Directory provided so ignoring files given on command line")
113
114         changes_files = utils.get_changes_files(Cnf["Dinstall::Options::Directory"])
115
116     Upload = queue.Upload(Cnf)
117
118     changes = Upload.pkg.changes
119     dsc = Upload.pkg.dsc
120     dsc_files = Upload.pkg.dsc_files
121     files = Upload.pkg.files
122     pkg = Upload.pkg
123
124     return changes_files
125
126 ################################################################################
127
128 def usage (exit_code=0):
129     print """Usage: dinstall [OPTION]... [CHANGES]...
130   -a, --automatic           automatic run
131   -h, --help                show this help and exit.
132   -n, --no-action           don't do anything
133   -p, --no-lock             don't check lockfile !! for cron.daily only !!
134   -s, --no-mail             don't send any mail
135   -V, --version             display the version number and exit"""
136     sys.exit(exit_code)
137
138 ################################################################################
139
140 def reject (str, prefix="Rejected: "):
141     global reject_message
142     if str:
143         reject_message += prefix + str + "\n"
144
145 ################################################################################
146
147 def copy_to_holding(filename):
148     global in_holding
149
150     base_filename = os.path.basename(filename)
151
152     dest = Cnf["Dir::Queue::Holding"] + '/' + base_filename
153     try:
154         fd = os.open(dest, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0640)
155         os.close(fd)
156     except OSError, e:
157         # Shouldn't happen, but will if, for example, someone lists a
158         # file twice in the .changes.
159         if errno.errorcode[e.errno] == 'EEXIST':
160             reject("%s: already exists in holding area; can not overwrite." % (base_filename))
161             return
162         raise
163
164     try:
165         shutil.copy(filename, dest)
166     except IOError, e:
167         # In either case (ENOENT or EACCES) we want to remove the
168         # O_CREAT | O_EXCLed ghost file, so add the file to the list
169         # of 'in holding' even if it's not the real file.
170         if errno.errorcode[e.errno] == 'ENOENT':
171             reject("%s: can not copy to holding area: file not found." % (base_filename))
172             os.unlink(dest)
173             return
174         elif errno.errorcode[e.errno] == 'EACCES':
175             reject("%s: can not copy to holding area: read permission denied." % (base_filename))
176             os.unlink(dest)
177             return
178         raise
179
180     in_holding[base_filename] = ""
181
182 ################################################################################
183
184 def clean_holding():
185     global in_holding
186
187     cwd = os.getcwd()
188     os.chdir(Cnf["Dir::Queue::Holding"])
189     for f in in_holding.keys():
190         if os.path.exists(f):
191             if f.find('/') != -1:
192                 utils.fubar("WTF? clean_holding() got a file ('%s') with / in it!" % (f))
193             else:
194                 os.unlink(f)
195     in_holding = {}
196     os.chdir(cwd)
197
198 ################################################################################
199
200 def check_changes():
201     filename = pkg.changes_file
202
203     # Parse the .changes field into a dictionary
204     try:
205         changes.update(utils.parse_changes(filename))
206     except CantOpenError:
207         reject("%s: can't read file." % (filename))
208         return 0
209     except ParseChangesError, line:
210         reject("%s: parse error, can't grok: %s." % (filename, line))
211         return 0
212     except ChangesUnicodeError:
213         reject("%s: changes file not proper utf-8" % (filename))
214         return 0
215
216     # Parse the Files field from the .changes into another dictionary
217     try:
218         files.update(utils.build_file_list(changes))
219     except ParseChangesError, line:
220         reject("%s: parse error, can't grok: %s." % (filename, line))
221     except UnknownFormatError, format:
222         reject("%s: unknown format '%s'." % (filename, format))
223         return 0
224
225     # Check for mandatory fields
226     for i in ("source", "binary", "architecture", "version", "distribution",
227               "maintainer", "files", "changes", "description"):
228         if not changes.has_key(i):
229             reject("%s: Missing mandatory field `%s'." % (filename, i))
230             return 0    # Avoid <undef> errors during later tests
231
232     # Strip a source version in brackets from the source field
233     if re_strip_srcver.search(changes["source"]):
234         changes["source"] = re_strip_srcver.sub('', changes["source"])
235
236     # Ensure the source field is a valid package name.
237     if not re_valid_pkg_name.match(changes["source"]):
238         reject("%s: invalid source name '%s'." % (filename, changes["source"]))
239
240     # Split multi-value fields into a lower-level dictionary
241     for i in ("architecture", "distribution", "binary", "closes"):
242         o = changes.get(i, "")
243         if o != "":
244             del changes[i]
245         changes[i] = {}
246         for j in o.split():
247             changes[i][j] = 1
248
249     # Fix the Maintainer: field to be RFC822/2047 compatible
250     try:
251         (changes["maintainer822"], changes["maintainer2047"],
252          changes["maintainername"], changes["maintaineremail"]) = \
253          utils.fix_maintainer (changes["maintainer"])
254     except ParseMaintError, msg:
255         reject("%s: Maintainer field ('%s') failed to parse: %s" \
256                % (filename, changes["maintainer"], msg))
257
258     # ...likewise for the Changed-By: field if it exists.
259     try:
260         (changes["changedby822"], changes["changedby2047"],
261          changes["changedbyname"], changes["changedbyemail"]) = \
262          utils.fix_maintainer (changes.get("changed-by", ""))
263     except ParseMaintError, msg:
264         (changes["changedby822"], changes["changedby2047"],
265          changes["changedbyname"], changes["changedbyemail"]) = \
266          ("", "", "", "")
267         reject("%s: Changed-By field ('%s') failed to parse: %s" \
268                % (filename, changes["changed-by"], msg))
269
270     # Ensure all the values in Closes: are numbers
271     if changes.has_key("closes"):
272         for i in changes["closes"].keys():
273             if re_isanum.match (i) == None:
274                 reject("%s: `%s' from Closes field isn't a number." % (filename, i))
275
276
277     # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
278     changes["chopversion"] = re_no_epoch.sub('', changes["version"])
279     changes["chopversion2"] = re_no_revision.sub('', changes["chopversion"])
280
281     # Check there isn't already a changes file of the same name in one
282     # of the queue directories.
283     base_filename = os.path.basename(filename)
284     for d in [ "Accepted", "Byhand", "Done", "New", "ProposedUpdates", "OldProposedUpdates" ]:
285         if os.path.exists(Cnf["Dir::Queue::%s" % (d) ]+'/'+base_filename):
286             reject("%s: a file with this name already exists in the %s directory." % (base_filename, d))
287
288     # Check the .changes is non-empty
289     if not files:
290         reject("%s: nothing to do (Files field is empty)." % (base_filename))
291         return 0
292
293     return 1
294
295 ################################################################################
296
297 def check_distributions():
298     "Check and map the Distribution field of a .changes file."
299
300     # Handle suite mappings
301     for m in Cnf.ValueList("SuiteMappings"):
302         args = m.split()
303         mtype = args[0]
304         if mtype == "map" or mtype == "silent-map":
305             (source, dest) = args[1:3]
306             if changes["distribution"].has_key(source):
307                 del changes["distribution"][source]
308                 changes["distribution"][dest] = 1
309                 if mtype != "silent-map":
310                     reject("Mapping %s to %s." % (source, dest),"")
311             if changes.has_key("distribution-version"):
312                 if changes["distribution-version"].has_key(source):
313                     changes["distribution-version"][source]=dest
314         elif mtype == "map-unreleased":
315             (source, dest) = args[1:3]
316             if changes["distribution"].has_key(source):
317                 for arch in changes["architecture"].keys():
318                     if arch not in DBConn().get_suite_architectures(source):
319                         reject("Mapping %s to %s for unreleased architecture %s." % (source, dest, arch),"")
320                         del changes["distribution"][source]
321                         changes["distribution"][dest] = 1
322                         break
323         elif mtype == "ignore":
324             suite = args[1]
325             if changes["distribution"].has_key(suite):
326                 del changes["distribution"][suite]
327                 reject("Ignoring %s as a target suite." % (suite), "Warning: ")
328         elif mtype == "reject":
329             suite = args[1]
330             if changes["distribution"].has_key(suite):
331                 reject("Uploads to %s are not accepted." % (suite))
332         elif mtype == "propup-version":
333             # give these as "uploaded-to(non-mapped) suites-to-add-when-upload-obsoletes"
334             #
335             # changes["distribution-version"] looks like: {'testing': 'testing-proposed-updates'}
336             if changes["distribution"].has_key(args[1]):
337                 changes.setdefault("distribution-version", {})
338                 for suite in args[2:]: changes["distribution-version"][suite]=suite
339
340     # Ensure there is (still) a target distribution
341     if changes["distribution"].keys() == []:
342         reject("no valid distribution.")
343
344     # Ensure target distributions exist
345     for suite in changes["distribution"].keys():
346         if not Cnf.has_key("Suite::%s" % (suite)):
347             reject("Unknown distribution `%s'." % (suite))
348
349 ################################################################################
350
351 def check_files():
352     global reprocess
353
354     archive = utils.where_am_i()
355     file_keys = files.keys()
356
357     # if reprocess is 2 we've already done this and we're checking
358     # things again for the new .orig.tar.gz.
359     # [Yes, I'm fully aware of how disgusting this is]
360     if not Options["No-Action"] and reprocess < 2:
361         cwd = os.getcwd()
362         os.chdir(pkg.directory)
363         for f in file_keys:
364             copy_to_holding(f)
365         os.chdir(cwd)
366
367     # Check there isn't already a .changes or .dak file of the same name in
368     # the proposed-updates "CopyChanges" or "CopyDotDak" storage directories.
369     # [NB: this check must be done post-suite mapping]
370     base_filename = os.path.basename(pkg.changes_file)
371     dot_dak_filename = base_filename[:-8]+".dak"
372     for suite in changes["distribution"].keys():
373         copychanges = "Suite::%s::CopyChanges" % (suite)
374         if Cnf.has_key(copychanges) and \
375                os.path.exists(Cnf[copychanges]+"/"+base_filename):
376             reject("%s: a file with this name already exists in %s" \
377                    % (base_filename, Cnf[copychanges]))
378
379         copy_dot_dak = "Suite::%s::CopyDotDak" % (suite)
380         if Cnf.has_key(copy_dot_dak) and \
381                os.path.exists(Cnf[copy_dot_dak]+"/"+dot_dak_filename):
382             reject("%s: a file with this name already exists in %s" \
383                    % (dot_dak_filename, Cnf[copy_dot_dak]))
384
385     reprocess = 0
386     has_binaries = 0
387     has_source = 0
388
389     cursor = DBConn().cursor()
390     # Check for packages that have moved from one component to another
391     # STU: this should probably be changed to not join on architecture, suite tables but instead to used their cached name->id mappings from DBConn
392     DBConn().prepare("moved_pkg_q", """
393         PREPARE moved_pkg_q(text,text,text) AS
394         SELECT c.name FROM binaries b, bin_associations ba, suite s, location l,
395                     component c, architecture a, files f
396         WHERE b.package = $1 AND s.suite_name = $2
397           AND (a.arch_string = $3 OR a.arch_string = 'all')
398           AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id
399           AND f.location = l.id
400           AND l.component = c.id
401           AND b.file = f.id""")
402
403     for f in file_keys:
404         # Ensure the file does not already exist in one of the accepted directories
405         for d in [ "Accepted", "Byhand", "New", "ProposedUpdates", "OldProposedUpdates", "Embargoed", "Unembargoed" ]:
406             if not Cnf.has_key("Dir::Queue::%s" % (d)): continue
407             if os.path.exists(Cnf["Dir::Queue::%s" % (d) ] + '/' + f):
408                 reject("%s file already exists in the %s directory." % (f, d))
409         if not re_taint_free.match(f):
410             reject("!!WARNING!! tainted filename: '%s'." % (f))
411         # Check the file is readable
412         if os.access(f, os.R_OK) == 0:
413             # When running in -n, copy_to_holding() won't have
414             # generated the reject_message, so we need to.
415             if Options["No-Action"]:
416                 if os.path.exists(f):
417                     reject("Can't read `%s'. [permission denied]" % (f))
418                 else:
419                     reject("Can't read `%s'. [file not found]" % (f))
420             files[f]["type"] = "unreadable"
421             continue
422         # If it's byhand skip remaining checks
423         if files[f]["section"] == "byhand" or files[f]["section"][:4] == "raw-":
424             files[f]["byhand"] = 1
425             files[f]["type"] = "byhand"
426         # Checks for a binary package...
427         elif re_isadeb.match(f):
428             has_binaries = 1
429             files[f]["type"] = "deb"
430
431             # Extract package control information
432             deb_file = utils.open_file(f)
433             try:
434                 control = apt_pkg.ParseSection(apt_inst.debExtractControl(deb_file))
435             except:
436                 reject("%s: debExtractControl() raised %s." % (f, sys.exc_type))
437                 deb_file.close()
438                 # Can't continue, none of the checks on control would work.
439                 continue
440             deb_file.close()
441
442             # Check for mandatory fields
443             for field in [ "Package", "Architecture", "Version" ]:
444                 if control.Find(field) == None:
445                     reject("%s: No %s field in control." % (f, field))
446                     # Can't continue
447                     continue
448
449             # Ensure the package name matches the one give in the .changes
450             if not changes["binary"].has_key(control.Find("Package", "")):
451                 reject("%s: control file lists name as `%s', which isn't in changes file." % (f, control.Find("Package", "")))
452
453             # Validate the package field
454             package = control.Find("Package")
455             if not re_valid_pkg_name.match(package):
456                 reject("%s: invalid package name '%s'." % (f, package))
457
458             # Validate the version field
459             version = control.Find("Version")
460             if not re_valid_version.match(version):
461                 reject("%s: invalid version number '%s'." % (f, version))
462
463             # Ensure the architecture of the .deb is one we know about.
464             default_suite = Cnf.get("Dinstall::DefaultSuite", "Unstable")
465             architecture = control.Find("Architecture")
466             upload_suite = changes["distribution"].keys()[0]
467             if architecture not in DBConn().get_suite_architectures(default_suite) and architecture not in DBConn().get_suite_architectures(upload_suite):
468                 reject("Unknown architecture '%s'." % (architecture))
469
470             # Ensure the architecture of the .deb is one of the ones
471             # listed in the .changes.
472             if not changes["architecture"].has_key(architecture):
473                 reject("%s: control file lists arch as `%s', which isn't in changes file." % (f, architecture))
474
475             # Sanity-check the Depends field
476             depends = control.Find("Depends")
477             if depends == '':
478                 reject("%s: Depends field is empty." % (f))
479
480             # Sanity-check the Provides field
481             provides = control.Find("Provides")
482             if provides:
483                 provide = re_spacestrip.sub('', provides)
484                 if provide == '':
485                     reject("%s: Provides field is empty." % (f))
486                 prov_list = provide.split(",")
487                 for prov in prov_list:
488                     if not re_valid_pkg_name.match(prov):
489                         reject("%s: Invalid Provides field content %s." % (f, prov))
490
491
492             # Check the section & priority match those given in the .changes (non-fatal)
493             if control.Find("Section") and files[f]["section"] != "" and files[f]["section"] != control.Find("Section"):
494                 reject("%s control file lists section as `%s', but changes file has `%s'." % (f, control.Find("Section", ""), files[f]["section"]), "Warning: ")
495             if control.Find("Priority") and files[f]["priority"] != "" and files[f]["priority"] != control.Find("Priority"):
496                 reject("%s control file lists priority as `%s', but changes file has `%s'." % (f, control.Find("Priority", ""), files[f]["priority"]),"Warning: ")
497
498             files[f]["package"] = package
499             files[f]["architecture"] = architecture
500             files[f]["version"] = version
501             files[f]["maintainer"] = control.Find("Maintainer", "")
502             if f.endswith(".udeb"):
503                 files[f]["dbtype"] = "udeb"
504             elif f.endswith(".deb"):
505                 files[f]["dbtype"] = "deb"
506             else:
507                 reject("%s is neither a .deb or a .udeb." % (f))
508             files[f]["source"] = control.Find("Source", files[f]["package"])
509             # Get the source version
510             source = files[f]["source"]
511             source_version = ""
512             if source.find("(") != -1:
513                 m = re_extract_src_version.match(source)
514                 source = m.group(1)
515                 source_version = m.group(2)
516             if not source_version:
517                 source_version = files[f]["version"]
518             files[f]["source package"] = source
519             files[f]["source version"] = source_version
520
521             # Ensure the filename matches the contents of the .deb
522             m = re_isadeb.match(f)
523             #  package name
524             file_package = m.group(1)
525             if files[f]["package"] != file_package:
526                 reject("%s: package part of filename (%s) does not match package name in the %s (%s)." % (f, file_package, files[f]["dbtype"], files[f]["package"]))
527             epochless_version = re_no_epoch.sub('', control.Find("Version"))
528             #  version
529             file_version = m.group(2)
530             if epochless_version != file_version:
531                 reject("%s: version part of filename (%s) does not match package version in the %s (%s)." % (f, file_version, files[f]["dbtype"], epochless_version))
532             #  architecture
533             file_architecture = m.group(3)
534             if files[f]["architecture"] != file_architecture:
535                 reject("%s: architecture part of filename (%s) does not match package architecture in the %s (%s)." % (f, file_architecture, files[f]["dbtype"], files[f]["architecture"]))
536
537             # Check for existent source
538             source_version = files[f]["source version"]
539             source_package = files[f]["source package"]
540             if changes["architecture"].has_key("source"):
541                 if source_version != changes["version"]:
542                     reject("source version (%s) for %s doesn't match changes version %s." % (source_version, f, changes["version"]))
543             else:
544                 # Check in the SQL database
545                 if not Upload.source_exists(source_package, source_version, changes["distribution"].keys()):
546                     # Check in one of the other directories
547                     source_epochless_version = re_no_epoch.sub('', source_version)
548                     dsc_filename = "%s_%s.dsc" % (source_package, source_epochless_version)
549                     if os.path.exists(Cnf["Dir::Queue::Byhand"] + '/' + dsc_filename):
550                         files[f]["byhand"] = 1
551                     elif os.path.exists(Cnf["Dir::Queue::New"] + '/' + dsc_filename):
552                         files[f]["new"] = 1
553                     else:
554                         dsc_file_exists = 0
555                         for myq in ["Accepted", "Embargoed", "Unembargoed", "ProposedUpdates", "OldProposedUpdates"]:
556                             if Cnf.has_key("Dir::Queue::%s" % (myq)):
557                                 if os.path.exists(Cnf["Dir::Queue::"+myq] + '/' + dsc_filename):
558                                     dsc_file_exists = 1
559                                     break
560                         if not dsc_file_exists:
561                             reject("no source found for %s %s (%s)." % (source_package, source_version, f))
562             # Check the version and for file overwrites
563             reject(Upload.check_binary_against_db(f),"")
564
565             Binary(f, reject).scan_package()
566
567         # Checks for a source package...
568         else:
569             m = re_issource.match(f)
570             if m:
571                 has_source = 1
572                 files[f]["package"] = m.group(1)
573                 files[f]["version"] = m.group(2)
574                 files[f]["type"] = m.group(3)
575
576                 # Ensure the source package name matches the Source filed in the .changes
577                 if changes["source"] != files[f]["package"]:
578                     reject("%s: changes file doesn't say %s for Source" % (f, files[f]["package"]))
579
580                 # Ensure the source version matches the version in the .changes file
581                 if files[f]["type"] == "orig.tar.gz":
582                     changes_version = changes["chopversion2"]
583                 else:
584                     changes_version = changes["chopversion"]
585                 if changes_version != files[f]["version"]:
586                     reject("%s: should be %s according to changes file." % (f, changes_version))
587
588                 # Ensure the .changes lists source in the Architecture field
589                 if not changes["architecture"].has_key("source"):
590                     reject("%s: changes file doesn't list `source' in Architecture field." % (f))
591
592                 # Check the signature of a .dsc file
593                 if files[f]["type"] == "dsc":
594                     dsc["fingerprint"] = utils.check_signature(f, reject)
595
596                 files[f]["architecture"] = "source"
597
598             # Not a binary or source package?  Assume byhand...
599             else:
600                 files[f]["byhand"] = 1
601                 files[f]["type"] = "byhand"
602
603         # Per-suite file checks
604         files[f]["oldfiles"] = {}
605         for suite in changes["distribution"].keys():
606             # Skip byhand
607             if files[f].has_key("byhand"):
608                 continue
609
610             # Handle component mappings
611             for m in Cnf.ValueList("ComponentMappings"):
612                 (source, dest) = m.split()
613                 if files[f]["component"] == source:
614                     files[f]["original component"] = source
615                     files[f]["component"] = dest
616
617             # Ensure the component is valid for the target suite
618             if Cnf.has_key("Suite:%s::Components" % (suite)) and \
619                files[f]["component"] not in Cnf.ValueList("Suite::%s::Components" % (suite)):
620                 reject("unknown component `%s' for suite `%s'." % (files[f]["component"], suite))
621                 continue
622
623             # Validate the component
624             component = files[f]["component"]
625             component_id = DBConn().get_component_id(component)
626             if component_id == -1:
627                 reject("file '%s' has unknown component '%s'." % (f, component))
628                 continue
629
630             # See if the package is NEW
631             if not Upload.in_override_p(files[f]["package"], files[f]["component"], suite, files[f].get("dbtype",""), f):
632                 files[f]["new"] = 1
633
634             # Validate the priority
635             if files[f]["priority"].find('/') != -1:
636                 reject("file '%s' has invalid priority '%s' [contains '/']." % (f, files[f]["priority"]))
637
638             # Determine the location
639             location = Cnf["Dir::Pool"]
640             location_id = DBConn().get_location_id(location, component, archive)
641             if location_id == -1:
642                 reject("[INTERNAL ERROR] couldn't determine location (Component: %s, Archive: %s)" % (component, archive))
643             files[f]["location id"] = location_id
644
645             # Check the md5sum & size against existing files (if any)
646             files[f]["pool name"] = utils.poolify (changes["source"], files[f]["component"])
647             files_id = DBConn().get_files_id(files[f]["pool name"] + f, files[f]["size"], files[f]["md5sum"], files[f]["location id"])
648             if files_id == -1:
649                 reject("INTERNAL ERROR, get_files_id() returned multiple matches for %s." % (f))
650             elif files_id == -2:
651                 reject("md5sum and/or size mismatch on existing copy of %s." % (f))
652             files[f]["files id"] = files_id
653
654             # Check for packages that have moved from one component to another
655             files[f]['suite'] = suite
656             cursor.execute("""EXECUTE moved_pkg_q( %(package)s, %(suite)s, %(architecture)s )""", ( files[f] ) )
657             ql = cursor.fetchone()
658             if ql:
659                 files[f]["othercomponents"] = ql[0][0]
660
661     # If the .changes file says it has source, it must have source.
662     if changes["architecture"].has_key("source"):
663         if not has_source:
664             reject("no source found and Architecture line in changes mention source.")
665
666         if not has_binaries and Cnf.FindB("Dinstall::Reject::NoSourceOnly"):
667             reject("source only uploads are not supported.")
668
669 ###############################################################################
670
671 def check_dsc():
672     global reprocess
673
674     # Ensure there is source to check
675     if not changes["architecture"].has_key("source"):
676         return 1
677
678     # Find the .dsc
679     dsc_filename = None
680     for f in files.keys():
681         if files[f]["type"] == "dsc":
682             if dsc_filename:
683                 reject("can not process a .changes file with multiple .dsc's.")
684                 return 0
685             else:
686                 dsc_filename = f
687
688     # If there isn't one, we have nothing to do. (We have reject()ed the upload already)
689     if not dsc_filename:
690         reject("source uploads must contain a dsc file")
691         return 0
692
693     # Parse the .dsc file
694     try:
695         dsc.update(utils.parse_changes(dsc_filename, signing_rules=1))
696     except CantOpenError:
697         # if not -n copy_to_holding() will have done this for us...
698         if Options["No-Action"]:
699             reject("%s: can't read file." % (dsc_filename))
700     except ParseChangesError, line:
701         reject("%s: parse error, can't grok: %s." % (dsc_filename, line))
702     except InvalidDscError, line:
703         reject("%s: syntax error on line %s." % (dsc_filename, line))
704     except ChangesUnicodeError:
705         reject("%s: dsc file not proper utf-8." % (dsc_filename))
706
707     # Build up the file list of files mentioned by the .dsc
708     try:
709         dsc_files.update(utils.build_file_list(dsc, is_a_dsc=1))
710     except NoFilesFieldError:
711         reject("%s: no Files: field." % (dsc_filename))
712         return 0
713     except UnknownFormatError, format:
714         reject("%s: unknown format '%s'." % (dsc_filename, format))
715         return 0
716     except ParseChangesError, line:
717         reject("%s: parse error, can't grok: %s." % (dsc_filename, line))
718         return 0
719
720     # Enforce mandatory fields
721     for i in ("format", "source", "version", "binary", "maintainer", "architecture", "files"):
722         if not dsc.has_key(i):
723             reject("%s: missing mandatory field `%s'." % (dsc_filename, i))
724             return 0
725
726     # Validate the source and version fields
727     if not re_valid_pkg_name.match(dsc["source"]):
728         reject("%s: invalid source name '%s'." % (dsc_filename, dsc["source"]))
729     if not re_valid_version.match(dsc["version"]):
730         reject("%s: invalid version number '%s'." % (dsc_filename, dsc["version"]))
731
732     # Bumping the version number of the .dsc breaks extraction by stable's
733     # dpkg-source.  So let's not do that...
734     if dsc["format"] != "1.0":
735         reject("%s: incompatible 'Format' version produced by a broken version of dpkg-dev 1.9.1{3,4}." % (dsc_filename))
736
737     # Validate the Maintainer field
738     try:
739         utils.fix_maintainer (dsc["maintainer"])
740     except ParseMaintError, msg:
741         reject("%s: Maintainer field ('%s') failed to parse: %s" \
742                % (dsc_filename, dsc["maintainer"], msg))
743
744     # Validate the build-depends field(s)
745     for field_name in [ "build-depends", "build-depends-indep" ]:
746         field = dsc.get(field_name)
747         if field:
748             # Check for broken dpkg-dev lossage...
749             if field.startswith("ARRAY"):
750                 reject("%s: invalid %s field produced by a broken version of dpkg-dev (1.10.11)" % (dsc_filename, field_name.title()))
751
752             # Have apt try to parse them...
753             try:
754                 apt_pkg.ParseSrcDepends(field)
755             except:
756                 reject("%s: invalid %s field (can not be parsed by apt)." % (dsc_filename, field_name.title()))
757                 pass
758
759     # Ensure the version number in the .dsc matches the version number in the .changes
760     epochless_dsc_version = re_no_epoch.sub('', dsc["version"])
761     changes_version = files[dsc_filename]["version"]
762     if epochless_dsc_version != files[dsc_filename]["version"]:
763         reject("version ('%s') in .dsc does not match version ('%s') in .changes." % (epochless_dsc_version, changes_version))
764
765     # Ensure there is a .tar.gz in the .dsc file
766     has_tar = 0
767     for f in dsc_files.keys():
768         m = re_issource.match(f)
769         if not m:
770             reject("%s: %s in Files field not recognised as source." % (dsc_filename, f))
771             continue
772         ftype = m.group(3)
773         if ftype == "orig.tar.gz" or ftype == "tar.gz":
774             has_tar = 1
775     if not has_tar:
776         reject("%s: no .tar.gz or .orig.tar.gz in 'Files' field." % (dsc_filename))
777
778     # Ensure source is newer than existing source in target suites
779     reject(Upload.check_source_against_db(dsc_filename),"")
780
781     (reject_msg, is_in_incoming) = Upload.check_dsc_against_db(dsc_filename)
782     reject(reject_msg, "")
783     if is_in_incoming:
784         if not Options["No-Action"]:
785             copy_to_holding(is_in_incoming)
786         orig_tar_gz = os.path.basename(is_in_incoming)
787         files[orig_tar_gz] = {}
788         files[orig_tar_gz]["size"] = os.stat(orig_tar_gz)[stat.ST_SIZE]
789         files[orig_tar_gz]["md5sum"] = dsc_files[orig_tar_gz]["md5sum"]
790         files[orig_tar_gz]["sha1sum"] = dsc_files[orig_tar_gz]["sha1sum"]
791         files[orig_tar_gz]["sha256sum"] = dsc_files[orig_tar_gz]["sha256sum"]
792         files[orig_tar_gz]["section"] = files[dsc_filename]["section"]
793         files[orig_tar_gz]["priority"] = files[dsc_filename]["priority"]
794         files[orig_tar_gz]["component"] = files[dsc_filename]["component"]
795         files[orig_tar_gz]["type"] = "orig.tar.gz"
796         reprocess = 2
797
798     return 1
799
800 ################################################################################
801
802 def get_changelog_versions(source_dir):
803     """Extracts a the source package and (optionally) grabs the
804     version history out of debian/changelog for the BTS."""
805
806     # Find the .dsc (again)
807     dsc_filename = None
808     for f in files.keys():
809         if files[f]["type"] == "dsc":
810             dsc_filename = f
811
812     # If there isn't one, we have nothing to do. (We have reject()ed the upload already)
813     if not dsc_filename:
814         return
815
816     # Create a symlink mirror of the source files in our temporary directory
817     for f in files.keys():
818         m = re_issource.match(f)
819         if m:
820             src = os.path.join(source_dir, f)
821             # If a file is missing for whatever reason, give up.
822             if not os.path.exists(src):
823                 return
824             ftype = m.group(3)
825             if ftype == "orig.tar.gz" and pkg.orig_tar_gz:
826                 continue
827             dest = os.path.join(os.getcwd(), f)
828             os.symlink(src, dest)
829
830     # If the orig.tar.gz is not a part of the upload, create a symlink to the
831     # existing copy.
832     if pkg.orig_tar_gz:
833         dest = os.path.join(os.getcwd(), os.path.basename(pkg.orig_tar_gz))
834         os.symlink(pkg.orig_tar_gz, dest)
835
836     # Extract the source
837     cmd = "dpkg-source -sn -x %s" % (dsc_filename)
838     (result, output) = commands.getstatusoutput(cmd)
839     if (result != 0):
840         reject("'dpkg-source -x' failed for %s [return code: %s]." % (dsc_filename, result))
841         reject(utils.prefix_multi_line_string(output, " [dpkg-source output:] "), "")
842         return
843
844     if not Cnf.Find("Dir::Queue::BTSVersionTrack"):
845         return
846
847     # Get the upstream version
848     upstr_version = re_no_epoch.sub('', dsc["version"])
849     if re_strip_revision.search(upstr_version):
850         upstr_version = re_strip_revision.sub('', upstr_version)
851
852     # Ensure the changelog file exists
853     changelog_filename = "%s-%s/debian/changelog" % (dsc["source"], upstr_version)
854     if not os.path.exists(changelog_filename):
855         reject("%s: debian/changelog not found in extracted source." % (dsc_filename))
856         return
857
858     # Parse the changelog
859     dsc["bts changelog"] = ""
860     changelog_file = utils.open_file(changelog_filename)
861     for line in changelog_file.readlines():
862         m = re_changelog_versions.match(line)
863         if m:
864             dsc["bts changelog"] += line
865     changelog_file.close()
866
867     # Check we found at least one revision in the changelog
868     if not dsc["bts changelog"]:
869         reject("%s: changelog format not recognised (empty version tree)." % (dsc_filename))
870
871 ########################################
872
873 def check_source():
874     # Bail out if:
875     #    a) there's no source
876     # or b) reprocess is 2 - we will do this check next time when orig.tar.gz is in 'files'
877     # or c) the orig.tar.gz is MIA
878     if not changes["architecture"].has_key("source") or reprocess == 2 \
879        or pkg.orig_tar_gz == -1:
880         return
881
882     tmpdir = utils.temp_dirname()
883
884     # Move into the temporary directory
885     cwd = os.getcwd()
886     os.chdir(tmpdir)
887
888     # Get the changelog version history
889     get_changelog_versions(cwd)
890
891     # Move back and cleanup the temporary tree
892     os.chdir(cwd)
893     try:
894         shutil.rmtree(tmpdir)
895     except OSError, e:
896         if errno.errorcode[e.errno] != 'EACCES':
897             utils.fubar("%s: couldn't remove tmp dir for source tree." % (dsc["source"]))
898
899         reject("%s: source tree could not be cleanly removed." % (dsc["source"]))
900         # We probably have u-r or u-w directories so chmod everything
901         # and try again.
902         cmd = "chmod -R u+rwx %s" % (tmpdir)
903         result = os.system(cmd)
904         if result != 0:
905             utils.fubar("'%s' failed with result %s." % (cmd, result))
906         shutil.rmtree(tmpdir)
907     except:
908         utils.fubar("%s: couldn't remove tmp dir for source tree." % (dsc["source"]))
909
910 ################################################################################
911
912 # FIXME: should be a debian specific check called from a hook
913
914 def check_urgency ():
915     if changes["architecture"].has_key("source"):
916         if not changes.has_key("urgency"):
917             changes["urgency"] = Cnf["Urgency::Default"]
918         changes["urgency"] = changes["urgency"].lower()
919         if changes["urgency"] not in Cnf.ValueList("Urgency::Valid"):
920             reject("%s is not a valid urgency; it will be treated as %s by testing." % (changes["urgency"], Cnf["Urgency::Default"]), "Warning: ")
921             changes["urgency"] = Cnf["Urgency::Default"]
922
923 ################################################################################
924
925 def check_hashes ():
926     utils.check_hash(".changes", files, "md5", apt_pkg.md5sum)
927     utils.check_size(".changes", files)
928     utils.check_hash(".dsc", dsc_files, "md5", apt_pkg.md5sum)
929     utils.check_size(".dsc", dsc_files)
930
931     # This is stupid API, but it'll have to do for now until
932     # we actually have proper abstraction
933     for m in utils.ensure_hashes(changes, dsc, files, dsc_files):
934         reject(m)
935
936 ################################################################################
937
938 # Sanity check the time stamps of files inside debs.
939 # [Files in the near future cause ugly warnings and extreme time
940 #  travel can cause errors on extraction]
941
942 def check_timestamps():
943     class Tar:
944         def __init__(self, future_cutoff, past_cutoff):
945             self.reset()
946             self.future_cutoff = future_cutoff
947             self.past_cutoff = past_cutoff
948
949         def reset(self):
950             self.future_files = {}
951             self.ancient_files = {}
952
953         def callback(self, Kind,Name,Link,Mode,UID,GID,Size,MTime,Major,Minor):
954             if MTime > self.future_cutoff:
955                 self.future_files[Name] = MTime
956             if MTime < self.past_cutoff:
957                 self.ancient_files[Name] = MTime
958     ####
959
960     future_cutoff = time.time() + int(Cnf["Dinstall::FutureTimeTravelGrace"])
961     past_cutoff = time.mktime(time.strptime(Cnf["Dinstall::PastCutoffYear"],"%Y"))
962     tar = Tar(future_cutoff, past_cutoff)
963     for filename in files.keys():
964         if files[filename]["type"] == "deb":
965             tar.reset()
966             try:
967                 deb_file = utils.open_file(filename)
968                 apt_inst.debExtract(deb_file,tar.callback,"control.tar.gz")
969                 deb_file.seek(0)
970                 try:
971                     apt_inst.debExtract(deb_file,tar.callback,"data.tar.gz")
972                 except SystemError, e:
973                     # If we can't find a data.tar.gz, look for data.tar.bz2 instead.
974                     if not re.search(r"Cannot f[ui]nd chunk data.tar.gz$", str(e)):
975                         raise
976                     deb_file.seek(0)
977                     apt_inst.debExtract(deb_file,tar.callback,"data.tar.bz2")
978                 deb_file.close()
979                 #
980                 future_files = tar.future_files.keys()
981                 if future_files:
982                     num_future_files = len(future_files)
983                     future_file = future_files[0]
984                     future_date = tar.future_files[future_file]
985                     reject("%s: has %s file(s) with a time stamp too far into the future (e.g. %s [%s])."
986                            % (filename, num_future_files, future_file,
987                               time.ctime(future_date)))
988                 #
989                 ancient_files = tar.ancient_files.keys()
990                 if ancient_files:
991                     num_ancient_files = len(ancient_files)
992                     ancient_file = ancient_files[0]
993                     ancient_date = tar.ancient_files[ancient_file]
994                     reject("%s: has %s file(s) with a time stamp too ancient (e.g. %s [%s])."
995                            % (filename, num_ancient_files, ancient_file,
996                               time.ctime(ancient_date)))
997             except:
998                 reject("%s: deb contents timestamp check failed [%s: %s]" % (filename, sys.exc_type, sys.exc_value))
999
1000 ################################################################################
1001
1002 def lookup_uid_from_fingerprint(fpr):
1003     """
1004     Return the uid,name,isdm for a given gpg fingerprint
1005
1006     @type fpr: string
1007     @param fpr: a 40 byte GPG fingerprint
1008
1009     @return: (uid, name, isdm)
1010     """
1011     cursor = DBConn().cursor()
1012     cursor.execute( "SELECT u.uid, u.name, k.debian_maintainer FROM fingerprint f JOIN keyrings k ON (f.keyring=k.id), uid u WHERE f.uid = u.id AND f.fingerprint = '%s'" % (fpr))
1013     qs = cursor.fetchone()
1014     if qs:
1015         return qs
1016     else:
1017         return (None, None, False)
1018
1019 def check_signed_by_key():
1020     """Ensure the .changes is signed by an authorized uploader."""
1021
1022     (uid, uid_name, is_dm) = lookup_uid_from_fingerprint(changes["fingerprint"])
1023     if uid_name == None:
1024         uid_name = ""
1025
1026     # match claimed name with actual name:
1027     if uid is None:
1028         # This is fundamentally broken but need us to refactor how we get
1029         # the UIDs/Fingerprints in order for us to fix it properly
1030         uid, uid_email = changes["fingerprint"], uid
1031         may_nmu, may_sponsor = 1, 1
1032         # XXX by default new dds don't have a fingerprint/uid in the db atm,
1033         #     and can't get one in there if we don't allow nmu/sponsorship
1034     elif is_dm is False:
1035         # If is_dm is False, we allow full upload rights
1036         uid_email = "%s@debian.org" % (uid)
1037         may_nmu, may_sponsor = 1, 1
1038     else:
1039         # Assume limited upload rights unless we've discovered otherwise
1040         uid_email = uid
1041         may_nmu, may_sponsor = 0, 0
1042
1043
1044     if uid_email in [changes["maintaineremail"], changes["changedbyemail"]]:
1045         sponsored = 0
1046     elif uid_name in [changes["maintainername"], changes["changedbyname"]]:
1047         sponsored = 0
1048         if uid_name == "": sponsored = 1
1049     else:
1050         sponsored = 1
1051         if ("source" in changes["architecture"] and
1052             uid_email and utils.is_email_alias(uid_email)):
1053             sponsor_addresses = utils.gpg_get_key_addresses(changes["fingerprint"])
1054             if (changes["maintaineremail"] not in sponsor_addresses and
1055                 changes["changedbyemail"] not in sponsor_addresses):
1056                 changes["sponsoremail"] = uid_email
1057
1058     if sponsored and not may_sponsor:
1059         reject("%s is not authorised to sponsor uploads" % (uid))
1060
1061     cursor = DBConn().cursor()
1062     if not sponsored and not may_nmu:
1063         source_ids = []
1064         cursor.execute( "SELECT s.id, s.version FROM source s JOIN src_associations sa ON (s.id = sa.source) WHERE s.source = %(source)s AND s.dm_upload_allowed = 'yes'", changes )
1065
1066         highest_sid, highest_version = None, None
1067
1068         should_reject = True
1069         while True:
1070             si = cursor.fetchone()
1071             if not si:
1072                 break
1073
1074             if highest_version == None or apt_pkg.VersionCompare(si[1], highest_version) == 1:
1075                  highest_sid = si[0]
1076                  highest_version = si[1]
1077
1078         if highest_sid == None:
1079            reject("Source package %s does not have 'DM-Upload-Allowed: yes' in its most recent version" % changes["source"])
1080         else:
1081
1082             cursor.execute("SELECT m.name FROM maintainer m WHERE m.id IN (SELECT su.maintainer FROM src_uploaders su JOIN source s ON (s.id = su.source) WHERE su.source = %s)" % (highest_sid))
1083
1084             while True:
1085                 m = cursor.fetchone()
1086                 if not m:
1087                     break
1088
1089                 (rfc822, rfc2047, name, email) = utils.fix_maintainer(m[0])
1090                 if email == uid_email or name == uid_name:
1091                     should_reject=False
1092                     break
1093
1094         if should_reject == True:
1095             reject("%s is not in Maintainer or Uploaders of source package %s" % (uid, changes["source"]))
1096
1097         for b in changes["binary"].keys():
1098             for suite in changes["distribution"].keys():
1099                 suite_id = DBConn().get_suite_id(suite)
1100
1101                 cursor.execute("SELECT DISTINCT s.source FROM source s JOIN binaries b ON (s.id = b.source) JOIN bin_associations ba On (b.id = ba.bin) WHERE b.package = %(package)s AND ba.suite = %(suite)s" , {'package':b, 'suite':suite_id} )
1102                 while True:
1103                     s = cursor.fetchone()
1104                     if not s:
1105                         break
1106
1107                     if s[0] != changes["source"]:
1108                         reject("%s may not hijack %s from source package %s in suite %s" % (uid, b, s, suite))
1109
1110         for f in files.keys():
1111             if files[f].has_key("byhand"):
1112                 reject("%s may not upload BYHAND file %s" % (uid, f))
1113             if files[f].has_key("new"):
1114                 reject("%s may not upload NEW file %s" % (uid, f))
1115
1116
1117 ################################################################################
1118 ################################################################################
1119
1120 # If any file of an upload has a recent mtime then chances are good
1121 # the file is still being uploaded.
1122
1123 def upload_too_new():
1124     too_new = 0
1125     # Move back to the original directory to get accurate time stamps
1126     cwd = os.getcwd()
1127     os.chdir(pkg.directory)
1128     file_list = pkg.files.keys()
1129     file_list.extend(pkg.dsc_files.keys())
1130     file_list.append(pkg.changes_file)
1131     for f in file_list:
1132         try:
1133             last_modified = time.time()-os.path.getmtime(f)
1134             if last_modified < int(Cnf["Dinstall::SkipTime"]):
1135                 too_new = 1
1136                 break
1137         except:
1138             pass
1139     os.chdir(cwd)
1140     return too_new
1141
1142 ################################################################################
1143
1144 def action ():
1145     # changes["distribution"] may not exist in corner cases
1146     # (e.g. unreadable changes files)
1147     if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
1148         changes["distribution"] = {}
1149
1150     (summary, short_summary) = Upload.build_summaries()
1151
1152     # q-unapproved hax0ring
1153     queue_info = {
1154          "New": { "is": is_new, "process": acknowledge_new },
1155          "Autobyhand" : { "is" : is_autobyhand, "process": do_autobyhand },
1156          "Byhand" : { "is": is_byhand, "process": do_byhand },
1157          "OldStableUpdate" : { "is": is_oldstableupdate,
1158                                 "process": do_oldstableupdate },
1159          "StableUpdate" : { "is": is_stableupdate, "process": do_stableupdate },
1160          "Unembargo" : { "is": is_unembargo, "process": queue_unembargo },
1161          "Embargo" : { "is": is_embargo, "process": queue_embargo },
1162     }
1163     queues = [ "New", "Autobyhand", "Byhand" ]
1164     if Cnf.FindB("Dinstall::SecurityQueueHandling"):
1165         queues += [ "Unembargo", "Embargo" ]
1166     else:
1167         queues += [ "OldStableUpdate", "StableUpdate" ]
1168
1169     (prompt, answer) = ("", "XXX")
1170     if Options["No-Action"] or Options["Automatic"]:
1171         answer = 'S'
1172
1173     queuekey = ''
1174
1175     if reject_message.find("Rejected") != -1:
1176         if upload_too_new():
1177             print "SKIP (too new)\n" + reject_message,
1178             prompt = "[S]kip, Quit ?"
1179         else:
1180             print "REJECT\n" + reject_message,
1181             prompt = "[R]eject, Skip, Quit ?"
1182             if Options["Automatic"]:
1183                 answer = 'R'
1184     else:
1185         qu = None
1186         for q in queues:
1187             if queue_info[q]["is"]():
1188                 qu = q
1189                 break
1190         if qu:
1191             print "%s for %s\n%s%s" % (
1192                 qu.upper(), ", ".join(changes["distribution"].keys()),
1193                 reject_message, summary),
1194             queuekey = qu[0].upper()
1195             if queuekey in "RQSA":
1196                 queuekey = "D"
1197                 prompt = "[D]ivert, Skip, Quit ?"
1198             else:
1199                 prompt = "[%s]%s, Skip, Quit ?" % (queuekey, qu[1:].lower())
1200             if Options["Automatic"]:
1201                 answer = queuekey
1202         else:
1203             print "ACCEPT\n" + reject_message + summary,
1204             prompt = "[A]ccept, Skip, Quit ?"
1205             if Options["Automatic"]:
1206                 answer = 'A'
1207
1208     while prompt.find(answer) == -1:
1209         answer = utils.our_raw_input(prompt)
1210         m = re_default_answer.match(prompt)
1211         if answer == "":
1212             answer = m.group(1)
1213         answer = answer[:1].upper()
1214
1215     if answer == 'R':
1216         os.chdir (pkg.directory)
1217         Upload.do_reject(0, reject_message)
1218     elif answer == 'A':
1219         accept(summary, short_summary)
1220         remove_from_unchecked()
1221     elif answer == queuekey:
1222         queue_info[qu]["process"](summary, short_summary)
1223         remove_from_unchecked()
1224     elif answer == 'Q':
1225         sys.exit(0)
1226
1227 def remove_from_unchecked():
1228     os.chdir (pkg.directory)
1229     for f in files.keys():
1230         os.unlink(f)
1231     os.unlink(pkg.changes_file)
1232
1233 ################################################################################
1234
1235 def accept (summary, short_summary):
1236     Upload.accept(summary, short_summary)
1237     Upload.check_override()
1238
1239 ################################################################################
1240
1241 def move_to_dir (dest, perms=0660, changesperms=0664):
1242     utils.move (pkg.changes_file, dest, perms=changesperms)
1243     file_keys = files.keys()
1244     for f in file_keys:
1245         utils.move (f, dest, perms=perms)
1246
1247 ################################################################################
1248
1249 def is_unembargo ():
1250     cursor = DBConn().cursor()
1251     cursor.execute( "SELECT package FROM disembargo WHERE package = %(source)s AND version = %(version)s", changes )
1252     if cursor.fetchone():
1253         return 1
1254
1255     oldcwd = os.getcwd()
1256     os.chdir(Cnf["Dir::Queue::Disembargo"])
1257     disdir = os.getcwd()
1258     os.chdir(oldcwd)
1259
1260     if pkg.directory == disdir:
1261         if changes["architecture"].has_key("source"):
1262             if Options["No-Action"]: return 1
1263
1264             cursor.execute( "INSERT INTO disembargo (package, version) VALUES ('%(package)s', '%(version)s')",
1265                             changes )
1266             cursor.execute( "COMMIT" )
1267             return 1
1268
1269     return 0
1270
1271 def queue_unembargo (summary, short_summary):
1272     print "Moving to UNEMBARGOED holding area."
1273     Logger.log(["Moving to unembargoed", pkg.changes_file])
1274
1275     Upload.dump_vars(Cnf["Dir::Queue::Unembargoed"])
1276     move_to_dir(Cnf["Dir::Queue::Unembargoed"])
1277     Upload.queue_build("unembargoed", Cnf["Dir::Queue::Unembargoed"])
1278
1279     # Check for override disparities
1280     Upload.Subst["__SUMMARY__"] = summary
1281     Upload.check_override()
1282
1283     # Send accept mail, announce to lists, close bugs and check for
1284     # override disparities
1285     if not Cnf["Dinstall::Options::No-Mail"]:
1286         Upload.Subst["__SUITE__"] = ""
1287         mail_message = utils.TemplateSubst(Upload.Subst,Cnf["Dir::Templates"]+"/process-unchecked.accepted")
1288         utils.send_mail(mail_message)
1289         Upload.announce(short_summary, 1)
1290
1291 ################################################################################
1292
1293 def is_embargo ():
1294     # if embargoed queues are enabled always embargo
1295     return 1
1296
1297 def queue_embargo (summary, short_summary):
1298     print "Moving to EMBARGOED holding area."
1299     Logger.log(["Moving to embargoed", pkg.changes_file])
1300
1301     Upload.dump_vars(Cnf["Dir::Queue::Embargoed"])
1302     move_to_dir(Cnf["Dir::Queue::Embargoed"])
1303     Upload.queue_build("embargoed", Cnf["Dir::Queue::Embargoed"])
1304
1305     # Check for override disparities
1306     Upload.Subst["__SUMMARY__"] = summary
1307     Upload.check_override()
1308
1309     # Send accept mail, announce to lists, close bugs and check for
1310     # override disparities
1311     if not Cnf["Dinstall::Options::No-Mail"]:
1312         Upload.Subst["__SUITE__"] = ""
1313         mail_message = utils.TemplateSubst(Upload.Subst,Cnf["Dir::Templates"]+"/process-unchecked.accepted")
1314         utils.send_mail(mail_message)
1315         Upload.announce(short_summary, 1)
1316
1317 ################################################################################
1318
1319 def is_stableupdate ():
1320     if not changes["distribution"].has_key("proposed-updates"):
1321         return 0
1322
1323     if not changes["architecture"].has_key("source"):
1324         pusuite = DBConn().get_suite_id("proposed-updates")
1325         cursor = DBConn().cursor()
1326         cursor.execute( """SELECT 1 FROM source s
1327                            JOIN src_associations sa ON (s.id = sa.source)
1328                            WHERE s.source = %(source)s
1329                               AND s.version = %(version)s
1330                               AND sa.suite = %(suite)s""",
1331                         {'source' : changes['source'],
1332                          'version' : changes['version'],
1333                          'suite' : pusuite})
1334
1335         if cursor.fetchone():
1336             # source is already in proposed-updates so no need to hold
1337             return 0
1338
1339     return 1
1340
1341 def do_stableupdate (summary, short_summary):
1342     print "Moving to PROPOSED-UPDATES holding area."
1343     Logger.log(["Moving to proposed-updates", pkg.changes_file])
1344
1345     Upload.dump_vars(Cnf["Dir::Queue::ProposedUpdates"])
1346     move_to_dir(Cnf["Dir::Queue::ProposedUpdates"], perms=0664)
1347
1348     # Check for override disparities
1349     Upload.Subst["__SUMMARY__"] = summary
1350     Upload.check_override()
1351
1352 ################################################################################
1353
1354 def is_oldstableupdate ():
1355     if not changes["distribution"].has_key("oldstable-proposed-updates"):
1356         return 0
1357
1358     if not changes["architecture"].has_key("source"):
1359         pusuite = DBConn().get_suite_id("oldstable-proposed-updates")
1360         cursor = DBConn().cursor()
1361         cursor.execute( """SELECT 1 FROM source s
1362                            JOIN src_associations sa ON (s.id = sa.source)
1363                            WHERE s.source = %(source)s
1364                              AND s.version = %(version)s
1365                              AND sa.suite = %(suite)s""",
1366                         {'source' :  changes['source'],
1367                          'version' : changes['version'],
1368                          'suite' :   pusuite})
1369         if cursor.fetchone():
1370             return 0
1371
1372     return 1
1373
1374 def do_oldstableupdate (summary, short_summary):
1375     print "Moving to OLDSTABLE-PROPOSED-UPDATES holding area."
1376     Logger.log(["Moving to oldstable-proposed-updates", pkg.changes_file])
1377
1378     Upload.dump_vars(Cnf["Dir::Queue::OldProposedUpdates"])
1379     move_to_dir(Cnf["Dir::Queue::OldProposedUpdates"], perms=0664)
1380
1381     # Check for override disparities
1382     Upload.Subst["__SUMMARY__"] = summary
1383     Upload.check_override()
1384
1385 ################################################################################
1386
1387 def is_autobyhand ():
1388     all_auto = 1
1389     any_auto = 0
1390     for f in files.keys():
1391         if files[f].has_key("byhand"):
1392             any_auto = 1
1393
1394             # filename is of form "PKG_VER_ARCH.EXT" where PKG, VER and ARCH
1395             # don't contain underscores, and ARCH doesn't contain dots.
1396             # further VER matches the .changes Version:, and ARCH should be in
1397             # the .changes Architecture: list.
1398             if f.count("_") < 2:
1399                 all_auto = 0
1400                 continue
1401
1402             (pckg, ver, archext) = f.split("_", 2)
1403             if archext.count(".") < 1 or changes["version"] != ver:
1404                 all_auto = 0
1405                 continue
1406
1407             ABH = Cnf.SubTree("AutomaticByHandPackages")
1408             if not ABH.has_key(pckg) or \
1409               ABH["%s::Source" % (pckg)] != changes["source"]:
1410                 print "not match %s %s" % (pckg, changes["source"])
1411                 all_auto = 0
1412                 continue
1413
1414             (arch, ext) = archext.split(".", 1)
1415             if arch not in changes["architecture"]:
1416                 all_auto = 0
1417                 continue
1418
1419             files[f]["byhand-arch"] = arch
1420             files[f]["byhand-script"] = ABH["%s::Script" % (pckg)]
1421
1422     return any_auto and all_auto
1423
1424 def do_autobyhand (summary, short_summary):
1425     print "Attempting AUTOBYHAND."
1426     byhandleft = 0
1427     for f in files.keys():
1428         byhandfile = f
1429         if not files[f].has_key("byhand"):
1430             continue
1431         if not files[f].has_key("byhand-script"):
1432             byhandleft = 1
1433             continue
1434
1435         os.system("ls -l %s" % byhandfile)
1436         result = os.system("%s %s %s %s %s" % (
1437                 files[f]["byhand-script"], byhandfile,
1438                 changes["version"], files[f]["byhand-arch"],
1439                 os.path.abspath(pkg.changes_file)))
1440         if result == 0:
1441             os.unlink(byhandfile)
1442             del files[f]
1443         else:
1444             print "Error processing %s, left as byhand." % (f)
1445             byhandleft = 1
1446
1447     if byhandleft:
1448         do_byhand(summary, short_summary)
1449     else:
1450         accept(summary, short_summary)
1451
1452 ################################################################################
1453
1454 def is_byhand ():
1455     for f in files.keys():
1456         if files[f].has_key("byhand"):
1457             return 1
1458     return 0
1459
1460 def do_byhand (summary, short_summary):
1461     print "Moving to BYHAND holding area."
1462     Logger.log(["Moving to byhand", pkg.changes_file])
1463
1464     Upload.dump_vars(Cnf["Dir::Queue::Byhand"])
1465     move_to_dir(Cnf["Dir::Queue::Byhand"])
1466
1467     # Check for override disparities
1468     Upload.Subst["__SUMMARY__"] = summary
1469     Upload.check_override()
1470
1471 ################################################################################
1472
1473 def is_new ():
1474     for f in files.keys():
1475         if files[f].has_key("new"):
1476             return 1
1477     return 0
1478
1479 def acknowledge_new (summary, short_summary):
1480     Subst = Upload.Subst
1481
1482     print "Moving to NEW holding area."
1483     Logger.log(["Moving to new", pkg.changes_file])
1484
1485     Upload.dump_vars(Cnf["Dir::Queue::New"])
1486     move_to_dir(Cnf["Dir::Queue::New"])
1487
1488     if not Options["No-Mail"]:
1489         print "Sending new ack."
1490         Subst["__SUMMARY__"] = summary
1491         new_ack_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/process-unchecked.new")
1492         utils.send_mail(new_ack_message)
1493
1494 ################################################################################
1495
1496 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1497 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1498 # Upload.check_dsc_against_db() can find the .orig.tar.gz but it will
1499 # not have processed it during it's checks of -2.  If -1 has been
1500 # deleted or otherwise not checked by 'dak process-unchecked', the
1501 # .orig.tar.gz will not have been checked at all.  To get round this,
1502 # we force the .orig.tar.gz into the .changes structure and reprocess
1503 # the .changes file.
1504
1505 def process_it (changes_file):
1506     global reprocess, reject_message
1507
1508     # Reset some globals
1509     reprocess = 1
1510     Upload.init_vars()
1511     # Some defaults in case we can't fully process the .changes file
1512     changes["maintainer2047"] = Cnf["Dinstall::MyEmailAddress"]
1513     changes["changedby2047"] = Cnf["Dinstall::MyEmailAddress"]
1514     reject_message = ""
1515
1516     # Absolutize the filename to avoid the requirement of being in the
1517     # same directory as the .changes file.
1518     pkg.changes_file = os.path.abspath(changes_file)
1519
1520     # Remember where we are so we can come back after cd-ing into the
1521     # holding directory.
1522     pkg.directory = os.getcwd()
1523
1524     try:
1525         # If this is the Real Thing(tm), copy things into a private
1526         # holding directory first to avoid replacable file races.
1527         if not Options["No-Action"]:
1528             os.chdir(Cnf["Dir::Queue::Holding"])
1529             copy_to_holding(pkg.changes_file)
1530             # Relativize the filename so we use the copy in holding
1531             # rather than the original...
1532             pkg.changes_file = os.path.basename(pkg.changes_file)
1533         changes["fingerprint"] = utils.check_signature(pkg.changes_file, reject)
1534         if changes["fingerprint"]:
1535             valid_changes_p = check_changes()
1536         else:
1537             valid_changes_p = 0
1538         if valid_changes_p:
1539             while reprocess:
1540                 check_distributions()
1541                 check_files()
1542                 valid_dsc_p = check_dsc()
1543                 if valid_dsc_p:
1544                     check_source()
1545                 check_hashes()
1546                 check_urgency()
1547                 check_timestamps()
1548                 check_signed_by_key()
1549         Upload.update_subst(reject_message)
1550         action()
1551     except SystemExit:
1552         raise
1553     except:
1554         print "ERROR"
1555         traceback.print_exc(file=sys.stderr)
1556         pass
1557
1558     # Restore previous WD
1559     os.chdir(pkg.directory)
1560
1561 ###############################################################################
1562
1563 def main():
1564     global Cnf, Options, Logger
1565
1566     changes_files = init()
1567
1568     # -n/--dry-run invalidates some other options which would involve things happening
1569     if Options["No-Action"]:
1570         Options["Automatic"] = ""
1571
1572     # Ensure all the arguments we were given are .changes files
1573     for f in changes_files:
1574         if not f.endswith(".changes"):
1575             utils.warn("Ignoring '%s' because it's not a .changes file." % (f))
1576             changes_files.remove(f)
1577
1578     if changes_files == []:
1579         utils.fubar("Need at least one .changes file as an argument.")
1580
1581     # Check that we aren't going to clash with the daily cron job
1582
1583     if not Options["No-Action"] and os.path.exists("%s/daily.lock" % (Cnf["Dir::Lock"])) and not Options["No-Lock"]:
1584         utils.fubar("Archive maintenance in progress.  Try again later.")
1585
1586     # Obtain lock if not in no-action mode and initialize the log
1587
1588     if not Options["No-Action"]:
1589         lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR | os.O_CREAT)
1590         try:
1591             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1592         except IOError, e:
1593             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EAGAIN':
1594                 utils.fubar("Couldn't obtain lock; assuming another 'dak process-unchecked' is already running.")
1595             else:
1596                 raise
1597         Logger = Upload.Logger = logging.Logger(Cnf, "process-unchecked")
1598
1599     # debian-{devel-,}-changes@lists.debian.org toggles writes access based on this header
1600     bcc = "X-DAK: dak process-unchecked\nX-Katie: $Revision: 1.65 $"
1601     if Cnf.has_key("Dinstall::Bcc"):
1602         Upload.Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"])
1603     else:
1604         Upload.Subst["__BCC__"] = bcc
1605
1606
1607     # Sort the .changes files so that we process sourceful ones first
1608     changes_files.sort(utils.changes_compare)
1609
1610     # Process the changes files
1611     for changes_file in changes_files:
1612         print "\n" + changes_file
1613         try:
1614             process_it (changes_file)
1615         finally:
1616             if not Options["No-Action"]:
1617                 clean_holding()
1618
1619     accept_count = Upload.accept_count
1620     accept_bytes = Upload.accept_bytes
1621     if accept_count:
1622         sets = "set"
1623         if accept_count > 1:
1624             sets = "sets"
1625         print "Accepted %d package %s, %s." % (accept_count, sets, utils.size_type(int(accept_bytes)))
1626         Logger.log(["total",accept_count,accept_bytes])
1627
1628     if not Options["No-Action"]:
1629         Logger.close()
1630
1631 ################################################################################
1632
1633 if __name__ == '__main__':
1634     main()