Coverage for daklib/checks.py: 70%

664 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-08-03 16:46 +0000

1# Copyright (C) 2012, Ansgar Burchardt <ansgar@debian.org> 

2# 

3# Parts based on code that is 

4# Copyright (C) 2001-2006, James Troup <james@nocrew.org> 

5# Copyright (C) 2009-2010, Joerg Jaspert <joerg@debian.org> 

6# 

7# This program is free software; you can redistribute it and/or modify 

8# it under the terms of the GNU General Public License as published by 

9# the Free Software Foundation; either version 2 of the License, or 

10# (at your option) any later version. 

11# 

12# This program is distributed in the hope that it will be useful, 

13# but WITHOUT ANY WARRANTY; without even the implied warranty of 

14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

15# GNU General Public License for more details. 

16# 

17# You should have received a copy of the GNU General Public License along 

18# with this program; if not, write to the Free Software Foundation, Inc., 

19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 

20 

21"""module provided pre-acceptance tests 

22 

23Please read the documentation for the :class:`Check` class for the interface. 

24""" 

25 

26import datetime 

27import os 

28import re 

29import subprocess 

30import tempfile 

31import textwrap 

32import time 

33from collections.abc import Callable, Iterable 

34from typing import TYPE_CHECKING, Literal, cast, override 

35 

36import apt_inst 

37import apt_pkg 

38import yaml 

39from apt_pkg import version_compare 

40from sqlalchemy import select, sql 

41 

42import daklib.gpg 

43import daklib.upload 

44from daklib import dbconn, lintian, utils 

45from daklib.config import Config 

46from daklib.dbconn import ( 

47 ACL, 

48 ACLPerSource, 

49 Architecture, 

50 DBBinary, 

51 DBSource, 

52 SignatureHistory, 

53 SrcFormat, 

54 Suite, 

55 get_source_in_suite, 

56) 

57from daklib.regexes import ( 

58 re_field_package, 

59 re_field_source, 

60 re_field_version, 

61 re_field_version_upstream, 

62 re_file_binary, 

63 re_file_changes, 

64 re_file_dsc, 

65 re_file_orig, 

66 re_file_source, 

67 re_isanum, 

68) 

69from daklib.textutils import ParseMaintError, fix_maintainer 

70 

71if TYPE_CHECKING: 

72 from sqlalchemy.orm import Session 

73 

74 import daklib.archive 

75 

76 

77def check_fields_for_valid_utf8(filename: str, control: apt_pkg.TagSection) -> None: 

78 """Check all fields of a control file for valid UTF-8""" 

79 for field in control.keys(): 

80 try: 

81 # Access the field value to make `TagSection` try to decode it. 

82 # We should also do the same for the field name, but this requires 

83 # https://bugs.debian.org/995118 to be fixed. 

84 # TODO: make sure the field name `field` is valid UTF-8 too 

85 control[field] 

86 except UnicodeDecodeError: 

87 raise Reject( 

88 "{0}: The {1} field is not valid UTF-8".format(filename, field) 

89 ) 

90 

91 

92class Reject(Exception): 

93 """exception raised by failing checks""" 

94 

95 

96class RejectExternalFilesMismatch(Reject): 

97 """exception raised by failing the external hashes check""" 

98 

99 @override 

100 def __str__(self): 

101 return ( 

102 "'%s' has mismatching %s from the external files db ('%s' [current] vs '%s' [external])" 

103 % self.args[:4] 

104 ) 

105 

106 

107class RejectACL(Reject): 

108 """exception raise by failing ACL checks""" 

109 

110 def __init__(self, acl: ACL, reason: str): 

111 self.acl = acl 

112 self.reason = reason 

113 

114 @override 

115 def __str__(self): 

116 return "ACL {0}: {1}".format(self.acl.name, self.reason) 

117 

118 

119class Check: 

120 """base class for checks 

121 

122 checks are called by :class:`daklib.archive.ArchiveUpload`. Failing tests should 

123 raise a :exc:`daklib.checks.Reject` exception including a human-readable 

124 description why the upload should be rejected. 

125 """ 

126 

127 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

128 """do checks 

129 

130 :param upload: upload to check 

131 

132 :raises Reject: upload should be rejected 

133 """ 

134 raise NotImplementedError 

135 

136 def per_suite_check( 

137 self, upload: "daklib.archive.ArchiveUpload", suite: Suite 

138 ) -> bool: 

139 """do per-suite checks 

140 

141 :param upload: upload to check 

142 :param suite: suite to check 

143 

144 :raises Reject: upload should be rejected 

145 """ 

146 raise NotImplementedError 

147 

148 @property 

149 def forcable(self) -> bool: 

150 """allow to force ignore failing test 

151 

152 :const:`True` if it is acceptable to force ignoring a failing test, 

153 :const:`False` otherwise 

154 """ 

155 return False 

156 

157 

158class SignatureAndHashesCheck(Check): 

159 """Check signature of changes and dsc file (if included in upload) 

160 

161 Make sure the signature is valid and done by a known user. 

162 """ 

163 

