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