164 def check_replay(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

165 # Use private session as we want to remember having seen the .changes 

166 # in all cases. 

167 session = upload.session 

168 history = SignatureHistory.from_signed_file(upload.changes.signature) 

169 r = history.query(session) 

170 if r is not None: 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true

171 raise Reject( 

172 "Signature for changes file was already seen at {0}.\nPlease refresh the signature of the changes file if you want to upload it again.".format( 

173 r.seen 

174 ) 

175 ) 

176 return True 

177 

178 @override 

179 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

180 allow_source_untrusted_sig_keys = Config().value_list( 

181 "Dinstall::AllowSourceUntrustedSigKeys" 

182 ) 

183 

184 changes = upload.changes 

185 if not changes.valid_signature: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true

186 raise Reject("Signature for .changes not valid.") 

187 self.check_replay(upload) 

188 self._check_hashes(upload, changes.filename, changes.files.values()) 

189 

190 source = None 

191 try: 

192 source = changes.source 

193 except Exception as e: 

194 raise Reject("Invalid dsc file: {0}".format(e)) 

195 if source is not None: 

196 if changes.primary_fingerprint not in allow_source_untrusted_sig_keys: 196 ↛ 201line 196 didn't jump to line 201 because the condition on line 196 was always true

197 if not source.valid_signature: 197 ↛ 198line 197 didn't jump to line 198 because the condition on line 197 was never true

198 raise Reject("Signature for .dsc not valid.") 

199 if source.primary_fingerprint != changes.primary_fingerprint: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true

200 raise Reject(".changes and .dsc not signed by the same key.") 

201 self._check_hashes(upload, source.filename, source.files.values()) 

202 

203 if upload.fingerprint is None or upload.fingerprint.uid is None: 

204 raise Reject(".changes signed by unknown key.") 

205 

206 return True 

207 

208 def _check_hashes( 

209 self, 

210 upload: "daklib.archive.ArchiveUpload", 

211 filename: str, 

212 files: Iterable[daklib.upload.HashedFile], 

213 ) -> None: 

214 """Make sure hashes match existing files 

215 

216 :param upload: upload we are processing 

217 :param filename: name of the file the expected hash values are taken from 

218 :param files: files to check the hashes for 

219 """ 

220 try: 

221 for f in files: 

222 f.check(upload.directory) 

223 except daklib.upload.FileDoesNotExist as e: 223 ↛ 231line 223 didn't jump to line 231

224 raise Reject( 

225 "{0}: {1}\n" 

226 "Perhaps you need to include the file in your upload?\n\n" 

227 "If the orig tarball is missing, the -sa flag for dpkg-buildpackage will be your friend.".format( 

228 filename, str(e) 

229 ) 

230 ) 

231 except daklib.upload.UploadException as e: 

232 raise Reject("{0}: {1}".format(filename, str(e))) 

233 

234 

235class WeakSignatureCheck(Check): 

236 """Check that .changes and .dsc are not signed using a weak algorithm""" 

237 

238 @override 

239 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

240 changes = upload.changes 

241 if changes.weak_signature: 241 ↛ 242line 241 didn't jump to line 242 because the condition on line 241 was never true

242 raise Reject( 

243 "The .changes was signed using a weak algorithm (such as SHA-1)" 

244 ) 

245 

246 source = changes.source 

247 if source is not None and source.weak_signature: 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true

248 raise Reject( 

249 "The source package was signed using a weak algorithm (such as SHA-1)" 

250 ) 

251 

252 return True 

253 

254 

255def check_signature_timestamp(prefix: str, signed_file: daklib.gpg.SignedFile) -> bool: 

256 now = datetime.datetime.now(datetime.UTC) 

257 timestamp = signed_file.signature_timestamp 

258 age = now - timestamp 

259 

260 age_max = datetime.timedelta(days=365) 

261 age_min = datetime.timedelta(days=-7) 

262 

263 if age > age_max: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true

264 raise Reject( 

265 "{0}: Signature from {1} is too old (maximum age is {2} days)".format( 

266 prefix, timestamp, age_max.days 

267 ) 

268 ) 

269 if age < age_min: 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true

270 raise Reject( 

271 "{0}: Signature from {1} is too far in the future (tolerance is {2} days)".format( 

272 prefix, timestamp, abs(age_min.days) 

273 ) 

274 ) 

275 return True 

276 

277 

278class SignatureTimestampCheck(Check): 

279 """Check timestamp of .changes signature""" 

280 

281 @override 

282 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

283 return check_signature_timestamp( 

284 upload.changes.filename, upload.changes.signature 

285 ) 

286 

287 

288class ChangesCheck(Check): 

289 """Check changes file for syntax errors.""" 

290 

291 @override 

292 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

293 changes = upload.changes 

294 control = changes.changes 

295 fn = changes.filename 

296 

297 for field in ( 

298 "Distribution", 

299 "Source", 

300 "Architecture", 

301 "Version", 

302 "Maintainer", 

303 "Files", 

304 "Changes", 

305 ): 

306 if field not in control: 306 ↛ 307line 306 didn't jump to line 307 because the condition on line 306 was never true

307 raise Reject("{0}: misses mandatory field {1}".format(fn, field)) 

308 

309 if len(changes.binaries) > 0: 

310 for field in ("Binary", "Description"): 

311 if field not in control: 311 ↛ 312line 311 didn't jump to line 312 because the condition on line 311 was never true

312 raise Reject( 

313 "{0}: binary upload requires {1} field".format(fn, field) 

314 ) 

315 

316 check_fields_for_valid_utf8(fn, control) 

317 

318 source_match = re_field_source.match(control["Source"]) 

319 if not source_match: 319 ↛ 320line 319 didn't jump to line 320 because the condition on line 319 was never true

320 raise Reject("{0}: Invalid Source field".format(fn)) 

321 version_match = re_field_version.match(control["Version"]) 

322 if not version_match: 322 ↛ 323line 322 didn't jump to line 323 because the condition on line 322 was never true

323 raise Reject("{0}: Invalid Version field".format(fn)) 

324 version_without_epoch = version_match.group("without_epoch") 

325 

326 match = re_file_changes.match(fn) 

327 if not match: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true

328 raise Reject("{0}: Does not match re_file_changes".format(fn)) 

329 if match.group("package") != source_match.group("package"): 329 ↛ 330line 329 didn't jump to line 330 because the condition on line 329 was never true

330 raise Reject("{0}: Filename does not match Source field".format(fn)) 

331 if match.group("version") != version_without_epoch: 331 ↛ 332line 331 didn't jump to line 332 because the condition on line 331 was never true

332 raise Reject("{0}: Filename does not match Version field".format(fn)) 

333 

334 for bn in changes.binary_names: 

335 if not re_field_package.match(bn): 335 ↛ 336line 335 didn't jump to line 336 because the condition on line 335 was never true

336 raise Reject("{0}: Invalid binary package name {1}".format(fn, bn)) 

337 

338 if changes.sourceful and changes.source is None: 338 ↛ 339line 338 didn't jump to line 339 because the condition on line 338 was never true

339 raise Reject("Changes has architecture source, but no source found.") 

340 if changes.source is not None and not changes.sourceful: 340 ↛ 341line 340 didn't jump to line 341 because the condition on line 340 was never true

341 raise Reject("Upload includes source, but changes does not say so.") 

342 

343 try: 

344 fix_maintainer(changes.changes["Maintainer"]) 

345 except ParseMaintError as e: 

346 raise Reject( 

347 "{0}: Failed to parse Maintainer field: {1}".format(changes.filename, e) 

348 ) 

349 

350 try: 

351 changed_by = changes.changes.get("Changed-By") 

352 if changed_by is not None: 352 ↛ 359line 352 didn't jump to line 359 because the condition on line 352 was always true

353 fix_maintainer(changed_by) 

354 except ParseMaintError as e: 

355 raise Reject( 

356 "{0}: Failed to parse Changed-By field: {1}".format(changes.filename, e) 

357 ) 

358 

359 try: 

360 changes.byhand_files 

361 except daklib.upload.InvalidChangesException as e: 

362 raise Reject("{0}".format(e)) 

363 

364 if len(changes.files) == 0: 364 ↛ 365line 364 didn't jump to line 365 because the condition on line 364 was never true

365 raise Reject("Changes includes no files.") 

366 

367 for bugnum in changes.closed_bugs: 367 ↛ 368line 367 didn't jump to line 368 because the loop on line 367 never started

368 if not re_isanum.match(bugnum): 

369 raise Reject( 

370 '{0}: "{1}" in Closes field is not a number'.format( 

371 changes.filename, bugnum 

372 ) 

373 ) 

374 

375 return True 

376 

377 

378class ExternalHashesCheck(Check): 

379 """Checks hashes in .changes and .dsc against an external database.""" 

380 

381 def check_single(self, session: "Session", f): 

382 q = session.execute( 

383 sql.text( 

384 "SELECT size, md5sum, sha1sum, sha256sum FROM external_files WHERE filename LIKE :pattern" 

385 ), 

386 {"pattern": "%/{}".format(f.filename)}, 

387 ) 

388 (ext_size, ext_md5sum, ext_sha1sum, ext_sha256sum) = q.fetchone() or ( 

389 None, 

390 None, 

391 None, 

392 None, 

393 ) 

394 

395 if not ext_size: 

396 return 

397 

398 if ext_size != f.size: 

399 raise RejectExternalFilesMismatch(f.filename, "size", f.size, ext_size) 

400 

401 if ext_md5sum != f.md5sum: 

402 raise RejectExternalFilesMismatch( 

403 f.filename, "md5sum", f.md5sum, ext_md5sum 

404 ) 

405 

406 if ext_sha1sum != f.sha1sum: 

407 raise RejectExternalFilesMismatch( 

408 f.filename, "sha1sum", f.sha1sum, ext_sha1sum 

409 ) 

410 

411 if ext_sha256sum != f.sha256sum: 

412 raise RejectExternalFilesMismatch( 

413 f.filename, "sha256sum", f.sha256sum, ext_sha256sum 

414 ) 

415 

416 @override 

417 def check(self, upload: "daklib.archive.ArchiveUpload"): 

418 cnf = Config() 

419 

420 if not cnf.use_extfiles: # type: ignore[attr-defined] 420 ↛ 423line 420 didn't jump to line 423 because the condition on line 420 was always true

421 return 

422 

423 session = upload.session 

424 changes = upload.changes 

425 

426 for f in changes.files.values(): 

427 self.check_single(session, f) 

428 source = changes.source 

429 if source is not None: 

430 for f in source.files.values(): 

431 self.check_single(session, f) 

432 

433 

434class BinaryCheck(Check): 

435 """Check binary packages for syntax errors.""" 

436 

437 @override 

438 def check(self, upload): 

439 debug_deb_name_postfix = "-dbgsym" 

440 # XXX: Handle dynamic debug section name here 

441 

442 self._architectures: set[str] = set() 

443 

444 for binary in upload.changes.binaries: 

445 self.check_binary(upload, binary) 

446 

447 for arch in upload.changes.architectures: 

448 if arch == "source": 

449 continue 

450 if arch not in self._architectures: 450 ↛ 451line 450 didn't jump to line 451 because the condition on line 450 was never true

451 raise Reject( 

452 "{}: Architecture field includes {}, but no binary packages for {} are included in the upload".format( 

453 upload.changes.filename, arch, arch 

454 ) 

455 ) 

456 

457 binaries = { 

458 binary.control["Package"]: binary for binary in upload.changes.binaries 

459 } 

460 

461 for name, binary in list(binaries.items()): 

462 if name in upload.changes.binary_names: 462 ↛ 465line 462 didn't jump to line 465 because the condition on line 462 was always true

463 # Package is listed in Binary field. Everything is good. 

464 pass 

465 elif daklib.utils.is_in_debug_section(binary.control): 

466 # If we have a binary package in the debug section, we 

467 # can allow it to not be present in the Binary field 

468 # in the .changes file, so long as its name (without 

469 # -dbgsym) is present in the Binary list. 

470 if not name.endswith(debug_deb_name_postfix): 

471 raise Reject( 

472 "Package {0} is in the debug section, but " 

473 "does not end in {1}.".format(name, debug_deb_name_postfix) 

474 ) 

475 

476 # Right, so, it's named properly, let's check that 

477 # the corresponding package is in the Binary list 

478 origin_package_name = name[: -len(debug_deb_name_postfix)] 

479 if origin_package_name not in upload.changes.binary_names: 

480 raise Reject( 

481 "Debug package {debug}'s corresponding binary package " 

482 "{origin} is not present in the Binary field.".format( 

483 debug=name, origin=origin_package_name 

484 ) 

485 ) 

486 else: 

487 # Someone was a nasty little hacker and put a package 

488 # into the .changes that isn't in debian/control. Bad, 

489 # bad person. 

490 raise Reject( 

491 "Package {0} is not mentioned in Binary field in changes".format( 

492 name 

493 ) 

494 ) 

495 

496 return True 

497 

498 def check_binary(self, upload: "daklib.archive.ArchiveUpload", binary): 

499 fn = binary.hashed_file.filename 

500 control = binary.control 

501 

502 for field in ("Package", "Architecture", "Version", "Description", "Section"): 

503 if field not in control: 503 ↛ 504line 503 didn't jump to line 504 because the condition on line 503 was never true

504 raise Reject("{0}: Missing mandatory field {1}.".format(fn, field)) 

505 

506 check_fields_for_valid_utf8(fn, control) 

507 

508 # check fields 

509 

510 package = control["Package"] 

511 if not re_field_package.match(package): 511 ↛ 512line 511 didn't jump to line 512 because the condition on line 511 was never true

512 raise Reject("{0}: Invalid Package field".format(fn)) 

513 

514 version = control["Version"] 

515 version_match = re_field_version.match(version) 

516 if not version_match: 516 ↛ 517line 516 didn't jump to line 517 because the condition on line 516 was never true

517 raise Reject("{0}: Invalid Version field".format(fn)) 

518 version_without_epoch = version_match.group("without_epoch") 

519 

520 architecture = control["Architecture"] 

521 if architecture not in upload.changes.architectures: 521 ↛ 522line 521 didn't jump to line 522 because the condition on line 521 was never true

522 raise Reject( 

523 "{0}: Architecture not in Architecture field in changes file".format(fn) 

524 ) 

525 if architecture == "source": 525 ↛ 526line 525 didn't jump to line 526 because the condition on line 525 was never true

526 raise Reject( 

527 '{0}: Architecture "source" invalid for binary packages'.format(fn) 

528 ) 

529 self._architectures.add(architecture) 

530 

531 source = control.get("Source") 

532 if source is not None and not re_field_source.match(source): 532 ↛ 533line 532 didn't jump to line 533 because the condition on line 532 was never true

533 raise Reject("{0}: Invalid Source field".format(fn)) 

534 

535 section = control.get("Section", "") 

536 if section == "" or section == "unknown" or section.endswith("/unknown"): 536 ↛ 537line 536 didn't jump to line 537 because the condition on line 536 was never true

537 raise Reject( 

538 '{0}: The "Section" field must be present and use a real section name.'.format( 

539 fn 

540 ) 

541 ) 

542 

543 # check filename 

544 

545 match = re_file_binary.match(fn) 

546 if match is None: 546 ↛ 547line 546 didn't jump to line 547 because the condition on line 546 was never true

547 raise Reject(f"{fn}: does not match re_file_binary") 

548 if package != match.group("package"): 548 ↛ 549line 548 didn't jump to line 549 because the condition on line 548 was never true

549 raise Reject("{0}: filename does not match Package field".format(fn)) 

550 if version_without_epoch != match.group("version"): 550 ↛ 551line 550 didn't jump to line 551 because the condition on line 550 was never true

551 raise Reject("{0}: filename does not match Version field".format(fn)) 

552 if architecture != match.group("architecture"): 552 ↛ 553line 552 didn't jump to line 553 because the condition on line 552 was never true

553 raise Reject("{0}: filename does not match Architecture field".format(fn)) 

554 

555 # check dependency field syntax 

556 

557 def check_dependency_field( 

558 field, 

559 control, 

560 dependency_parser=apt_pkg.parse_depends, 

561 allow_alternatives=True, 

562 allow_relations=("", "<", "<=", "=", ">=", ">"), 

563 ): 

564 value = control.get(field) 

565 if value is not None: 

566 if value.strip() == "": 566 ↛ 567line 566 didn't jump to line 567 because the condition on line 566 was never true

567 raise Reject("{0}: empty {1} field".format(fn, field)) 

568 try: 

569 depends = dependency_parser(value) 

570 except: 

571 raise Reject("{0}: APT could not parse {1} field".format(fn, field)) 

572 for group in depends: 

573 if not allow_alternatives and len(group) != 1: 573 ↛ 574line 573 didn't jump to line 574 because the condition on line 573 was never true

574 raise Reject( 

575 "{0}: {1}: alternatives are not allowed".format(fn, field) 

576 ) 

577 for dep_pkg, dep_ver, dep_rel in group: 

578 if dep_rel not in allow_relations: 578 ↛ 579line 578 didn't jump to line 579 because the condition on line 578 was never true

579 raise Reject( 

580 "{}: {}: depends on {}, but only relations {} are allowed for this field".format( 

581 fn, 

582 field, 

583 " ".join([dep_pkg, dep_rel, dep_ver]), 

584 allow_relations, 

585 ) 

586 ) 

587 

588 for field in ( 

589 "Breaks", 

590 "Conflicts", 

591 "Depends", 

592 "Enhances", 

593 "Pre-Depends", 

594 "Recommends", 

595 "Replaces", 

596 "Suggests", 

597 ): 

598 check_dependency_field(field, control) 

599 

600 check_dependency_field( 

601 "Provides", control, allow_alternatives=False, allow_relations=("", "=") 

602 ) 

603 check_dependency_field( 

604 "Built-Using", 

605 control, 

606 dependency_parser=apt_pkg.parse_src_depends, 

607 allow_alternatives=False, 

608 allow_relations=("=",), 

609 ) 

610 

611 

612_DEB_ALLOWED_MEMBERS = { 

613 "debian-binary", 

614 *(f"control.tar.{comp}" for comp in ("gz", "xz")), 

615 *(f"data.tar.{comp}" for comp in ("gz", "bz2", "xz")), 

616} 

617 

618 

619class BinaryMembersCheck(Check): 

620 """check members of .deb file""" 

621 

622 @override 

623 def check(self, upload: "daklib.archive.ArchiveUpload"): 

624 for binary in upload.changes.binaries: 

625 filename = binary.hashed_file.filename 

626 path = os.path.join(upload.directory, filename) 

627 self._check_binary(filename, path) 

628 return True 

629 

630 def _check_binary(self, filename: str, path: str) -> None: 

631 deb = apt_inst.DebFile(path) 

632 members = {member.name for member in deb.getmembers()} # type: ignore[attr-defined] 

633 if blocked_members := members - _DEB_ALLOWED_MEMBERS: 633 ↛ 634line 633 didn't jump to line 634 because the condition on line 633 was never true

634 raise Reject( 

635 f"{filename}: Contains blocked members {', '.join(blocked_members)}" 

636 ) 

637 

638 

639class BinaryTimestampCheck(Check): 

640 """check timestamps of files in binary packages 

641 

642 Files in the near future cause ugly warnings and extreme time travel 

643 can cause errors on extraction. 

644 """ 

645 

646 @override 

647 def check(self, upload: "daklib.archive.ArchiveUpload"): 

648 cnf = Config() 

649 future_cutoff = time.time() + cnf.find_i( 

650 "Dinstall::FutureTimeTravelGrace", 24 * 3600 

651 ) 

652 past_cutoff = time.mktime( 

653 time.strptime(cnf.find("Dinstall::PastCutoffYear", "1975"), "%Y") 

654 ) 

655 

656 class TarTime: 

657 def __init__(self): 

658 self.future_files: dict[str, int] = {} 

659 self.past_files: dict[str, int] = {} 

660 

661 def callback(self, member, data) -> None: 

662 if member.mtime > future_cutoff: 662 ↛ 663line 662 didn't jump to line 663 because the condition on line 662 was never true

663 self.future_files[member.name] = member.mtime 

664 elif member.mtime < past_cutoff: 664 ↛ 665line 664 didn't jump to line 665 because the condition on line 664 was never true

665 self.past_files[member.name] = member.mtime 

666 

667 def format_reason(filename, direction, files) -> str: 

668 reason = ( 

669 "{0}: has {1} file(s) with a timestamp too far in the {2}:\n".format( 

670 filename, len(files), direction 

671 ) 

672 ) 

673 for fn, ts in files.items(): 

674 reason += " {0} ({1})".format(fn, time.ctime(ts)) 

675 return reason 

676 

677 for binary in upload.changes.binaries: 

678 filename = binary.hashed_file.filename 

679 path = os.path.join(upload.directory, filename) 

680 deb = apt_inst.DebFile(path) 

681 tar = TarTime() 

682 for archive in (deb.control, deb.data): 

683 archive.go(tar.callback) 

684 if tar.future_files: 684 ↛ 685line 684 didn't jump to line 685 because the condition on line 684 was never true

685 raise Reject(format_reason(filename, "future", tar.future_files)) 

686 if tar.past_files: 686 ↛ 687line 686 didn't jump to line 687 because the condition on line 686 was never true

687 raise Reject(format_reason(filename, "past", tar.past_files)) 

688 

689 

690class SourceCheck(Check): 

691 """Check source package for syntax errors.""" 

692 

693 def check_filename(self, control, filename, regex: re.Pattern) -> None: 

694 # In case we have an .orig.tar.*, we have to strip the Debian revison 

695 # from the version number. So handle this special case first. 

696 is_orig = True 

697 match = re_file_orig.match(filename) 

698 if not match: 

699 is_orig = False 

700 match = regex.match(filename) 

701 

702 if not match: 702 ↛ 703line 702 didn't jump to line 703 because the condition on line 702 was never true

703 raise Reject( 

704 "{0}: does not match regular expression for source filenames".format( 

705 filename 

706 ) 

707 ) 

708 if match.group("package") != control["Source"]: 708 ↛ 709line 708 didn't jump to line 709 because the condition on line 708 was never true

709 raise Reject("{0}: filename does not match Source field".format(filename)) 

710 

711 version = control["Version"] 

712 if is_orig: 

713 upstream_match = re_field_version_upstream.match(version) 

714 if not upstream_match: 714 ↛ 715line 714 didn't jump to line 715 because the condition on line 714 was never true

715 raise Reject( 

716 "{0}: Source package includes upstream tarball, but {1} has no Debian revision.".format( 

717 filename, version 

718 ) 

719 ) 

720 version = upstream_match.group("upstream") 

721 version_match = re_field_version.match(version) 

722 if version_match is None: 722 ↛ 723line 722 didn't jump to line 723 because the condition on line 722 was never true

723 raise Reject(f"{filename}: Version field does not match re_field_version") 

724 version_without_epoch = version_match.group("without_epoch") 

725 if match.group("version") != version_without_epoch: 725 ↛ 726line 725 didn't jump to line 726 because the condition on line 725 was never true

726 raise Reject("{0}: filename does not match Version field".format(filename)) 

727 

728 @override 

729 def check(self, upload: "daklib.archive.ArchiveUpload"): 

730 if upload.changes.source is None: 

731 if upload.changes.sourceful: 731 ↛ 732line 731 didn't jump to line 732 because the condition on line 731 was never true

732 raise Reject( 

733 "{}: Architecture field includes source, but no source package is included in the upload".format( 

734 upload.changes.filename 

735 ) 

736 ) 

737 return True 

738 

739 if not upload.changes.sourceful: 739 ↛ 740line 739 didn't jump to line 740 because the condition on line 739 was never true

740 raise Reject( 

741 "{}: Architecture field does not include source, but a source package is included in the upload".format( 

742 upload.changes.filename 

743 ) 

744 ) 

745 

746 changes = upload.changes.changes 

747 source = upload.changes.source 

748 control = cast(apt_pkg.TagSection, source.dsc) 

749 dsc_fn = source._dsc_file.filename 

750 

751 check_fields_for_valid_utf8(dsc_fn, control) 

752 

753 # check fields 

754 if not re_field_package.match(control["Source"]): 754 ↛ 755line 754 didn't jump to line 755 because the condition on line 754 was never true

755 raise Reject("{0}: Invalid Source field".format(dsc_fn)) 

756 if control["Source"] != changes["Source"]: 756 ↛ 757line 756 didn't jump to line 757 because the condition on line 756 was never true

757 raise Reject( 

758 "{0}: Source field does not match Source field in changes".format( 

759 dsc_fn 

760 ) 

761 ) 

762 if control["Version"] != changes["Version"]: 762 ↛ 763line 762 didn't jump to line 763 because the condition on line 762 was never true

763 raise Reject( 

764 "{0}: Version field does not match Version field in changes".format( 

765 dsc_fn 

766 ) 

767 ) 

768 

769 # check filenames 

770 self.check_filename(control, dsc_fn, re_file_dsc) 

771 for f in source.files.values(): 

772 self.check_filename(control, f.filename, re_file_source) 

773 

774 # check dependency field syntax 

775 for field in ( 

776 "Build-Conflicts", 

777 "Build-Conflicts-Indep", 

778 "Build-Depends", 

779 "Build-Depends-Arch", 

780 "Build-Depends-Indep", 

781 ): 

782 value = control.get(field) 

783 if value is not None: 

784 if value.strip() == "": 784 ↛ 785line 784 didn't jump to line 785 because the condition on line 784 was never true

785 raise Reject("{0}: empty {1} field".format(dsc_fn, field)) 

786 try: 

787 apt_pkg.parse_src_depends(value) 

788 except Exception as e: 

789 raise Reject( 

790 "{0}: APT could not parse {1} field: {2}".format( 

791 dsc_fn, field, e 

792 ) 

793 ) 

794 

795 rejects = utils.check_dsc_files(dsc_fn, control, list(source.files.keys())) 

796 if len(rejects) > 0: 796 ↛ 797line 796 didn't jump to line 797 because the condition on line 796 was never true

797 raise Reject("\n".join(rejects)) 

798 

799 return True 

800 

801 

802class SingleDistributionCheck(Check): 

803 """Check that the .changes targets only a single distribution.""" 

804 

805 @override 

806 def check(self, upload): 

807 if len(upload.changes.distributions) != 1: 807 ↛ 808line 807 didn't jump to line 808 because the condition on line 807 was never true

808 raise Reject("Only uploads to a single distribution are allowed.") 

809 

810 

811class ACLCheck(Check): 

812 """Check the uploader is allowed to upload the packages in .changes""" 

813 

814 def _does_hijack( 

815 self, session: "Session", upload: "daklib.archive.ArchiveUpload", suite: Suite 

816 ) -> tuple[Literal[True], str, str] | tuple[Literal[False], None, None]: 

817 # Try to catch hijacks. 

818 # This doesn't work correctly. Uploads to experimental can still 

819 # "hijack" binaries from unstable. Also one can hijack packages 

820 # via buildds (but people who try this should not be DMs). 

821 for binary_name in upload.changes.binary_names: 

822 binaries_query = ( 

823 select(DBBinary) 

824 .join(DBBinary.source) 

825 .where(DBBinary.suites.contains(suite)) 

826 .where(DBBinary.package == binary_name) 

827 ) 

828 for binary in session.scalars(binaries_query): 

829 if binary.source.source != upload.changes.changes["Source"]: 829 ↛ 830line 829 didn't jump to line 830 because the condition on line 829 was never true

830 return True, binary.package, binary.source.source 

831 return False, None, None 

832 

833 def _check_acl( 

834 self, session: "Session", upload: "daklib.archive.ArchiveUpload", acl: ACL 

835 ) -> tuple[Literal[False] | None, str] | tuple[Literal[True], None]: 

836 source_name = upload.changes.source_name 

837 fingerprint = upload.authorized_by_fingerprint 

838 

839 if acl.match_fingerprint and fingerprint not in acl.fingerprints: 839 ↛ 840line 839 didn't jump to line 840 because the condition on line 839 was never true

840 return None, "Fingerprint not in ACL" 

841 if acl.match_keyring is not None and fingerprint.keyring != acl.match_keyring: 841 ↛ 842line 841 didn't jump to line 842 because the condition on line 841 was never true

842 return None, "Fingerprint not in ACL's keyring" 

843 

844 if not acl.allow_new: 

845 if upload.new: 

846 return False, "NEW uploads are not allowed" 

847 for f in upload.changes.files.values(): 

848 assert f.section is not None 

849 if f.section == "byhand" or f.section.startswith("raw-"): 849 ↛ 850line 849 didn't jump to line 850 because the condition on line 849 was never true

850 return False, "BYHAND uploads are not allowed" 

851 if not acl.allow_source and upload.changes.source is not None: 851 ↛ 852line 851 didn't jump to line 852 because the condition on line 851 was never true

852 return False, "sourceful uploads are not allowed" 

853 binaries = upload.changes.binaries 

854 if len(binaries) != 0: 

855 if not acl.allow_binary: 855 ↛ 856line 855 didn't jump to line 856 because the condition on line 855 was never true

856 return False, "binary uploads are not allowed" 

857 if upload.changes.source is None and not acl.allow_binary_only: 857 ↛ 858line 857 didn't jump to line 858 because the condition on line 857 was never true

858 return False, "binary-only uploads are not allowed" 

859 if not acl.allow_binary_all: 859 ↛ 860line 859 didn't jump to line 860 because the condition on line 859 was never true

860 uploaded_arches = set(upload.changes.architectures) 

861 uploaded_arches.discard("source") 

862 allowed_arches = {a.arch_string for a in acl.architectures} 

863 forbidden_arches = uploaded_arches - allowed_arches 

864 if len(forbidden_arches) != 0: 

865 return ( 

866 False, 

867 "uploads for architecture(s) {0} are not allowed".format( 

868 ", ".join(forbidden_arches) 

869 ), 

870 ) 

871 if not acl.allow_hijack: 

872 assert upload.final_suites is not None 

873 for suite in upload.final_suites: 

874 does_hijack, hijacked_binary, hijacked_from = self._does_hijack( 

875 session, upload, suite 

876 ) 

877 if does_hijack: 877 ↛ 878line 877 didn't jump to line 878 because the condition on line 877 was never true

878 return ( 

879 False, 

880 "hijacks are not allowed (binary={0}, other-source={1})".format( 

881 hijacked_binary, hijacked_from 

882 ), 

883 ) 

884 

885 acl_per_source = session.scalars( 

886 select(ACLPerSource) 

887 .filter_by(acl=acl, fingerprint=fingerprint, source=source_name) 

888 .limit(1) 

889 ).first() 

890 if acl.allow_per_source and acl_per_source is None: 

891 return False, "not allowed to upload source package '{0}'".format( 

892 source_name 

893 ) 

894 if acl.deny_per_source and acl_per_source is not None: 894 ↛ 895line 894 didn't jump to line 895 because the condition on line 894 was never true

895 return ( 

896 False, 

897 acl_per_source.reason 

898 or "forbidden to upload source package '{0}'".format(source_name), 

899 ) 

900 

901 return True, None 

902 

903 @override 

904 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

905 session = upload.session 

906 fingerprint = upload.authorized_by_fingerprint 

907 keyring = fingerprint.keyring 

908 

909 if keyring is None: 909 ↛ 910line 909 didn't jump to line 910 because the condition on line 909 was never true

910 raise Reject( 

911 "No keyring for fingerprint {0}".format(fingerprint.fingerprint) 

912 ) 

913 if not keyring.active: 913 ↛ 914line 913 didn't jump to line 914 because the condition on line 913 was never true

914 raise Reject("Keyring {0} is not active".format(keyring.keyring_name)) 

915 

916 acl = fingerprint.acl or keyring.acl 

917 if acl is None: 917 ↛ 918line 917 didn't jump to line 918 because the condition on line 917 was never true

918 raise Reject("No ACL for fingerprint {0}".format(fingerprint.fingerprint)) 

919 result, reason = self._check_acl(session, upload, acl) 

920 if not result: 

921 assert reason is not None 

922 raise RejectACL(acl, reason) 

923 

924 for acl in session.scalars(select(ACL).filter_by(is_global=True)): 

925 result, reason = self._check_acl(session, upload, acl) 

926 if result is False: 926 ↛ 927line 926 didn't jump to line 927 because the condition on line 926 was never true

927 assert reason is not None 

928 raise RejectACL(acl, reason) 

929 

930 return True 

931 

932 @override 

933 def per_suite_check( 

934 self, upload: "daklib.archive.ArchiveUpload", suite: Suite 

935 ) -> bool: 

936 acls = suite.acls 

937 if len(acls) != 0: 937 ↛ 938line 937 didn't jump to line 938 because the condition on line 937 was never true

938 accept = False 

939 for acl in acls: 

940 result, reason = self._check_acl(upload.session, upload, acl) 

941 if result is False: 

942 raise Reject(reason) 

943 accept = accept or bool(result) 

944 if not accept: 

945 raise Reject( 

946 "Not accepted by any per-suite acl (suite={0})".format( 

947 suite.suite_name 

948 ) 

949 ) 

950 return True 

951 

952 

953class TransitionCheck(Check): 

954 """check for a transition""" 

955 

956 @override 

957 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

958 if not upload.changes.sourceful: 

959 return True 

960 

961 transitions = self.get_transitions() 

962 if transitions is None: 962 ↛ 965line 962 didn't jump to line 965 because the condition on line 962 was always true

963 return True 

964 

965 session = upload.session 

966 

967 control = upload.changes.changes 

968 source_match = re_field_source.match(control["Source"]) 

969 if source_match is None: 

970 raise Reject( 

971 f"{upload.changes.filename}: Source field does not match re_field_source" 

972 ) 

973 source = source_match.group("package") 

974 

975 for trans in transitions: 

976 t = transitions[trans] 

977 transition_source = t["source"] 

978 expected = t["new"] 

979 

980 # Will be None if nothing is in testing. 

981 current = get_source_in_suite(transition_source, "testing", session) 

982 if current is not None: 

983 compare = apt_pkg.version_compare(current.version, expected) 

984 

985 # This is still valid, the current version in testing is older than 

986 # the new version we wait for, or there is none in testing yet. 

987 # Check if the source we look at is affected by this. 

988 if current is None or compare < 0 and source in t["packages"]: 

989 # The source is affected, lets reject it. 

990 

991 rejectmsg = "{0}: part of the {1} transition.\n\n".format(source, trans) 

992 

993 if current is not None: 

994 currentlymsg = "at version {0}".format(current.version) 

995 else: 

996 currentlymsg = "not present in testing" 

997 

998 rejectmsg += "Transition description: {0}\n\n".format(t["reason"]) 

999 

1000 rejectmsg += "\n".join( 

1001 textwrap.wrap( 

1002 """Your package 

1003is part of a testing transition designed to get {0} migrated (it is 

1004currently {1}, we need version {2}). This transition is managed by the 

1005Release Team, and {3} is the Release-Team member responsible for it. 

1006Please mail debian-release@lists.debian.org or contact {3} directly if you 

1007need further assistance. You might want to upload to experimental until this 

1008transition is done.""".format( 

1009 transition_source, currentlymsg, expected, t["rm"] 

1010 ) 

1011 ) 

1012 ) 

1013 

1014 raise Reject(rejectmsg) 

1015 

1016 return True 

1017 

1018 def get_transitions(self): 

1019 cnf = Config() 

1020 path = cnf.get("Dinstall::ReleaseTransitions", "") 

1021 if path == "" or not os.path.exists(path): 1021 ↛ 1024line 1021 didn't jump to line 1024 because the condition on line 1021 was always true

1022 return None 

1023 

1024 with open(path, "r") as fd: 

1025 contents = fd.read() 

1026 try: 

1027 transitions = yaml.safe_load(contents) 

1028 return transitions 

1029 except yaml.YAMLError as msg: 

1030 utils.warn( 

1031 "Not checking transitions, the transitions file is broken: {0}".format( 

1032 msg 

1033 ) 

1034 ) 

1035 

1036 return None 

1037 

1038 

1039class NoSourceOnlyCheck(Check): 

1040 def is_source_only_upload(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

1041 changes = upload.changes 

1042 return bool(changes.source is not None and len(changes.binaries) == 0) 

1043 

1044 """Check for source-only upload 

1045 

1046 Source-only uploads are only allowed if Dinstall::AllowSourceOnlyUploads is 

1047 set. Otherwise they are rejected. 

1048 

1049 Source-only uploads are only accepted for source packages having a 

1050 Package-List field that also lists architectures per package. This 

1051 check can be disabled via 

1052 Dinstall::AllowSourceOnlyUploadsWithoutPackageList. 

1053 

1054 Source-only uploads to NEW are only allowed if 

1055 Dinstall::AllowSourceOnlyNew is set. 

1056 

1057 Uploads not including architecture-independent packages are only 

1058 allowed if Dinstall::AllowNoArchIndepUploads is set. 

1059 

1060 """ 

1061 

1062 @override 

1063 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

1064 if not self.is_source_only_upload(upload): 

1065 return True 

1066 changes = upload.changes 

1067 assert changes.source is not None 

1068 

1069 allow_source_only_uploads = Config().find_b("Dinstall::AllowSourceOnlyUploads") 

1070 allow_source_only_uploads_without_package_list = Config().find_b( 

1071 "Dinstall::AllowSourceOnlyUploadsWithoutPackageList" 

1072 ) 

1073 allow_source_only_new = Config().find_b("Dinstall::AllowSourceOnlyNew") 

1074 allow_source_only_new_keys = Config().value_list( 

1075 "Dinstall::AllowSourceOnlyNewKeys" 

1076 ) 

1077 allow_source_only_new_sources = Config().value_list( 

1078 "Dinstall::AllowSourceOnlyNewSources" 

1079 ) 

1080 allow_no_arch_indep_uploads = Config().find_b( 

1081 "Dinstall::AllowNoArchIndepUploads", True 

1082 ) 

1083 

1084 if not allow_source_only_uploads: 1084 ↛ 1085line 1084 didn't jump to line 1085 because the condition on line 1084 was never true

1085 raise Reject("Source-only uploads are not allowed.") 

1086 if ( 1086 ↛ 1090line 1086 didn't jump to line 1090

1087 not allow_source_only_uploads_without_package_list 

1088 and changes.source.package_list.fallback 

1089 ): 

1090 raise Reject( 

1091 "Source-only uploads are only allowed if a Package-List field that also list architectures is included in the source package. dpkg (>= 1.17.7) includes this information." 

1092 ) 

1093 if ( 1093 ↛ 1099line 1093 didn't jump to line 1099

1094 not allow_source_only_new 

1095 and upload.new 

1096 and changes.primary_fingerprint not in allow_source_only_new_keys 

1097 and changes.source_name not in allow_source_only_new_sources 

1098 ): 

1099 raise Reject("Source-only uploads to NEW are not allowed.") 

1100 

1101 if ( 1101 ↛ 1106line 1101 didn't jump to line 1106

1102 "all" not in changes.architectures 

1103 and changes.source.package_list.has_arch_indep_packages() 

1104 and not allow_no_arch_indep_uploads 

1105 ): 

1106 raise Reject("Uploads must include architecture-independent packages.") 

1107 

1108 return True 

1109 

1110 

1111class NewOverrideCheck(Check): 

1112 """Override NEW requirement""" 

1113 

1114 @override 

1115 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

1116 if not upload.new: 

1117 return True 

1118 

1119 new_override_keys = Config().value_list("Dinstall::NewOverrideKeys") 

1120 changes = upload.changes 

1121 

1122 if changes.primary_fingerprint in new_override_keys: 1122 ↛ 1123line 1122 didn't jump to line 1123 because the condition on line 1122 was never true

1123 upload.new = False 

1124 

1125 return True 

1126 

1127 

1128class ArchAllBinNMUCheck(Check): 

1129 """Check for arch:all binNMUs""" 

1130 

1131 @override 

1132 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

1133 changes = upload.changes 

1134 

1135 if ( 1135 ↛ 1139line 1135 didn't jump to line 1139

1136 "all" in changes.architectures 

1137 and changes.changes.get("Binary-Only") == "yes" 

1138 ): 

1139 raise Reject("arch:all binNMUs are not allowed.") 

1140 

1141 return True 

1142 

1143 

1144class LintianCheck(Check): 

1145 """Check package using lintian""" 

1146 

1147 @override 

1148 def check(self, upload: "daklib.archive.ArchiveUpload") -> bool: 

1149 changes = upload.changes 

1150 

1151 # Only check sourceful uploads. 

1152 if changes.source is None: 

1153 return True 

1154 # Only check uploads to unstable or experimental. 

1155 if ( 1155 ↛ 1159line 1155 didn't jump to line 1159

1156 "unstable" not in changes.distributions 

1157 and "experimental" not in changes.distributions 

1158 ): 

1159 return True 

1160 

1161 cnf = Config() 

1162 if "Dinstall::LintianTags" not in cnf: 

1163 return True 

1164 tagfile = cnf["Dinstall::LintianTags"] 

1165 

1166 with open(tagfile, "r") as sourcefile: 

1167 sourcecontent = sourcefile.read() 

1168 try: 

1169 lintiantags = yaml.safe_load(sourcecontent)["lintian"] 

1170 except yaml.YAMLError as msg: 

1171 raise Exception( 

1172 "Could not read lintian tags file {0}, YAML error: {1}".format( 

1173 tagfile, msg 

1174 ) 

1175 ) 

1176 

1177 with tempfile.NamedTemporaryFile(mode="w+t") as temptagfile: 

1178 os.fchmod(temptagfile.fileno(), 0o644) 

1179 for tags in lintiantags.values(): 

1180 for tag in tags: 

1181 print(tag, file=temptagfile) 

1182 temptagfile.flush() 

1183 

1184 changespath = os.path.join(upload.directory, changes.filename) 

1185 

1186 tempdir = cnf.get("Dir::TempPath") or os.environ.get("TMPDIR", "/tmp") 

1187 cmd = [] 

1188 user = cnf.get("Dinstall::UnprivUser") or None 

1189 if user is not None: 1189 ↛ 1190line 1189 didn't jump to line 1190 because the condition on line 1189 was never true

1190 cmd.extend(["sudo", "-H", "-u", user, "TMPDIR={0}".format(tempdir)]) 

1191 cmd.extend( 

1192 [ 

1193 "/usr/bin/lintian", 

1194 "--show-overrides", 

1195 "--tags-from-file", 

1196 temptagfile.name, 

1197 changespath, 

1198 ] 

1199 ) 

1200 process = subprocess.run( 

1201 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8" 

1202 ) 

1203 output = process.stdout 

1204 result = process.returncode 

1205 

1206 if result == 2: 1206 ↛ 1207line 1206 didn't jump to line 1207 because the condition on line 1206 was never true

1207 utils.warn( 

1208 "lintian failed for %s [return code: %s]." % (changespath, result) 

1209 ) 

1210 utils.warn(utils.prefix_multi_line_string(output, " [possible output:] ")) 

1211 

1212 parsed_tags = lintian.parse_lintian_output(output) 

1213 rejects = list(lintian.generate_reject_messages(parsed_tags, lintiantags)) 

1214 if len(rejects) != 0: 1214 ↛ 1215line 1214 didn't jump to line 1215 because the condition on line 1214 was never true

1215 raise Reject("\n".join(rejects)) 

1216 

1217 return True 

1218 

1219 

1220class SourceFormatCheck(Check): 

1221 """Check source format is allowed in the target suite""" 

1222 

1223 @override 

1224 def per_suite_check( 

1225 self, upload: "daklib.archive.ArchiveUpload", suite: Suite 

1226 ) -> bool: 

1227 source = upload.changes.source 

1228 session = upload.session 

1229 if source is None: 

1230 return True 

1231 

1232 source_format = source.dsc["Format"] 

1233 query = ( 

1234 select(SrcFormat) 

1235 .where(SrcFormat.format_name == source_format) 

1236 .where(SrcFormat.suites.contains(suite)) 

1237 .limit(1) 

1238 ) 

1239 if session.scalars(query).first() is None: 

1240 raise Reject( 

1241 "source format {0} is not allowed in suite {1}".format( 

1242 source_format, suite.suite_name 

1243 ) 

1244 ) 

1245 

1246 return True 

1247 

1248 

1249class SuiteCheck(Check): 

1250 @override 

1251 def per_suite_check( 

1252 self, upload: "daklib.archive.ArchiveUpload", suite: Suite 

1253 ) -> bool: 

1254 if not suite.accept_source_uploads and upload.changes.source is not None: 1254 ↛ 1255line 1254 didn't jump to line 1255 because the condition on line 1254 was never true

1255 raise Reject( 

1256 'The suite "{0}" does not accept source uploads.'.format( 

1257 suite.suite_name 

1258 ) 

1259 ) 

1260 if not suite.accept_binary_uploads and len(upload.changes.binaries) != 0: 1260 ↛ 1261line 1260 didn't jump to line 1261 because the condition on line 1260 was never true

1261 raise Reject( 

1262 'The suite "{0}" does not accept binary uploads.'.format( 

1263 suite.suite_name 

1264 ) 

1265 ) 

1266 return True 

1267 

1268 

1269class SuiteArchitectureCheck(Check): 

1270 @override 

1271 def per_suite_check( 

1272 self, upload: "daklib.archive.ArchiveUpload", suite: Suite 

1273 ) -> bool: 

1274 session = upload.session 

1275 for arch in upload.changes.architectures: 

1276 query = ( 

1277 select(Architecture) 

1278 .where(Architecture.arch_string == arch) 

1279 .where(Architecture.suites.contains(suite)) 

1280 .limit(1) 

1281 ) 

1282 if session.scalars(query).first() is None: 

1283 raise Reject( 

1284 "Architecture {0} is not allowed in suite {1}".format( 

1285 arch, suite.suite_name 

1286 ) 

1287 ) 

1288 

1289 return True 

1290 

1291 

1292class VersionCheck(Check): 

1293 """Check version constraints""" 

1294 

1295 def _highest_source_version( 

1296 self, session: "Session", source_name: str, suite: Suite 

1297 ) -> str | None: 

1298 db_source = session.scalars( 

1299 select(DBSource) 

1300 .where(DBSource.source == source_name) 

1301 .where(DBSource.suites.contains(suite)) 

1302 .order_by(DBSource.version.desc()) 

1303 .limit(1) 

1304 ).first() 

1305 if db_source is None: 

1306 return None 

1307 else: 

1308 return db_source.version 

1309 

1310 def _highest_binary_version( 

1311 self, session: "Session", binary_name: str, suite: Suite, architecture: str 

1312 ) -> str | None: 

1313 db_binary = session.scalars( 

1314 select(DBBinary) 

1315 .where(DBBinary.package == binary_name) 

1316 .where(DBBinary.suites.contains(suite)) 

1317 .join(DBBinary.architecture) 

1318 .where(Architecture.arch_string.in_(["all", architecture])) 

1319 .order_by(DBBinary.version.desc()) 

1320 .limit(1) 

1321 ).first() 

1322 if db_binary is None: 

1323 return None 

1324 else: 

1325 return db_binary.version 

1326 

1327 def _version_checks( 

1328 self, 

1329 upload: "daklib.archive.ArchiveUpload", 

1330 suite: Suite, 

1331 other_suite: Suite, 

1332 op: Callable[[int], bool], 

1333 op_name: str, 

1334 ) -> None: 

1335 session = upload.session 

1336 

1337 if upload.changes.source is not None: 

1338 source_name = upload.changes.source.dsc["Source"] 

1339 source_version = upload.changes.source.dsc["Version"] 

1340 v = self._highest_source_version(session, source_name, other_suite) 

1341 if v is not None and not op(version_compare(source_version, v)): 1341 ↛ 1342line 1341 didn't jump to line 1342 because the condition on line 1341 was never true

1342 raise Reject( 

1343 "Version check failed:\n" 

1344 "Your upload included the source package {0}, version {1},\n" 

1345 "however {3} already has version {2}.\n" 

1346 "Uploads to {5} must have a {4} version than present in {3}.".format( 

1347 source_name, 

1348 source_version, 

1349 v, 

1350 other_suite.suite_name, 

1351 op_name, 

1352 suite.suite_name, 

1353 ) 

1354 ) 

1355 

1356 for binary in upload.changes.binaries: 

1357 binary_name = binary.control["Package"] 

1358 binary_version = binary.control["Version"] 

1359 architecture = binary.control["Architecture"] 

1360 v = self._highest_binary_version( 

1361 session, binary_name, other_suite, architecture 

1362 ) 

1363 if v is not None and not op(version_compare(binary_version, v)): 1363 ↛ 1364line 1363 didn't jump to line 1364 because the condition on line 1363 was never true

1364 raise Reject( 

1365 "Version check failed:\n" 

1366 "Your upload included the binary package {0}, version {1}, for {2},\n" 

1367 "however {4} already has version {3}.\n" 

1368 "Uploads to {6} must have a {5} version than present in {4}.".format( 

1369 binary_name, 

1370 binary_version, 

1371 architecture, 

1372 v, 

1373 other_suite.suite_name, 

1374 op_name, 

1375 suite.suite_name, 

1376 ) 

1377 ) 

1378 

1379 @override 

1380 def per_suite_check( 

1381 self, upload: "daklib.archive.ArchiveUpload", suite: Suite 

1382 ) -> bool: 

1383 session = upload.session 

1384 

1385 vc_newer = session.scalars( 

1386 select(dbconn.VersionCheck) 

1387 .filter_by(suite=suite) 

1388 .where(dbconn.VersionCheck.check.in_(["MustBeNewerThan", "Enhances"])) 

1389 ) 

1390 must_be_newer_than = [vc.reference for vc in vc_newer] 

1391 # Must be newer than old versions in `suite` 

1392 must_be_newer_than.append(suite) 

1393 

1394 for s in must_be_newer_than: 

1395 self._version_checks(upload, suite, s, lambda result: result > 0, "higher") 

1396 

1397 vc_older = session.scalars( 

1398 select(dbconn.VersionCheck).filter_by(suite=suite, check="MustBeOlderThan") 

1399 ) 

1400 must_be_older_than = [vc.reference for vc in vc_older] 

1401 

1402 for s in must_be_older_than: 1402 ↛ 1403line 1402 didn't jump to line 1403 because the loop on line 1402 never started

1403 self._version_checks(upload, suite, s, lambda result: result < 0, "lower") 

1404 

1405 return True 

1406 

1407 @property 

1408 @override 

1409 def forcable(self) -> bool: 

1410 return True