Coverage for dak/examine_package.py: 65%

430 statements  

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

1""" 

2Script to automate some parts of checking NEW packages 

3 

4Most functions are written in a functional programming style. They 

5return a string avoiding the side effect of directly printing the string 

6to stdout. Those functions can be used in multithreaded parts of dak. 

7 

8@contact: Debian FTP Master <ftpmaster@debian.org> 

9@copyright: 2000, 2001, 2002, 2003, 2006 James Troup <james@nocrew.org> 

10@copyright: 2009 Joerg Jaspert <joerg@debian.org> 

11@license: GNU General Public License version 2 or later 

12""" 

13 

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

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

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

17# (at your option) any later version. 

18 

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

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

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

22# GNU General Public License for more details. 

23 

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

25# along with this program; if not, write to the Free Software 

26# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

27 

28################################################################################ 

29 

30# <Omnic> elmo wrote docs?!!?!?!?!?!?! 

31# <aj> as if he wasn't scary enough before!! 

32# * aj imagines a little red furry toy sitting hunched over a computer 

33# tapping furiously and giggling to himself 

34# <aj> eventually he stops, and his heads slowly spins around and you 

35# see this really evil grin and then he sees you, and picks up a 

36# knife from beside the keyboard and throws it at you, and as you 

37# breathe your last breath, he starts giggling again 

38# <aj> but i should be telling this to my psychiatrist, not you guys, 

39# right? :) 

40 

41################################################################################ 

42 

43import errno 

44import hashlib 

45import html 

46import os 

47import re 

48import subprocess 

49import sys 

50import tarfile 

51import tempfile 

52import threading 

53from functools import cache 

54from typing import IO, NoReturn 

55 

56import apt_pkg 

57from sqlalchemy import sql 

58 

59from daklib import sandbox, utils 

60from daklib.config import Config 

61from daklib.dbconn import DBConn, get_component_by_package_suite 

62from daklib.gpg import SignedFile 

63from daklib.regexes import ( 

64 re_contrib, 

65 re_file_binary, 

66 re_localhost, 

67 re_newlinespace, 

68 re_nonfree, 

69 re_spacestrip, 

70 re_version, 

71) 

72 

73################################################################################ 

74 

75Cnf = utils.get_conf() 

76 

77printed = threading.local() 

78printed.copyrights = {} 

79package_relations: dict[str, dict[str, str]] = ( 

80 {} 

81) #: Store relations of packages for later output 

82 

83# default is to not output html. 

84use_html = False 

85 

86################################################################################ 

87 

88 

89def usage(exit_code=0) -> NoReturn: 

90 print( 

91 """Usage: dak examine-package [PACKAGE]... 

92Check NEW package(s). 

93 

94 -h, --help show this help and exit 

95 -H, --html-output output html page with inspection result 

96 -f, --file-name filename for the html page 

97 

98PACKAGE can be a .changes, .dsc, .deb or .udeb filename.""" 

99 ) 

100 

101 sys.exit(exit_code) 

102 

103 

104################################################################################ 

105# probably xml.sax.saxutils would work as well 

106 

107 

108def escape_if_needed(s: str) -> str: 

109 if use_html: 

110 return html.escape(s) 

111 else: 

112 return s 

113 

114 

115def headline(s: str, level=2, bodyelement: str | None = None) -> str: 

116 if use_html: 

117 if bodyelement: 117 ↛ 125line 117 didn't jump to line 125 because the condition on line 117 was always true

118 return """<thead> 

119 <tr><th colspan="2" class="title" onclick="toggle('%(bodyelement)s', 'table-row-group', 'table-row-group')">%(title)s <span class="toggle-msg">(click to toggle)</span></th></tr> 

120 </thead>\n""" % { 

121 "bodyelement": bodyelement, 

122 "title": html.escape(os.path.basename(s), quote=False), 

123 } 

124 else: 

125 return "<h%d>%s</h%d>\n" % (level, html.escape(s, quote=False), level) 

126 else: 

127 return "---- %s ----\n" % (s) 

128 

129 

130# Colour definitions, 'end' isn't really for use 

131 

132ansi_colours = { 

133 "main": "\033[36m", 

134 "contrib": "\033[33m", 

135 "nonfree": "\033[31m", 

136 "provides": "\033[35m", 

137 "arch": "\033[32m", 

138 "end": "\033[0m", 

139 "bold": "\033[1m", 

140 "maintainer": "\033[32m", 

141 "distro": "\033[1m\033[41m", 

142 "error": "\033[1m\033[41m", 

143} 

144 

145html_colours = { 

146 "main": ('<span style="color: green">', "</span>"), 

147 "contrib": ('<span style="color: orange">', "</span>"), 

148 "nonfree": ('<span style="color: red">', "</span>"), 

149 "provides": ('<span style="color: magenta">', "</span>"), 

150 "arch": ('<span style="color: green">', "</span>"), 

151 "bold": ('<span style="font-weight: bold">', "</span>"), 

152 "maintainer": ('<span style="color: green">', "</span>"), 

153 "distro": ('<span style="font-weight: bold; background-color: red">', "</span>"), 

154 "error": ('<span style="font-weight: bold; background-color: red">', "</span>"), 

155} 

156 

157 

158def colour_output(s: str, colour: str) -> str: 

159 if use_html: 

160 return "%s%s%s" % ( 

161 html_colours[colour][0], 

162 html.escape(s, quote=False), 

163 html_colours[colour][1], 

164 ) 

165 else: 

166 return "%s%s%s" % (ansi_colours[colour], s, ansi_colours["end"]) 

167 

168 

169def escaped_text(s: str, strip=False) -> str: 

170 if use_html: 

171 if strip: 

172 s = s.strip() 

173 return "<pre>%s</pre>" % (s) 

174 else: 

175 return s 

176 

177 

178def formatted_text(s: str, strip=False) -> str: 

179 if use_html: 

180 if strip: 

181 s = s.strip() 

182 return "<pre>%s</pre>" % (html.escape(s, quote=False)) 

183 else: 

184 return s 

185 

186 

187def output_row(s: str) -> str: 

188 if use_html: 

189 return """<tr><td>""" + s + """</td></tr>""" 

190 else: 

191 return s 

192 

193 

194def format_field(k: str, v: str) -> str: 

195 if use_html: 

196 return """<tr><td class="key">%s:</td><td class="val">%s</td></tr>""" % (k, v) 

197 else: 

198 return "%s: %s" % (k, v) 

199 

200 

201def foldable_output( 

202 title: str, elementnameprefix: str, content: str, norow=False 

203) -> str: 

204 d = {"elementnameprefix": elementnameprefix} 

205 result = "" 

206 if use_html: 

207 result += ( 

208 """<div id="%(elementnameprefix)s-wrap"><a name="%(elementnameprefix)s"></a> 

209 <table class="infobox rfc822">\n""" 

210 % d 

211 ) 

212 result += headline(title, bodyelement="%(elementnameprefix)s-body" % d) 

213 if use_html: 

214 result += ( 

215 """ <tbody id="%(elementnameprefix)s-body" class="infobody">\n""" % d 

216 ) 

217 if norow: 

218 result += content + "\n" 

219 else: 

220 result += output_row(content) + "\n" 

221 if use_html: 

222 result += """</tbody></table></div>""" 

223 return result 

224 

225 

226################################################################################ 

227 

228 

229def get_depends_parts(depend: str) -> dict[str, str]: 

230 v_match = re_version.match(depend) 

231 if v_match: 231 ↛ 234line 231 didn't jump to line 234 because the condition on line 231 was always true

232 d_parts = {"name": v_match.group(1), "version": v_match.group(2)} 

233 else: 

234 d_parts = {"name": depend, "version": ""} 

235 return d_parts 

236 

237 

238def get_or_list(depend: str) -> list[str]: 

239 or_list = depend.split("|") 

240 return or_list 

241 

242 

243def get_comma_list(depend: str) -> list[str]: 

244 dep_list = depend.split(",") 

245 return dep_list 

246 

247 

248type Depends = list[list[dict[str, str]]] 

249 

250 

251def split_depends(d_str: str) -> Depends: 

252 # creates a list of lists of dictionaries of depends (package,version relation) 

253 

254 d_str = re_spacestrip.sub("", d_str) 

255 return [ 

256 [get_depends_parts(or_list) for or_list in get_or_list(comma_list)] 

257 for comma_list in get_comma_list(d_str) 

258 ] 

259 

260 

261def read_control( 

262 filename: str, 

263) -> tuple[apt_pkg.TagSection, list[str], str, Depends, Depends, Depends, str, str]: 

264 recommends: Depends = [] 

265 predepends: Depends = [] 

266 depends: Depends = [] 

267 section = "" 

268 maintainer = "" 

269 arch = "" 

270 

271 try: 

272 extracts = utils.deb_extract_control(filename) 

273 control = apt_pkg.TagSection(extracts) 

274 except: 

275 print(formatted_text("can't parse control info")) 

276 raise 

277 

278 control_keys = list(control.keys()) 

279 

280 if "Pre-Depends" in control: 280 ↛ 281line 280 didn't jump to line 281 because the condition on line 280 was never true

281 predepends_str = control["Pre-Depends"] 

282 predepends = split_depends(predepends_str) 

283 

284 if "Depends" in control: 284 ↛ 285line 284 didn't jump to line 285 because the condition on line 284 was never true

285 depends_str = control["Depends"] 

286 # create list of dependancy lists 

287 depends = split_depends(depends_str) 

288 

289 if "Recommends" in control: 289 ↛ 290line 289 didn't jump to line 290 because the condition on line 289 was never true

290 recommends_str = control["Recommends"] 

291 recommends = split_depends(recommends_str) 

292 

293 if "Section" in control: 293 ↛ 307line 293 didn't jump to line 307 because the condition on line 293 was always true

294 section_str = control["Section"] 

295 

296 c_match = re_contrib.search(section_str) 

297 nf_match = re_nonfree.search(section_str) 

298 if c_match: 298 ↛ 300line 298 didn't jump to line 300 because the condition on line 298 was never true

299 # contrib colour 

300 section = colour_output(section_str, "contrib") 

301 elif nf_match: 301 ↛ 303line 301 didn't jump to line 303 because the condition on line 301 was never true

302 # non-free colour 

303 section = colour_output(section_str, "nonfree") 

304 else: 

305 # main 

306 section = colour_output(section_str, "main") 

307 if "Architecture" in control: 307 ↛ 311line 307 didn't jump to line 311 because the condition on line 307 was always true

308 arch_str = control["Architecture"] 

309 arch = colour_output(arch_str, "arch") 

310 

311 if "Maintainer" in control: 311 ↛ 320line 311 didn't jump to line 320 because the condition on line 311 was always true

312 maintainer = control["Maintainer"] 

313 localhost = re_localhost.search(maintainer) 

314 if localhost: 314 ↛ 316line 314 didn't jump to line 316 because the condition on line 314 was never true

315 # highlight bad email 

316 maintainer = colour_output(maintainer, "maintainer") 

317 else: 

318 maintainer = escape_if_needed(maintainer) 

319 

320 return ( 

321 control, 

322 control_keys, 

323 section, 

324 predepends, 

325 depends, 

326 recommends, 

327 arch, 

328 maintainer, 

329 ) 

330 

331 

332def read_changes_or_dsc(suite: str, filename: str, session=None) -> str: 

333 dsc = {} 

334 

335 try: 

336 dsc = utils.parse_changes(filename, dsc_file=True) 

337 except: 

338 return formatted_text("can't parse .dsc control info") 

339 

340 filecontents = strip_pgp_signature(filename) 

341 keysinorder = [] 

342 for line in filecontents.split("\n"): 

343 m = re.match(r"([-a-zA-Z0-9]*):", line) 

344 if m: 

345 keysinorder.append(m.group(1)) 

346 

347 for k in list(dsc.keys()): 

348 if k in ("build-depends", "build-depends-indep"): 

349 dsc[k] = create_depends_string(suite, split_depends(dsc[k]), session) 

350 elif k == "architecture": 

351 if dsc["architecture"] != "any": 351 ↛ 347line 351 didn't jump to line 347 because the condition on line 351 was always true

352 dsc["architecture"] = colour_output(dsc["architecture"], "arch") 

353 elif k == "distribution": 

354 if dsc["distribution"] not in ("unstable", "experimental"): 354 ↛ 355line 354 didn't jump to line 355 because the condition on line 354 was never true

355 dsc["distribution"] = colour_output(dsc["distribution"], "distro") 

356 elif k in ("files", "changes", "description"): 

357 if use_html: 

358 dsc[k] = formatted_text(dsc[k], strip=True) 

359 else: 

360 dsc[k] = ( 

361 "\n" + "\n".join(" " + x for x in dsc[k].split("\n")) 

362 ).rstrip() 

363 else: 

364 dsc[k] = escape_if_needed(dsc[k]) 

365 

366 filecontents = ( 

367 "\n".join( 

368 format_field(x, dsc[x.lower()]) 

369 for x in keysinorder 

370 if not x.lower().startswith("checksums-") 

371 ) 

372 + "\n" 

373 ) 

374 return filecontents 

375 

376 

377def get_provides(suite: str) -> set[str]: 

378 provides: set[str] = set() 

379 session = DBConn().session() 

380 query = """SELECT DISTINCT value 

381 FROM binaries_metadata m 

382 JOIN bin_associations b 

383 ON b.bin = m.bin_id 

384 WHERE key_id = ( 

385 SELECT key_id 

386 FROM metadata_keys 

387 WHERE key = 'Provides' ) 

388 AND b.suite = ( 

389 SELECT id 

390 FROM suite 

391 WHERE suite_name = :suite 

392 OR codename = :suite)""" 

393 for p in session.execute(sql.text(query), {"suite": suite}).scalars(): 393 ↛ 394line 393 didn't jump to line 394 because the loop on line 393 never started

394 for e in p.split(","): 

395 provides.add(e.strip()) 

396 session.close() 

397 return provides 

398 

399 

400def create_depends_string(suite: str, depends_tree: Depends, session=None) -> str: 

401 result = "" 

402 if suite == "experimental": 402 ↛ 403line 402 didn't jump to line 403 because the condition on line 402 was never true

403 suite_list = ["experimental", "unstable"] 

404 else: 

405 suite_list = [suite] 

406 

407 provides: set[str] = set() 

408 comma_count = 1 

409 for item in depends_tree: 

410 if comma_count >= 2: 410 ↛ 411line 410 didn't jump to line 411 because the condition on line 410 was never true

411 result += ", " 

412 or_count = 1 

413 for d in item: 

414 if or_count >= 2: 414 ↛ 415line 414 didn't jump to line 415 because the condition on line 414 was never true

415 result += " | " 

416 # doesn't do version lookup yet. 

417 

418 component = get_component_by_package_suite( 

419 d["name"], suite_list, session=session 

420 ) 

421 if component is not None: 421 ↛ 422line 421 didn't jump to line 422 because the condition on line 421 was never true

422 adepends = d["name"] 

423 if d["version"] != "": 

424 adepends += " (%s)" % (d["version"]) 

425 

426 if component == "contrib": 

427 result += colour_output(adepends, "contrib") 

428 elif component in ("non-free-firmware", "non-free"): 

429 result += colour_output(adepends, "nonfree") 

430 else: 

431 result += colour_output(adepends, "main") 

432 else: 

433 adepends = d["name"] 

434 if d["version"] != "": 434 ↛ 436line 434 didn't jump to line 436 because the condition on line 434 was always true

435 adepends += " (%s)" % (d["version"]) 

436 if not provides: 436 ↛ 438line 436 didn't jump to line 438 because the condition on line 436 was always true

437 provides = get_provides(suite) 

438 if d["name"] in provides: 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true

439 result += colour_output(adepends, "provides") 

440 else: 

441 result += colour_output(adepends, "bold") 

442 or_count += 1 

443 comma_count += 1 

444 return result 

445 

446 

447def output_package_relations() -> str: 

448 """ 

449 Output the package relations, if there is more than one package checked in this run. 

450 """ 

451 

452 if len(package_relations) < 2: 452 ↛ 457line 452 didn't jump to line 457 because the condition on line 452 was always true

453 # Only list something if we have more than one binary to compare 

454 package_relations.clear() 

455 result = "" 

456 else: 

457 to_print = "" 

458 for package, relations in package_relations.items(): 

459 for relation, value in relations.items(): 

460 to_print += "%-15s: (%s) %s\n" % ( 

461 package, 

462 relation, 

463 value, 

464 ) 

465 

466 package_relations.clear() 

467 result = foldable_output("Package relations", "relations", to_print) 

468 package_relations.clear() 

469 return result 

470 

471 

472def output_deb_info(suite: str, filename: str, packagename: str, session=None) -> str: 

473 ( 

474 control, 

475 control_keys, 

476 section, 

477 predepends, 

478 depends, 

479 recommends, 

480 arch, 

481 maintainer, 

482 ) = read_control(filename) 

483 

484 if control == "": 484 ↛ 485line 484 didn't jump to line 485 because the condition on line 484 was never true

485 return formatted_text("no control info") 

486 to_print = "" 

487 if packagename not in package_relations: 487 ↛ 489line 487 didn't jump to line 489 because the condition on line 487 was always true

488 package_relations[packagename] = {} 

489 for key in control_keys: 

490 if key == "Source": 490 ↛ 491line 490 didn't jump to line 491 because the condition on line 490 was never true

491 field_value = escape_if_needed(control.find(key)) 

492 if use_html: 

493 field_value = '<a href="https://tracker.debian.org/pkg/{0}" rel="nofollow">{0}</a>'.format( 

494 field_value 

495 ) 

496 elif key == "Pre-Depends": 496 ↛ 497line 496 didn't jump to line 497 because the condition on line 496 was never true

497 field_value = create_depends_string(suite, predepends, session) 

498 package_relations[packagename][key] = field_value 

499 elif key == "Depends": 499 ↛ 500line 499 didn't jump to line 500 because the condition on line 499 was never true

500 field_value = create_depends_string(suite, depends, session) 

501 package_relations[packagename][key] = field_value 

502 elif key == "Recommends": 502 ↛ 503line 502 didn't jump to line 503 because the condition on line 502 was never true

503 field_value = create_depends_string(suite, recommends, session) 

504 package_relations[packagename][key] = field_value 

505 elif key == "Section": 

506 field_value = section 

507 elif key == "Architecture": 

508 field_value = arch 

509 elif key == "Maintainer": 

510 field_value = maintainer 

511 elif key in ("Homepage", "Vcs-Browser"): 511 ↛ 512line 511 didn't jump to line 512 because the condition on line 511 was never true

512 field_value = escape_if_needed(control.find(key)) 

513 if use_html: 

514 field_value = '<a href="%s" rel="nofollow">%s</a>' % ( 

515 field_value, 

516 field_value, 

517 ) 

518 elif key == "Description": 

519 if use_html: 

520 field_value = formatted_text(control.find(key), strip=True) 

521 else: 

522 desc = control.find(key) 

523 desc = re_newlinespace.sub("\n ", desc) 

524 field_value = escape_if_needed(desc) 

525 else: 

526 field_value = escape_if_needed(control.find(key)) 

527 to_print += " " + format_field(key, field_value) + "\n" 

528 return to_print 

529 

530 

531def do_command(command: list[str], escaped=False) -> str: 

532 result = subprocess.run(command, stdout=subprocess.PIPE, text=True) 

533 if escaped: 533 ↛ 534line 533 didn't jump to line 534 because the condition on line 533 was never true

534 return escaped_text(result.stdout) 

535 else: 

536 return formatted_text(result.stdout) 

537 

538 

539def do_lintian(filename: str) -> str: 

540 cnf = Config() 

541 if not cnf.find_b("Examine-Package::EnableLintian"): 

542 return "" 

543 

544 cmd = [] 

545 

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

547 if user is not None: 

548 cmd.extend(["sudo", "-H", "-u", user]) 

549 

550 color = "always" 

551 if use_html: 

552 color = "html" 

553 

554 cmd.extend(["lintian", "--show-overrides", "--color", color, "--", filename]) 

555 

556 try: 

557 return do_command(cmd, escaped=True) 

558 except OSError as e: 

559 return colour_output("Running lintian failed: %s" % (e), "error") 

560 

561 

562def foldable_lintian_output(elementnameprefix: str, filename: str) -> str: 

563 cnf = Config() 

564 if not cnf.find_b("Examine-Package::EnableLintian"): 564 ↛ 567line 564 didn't jump to line 567 because the condition on line 564 was always true

565 return "" 

566 

567 title = f"lintian {get_lintian_version()} check for {os.path.basename(filename)}" 

568 return foldable_output(title, elementnameprefix, do_lintian(filename)) 

569 

570 

571def extract_one_file_from_deb( 

572 deb_filename: str, match: re.Pattern 

573) -> tuple[str, bytes] | tuple[None, None]: 

574 with tempfile.TemporaryFile() as tmpfh: 

575 dpkg_cmd = ("dpkg-deb", "--fsys-tarfile", deb_filename) 

576 subprocess.check_call(dpkg_cmd, stdout=tmpfh) 

577 

578 tmpfh.seek(0) 

579 with tarfile.open(fileobj=tmpfh, mode="r") as tar: 

580 matched_member = None 

581 for member in tar: 581 ↛ 586line 581 didn't jump to line 586 because the loop on line 581 didn't complete

582 if member.isfile() and match.match(member.name): 

583 matched_member = member 

584 break 

585 

586 if not matched_member: 586 ↛ 587line 586 didn't jump to line 587 because the condition on line 586 was never true

587 return None, None 

588 

589 fh = tar.extractfile(matched_member) 

590 assert fh is not None # checked for regular file with `member.isfile()` 

591 matched_data = fh.read() 

592 fh.close() 

593 

594 return matched_member.name, matched_data 

595 

596 

597def get_copyright(deb_filename: str) -> str: 

598 global printed 

599 

600 re_copyright = re.compile(r"\./usr(/share)?/doc/(?P<package>[^/]+)/copyright") 

601 cright_path, cright = extract_one_file_from_deb(deb_filename, re_copyright) 

602 

603 if not cright_path: 603 ↛ 604line 603 didn't jump to line 604 because the condition on line 603 was never true

604 return formatted_text( 

605 "WARNING: No copyright found, please check package manually." 

606 ) 

607 assert cright is not None 

608 

609 package_match = re_file_binary.match(os.path.basename(deb_filename)) 

610 assert package_match is not None 

611 package = package_match.group("package") 

612 doc_directory_match = re_copyright.match(cright_path) 

613 assert doc_directory_match is not None 

614 doc_directory = doc_directory_match.group("package") 

615 if package != doc_directory: 615 ↛ 616line 615 didn't jump to line 616 because the condition on line 615 was never true

616 return formatted_text( 

617 "WARNING: wrong doc directory (expected %s, got %s)." 

618 % (package, doc_directory) 

619 ) 

620 

621 copyrightmd5 = hashlib.md5(cright).hexdigest() 

622 

623 res = "" 

624 if copyrightmd5 in printed.copyrights and printed.copyrights[ 624 ↛ 627line 624 didn't jump to line 627 because the condition on line 624 was never true

625 copyrightmd5 

626 ] != "%s (%s)" % (package, os.path.basename(deb_filename)): 

627 res += formatted_text( 

628 "NOTE: Copyright is the same as %s.\n\n" 

629 % (printed.copyrights[copyrightmd5]) 

630 ) 

631 else: 

632 printed.copyrights[copyrightmd5] = "%s (%s)" % ( 

633 package, 

634 os.path.basename(deb_filename), 

635 ) 

636 return res + formatted_text(cright.decode()) 

637 

638 

639def get_readme_source(dsc_filename: str) -> str: 

640 with tempfile.TemporaryDirectory(prefix="dak-examine-package") as tempdir: 640 ↛ exitline 640 didn't return from function 'get_readme_source' because the return on line 659 wasn't executed

641 targetdir = os.path.join(tempdir, "source") 

642 

643 cmd = ("dpkg-source", "--no-check", "--no-copy", "-x", dsc_filename, targetdir) 

644 try: 

645 sandbox.run( 

646 cmd, 

647 sandbox=sandbox.Sandbox( 

648 extra_read_write_paths=[tempdir, os.environ.get("TMPDIR", "/tmp")], 

649 ), 

650 stdout=subprocess.PIPE, 

651 stderr=subprocess.STDOUT, 

652 check=True, 

653 ) 

654 except subprocess.CalledProcessError as e: 

655 res = "How is education supposed to make me feel smarter? Besides, every time I learn something new, it pushes some\n old stuff out of my brain. Remember when I took that home winemaking course, and I forgot how to drive?\n" 

656 res += "Error, couldn't extract source, WTF?\n" 

657 res += "'dpkg-source -x' failed. return code: %s.\n\n" % (e.returncode) 

658 res += e.output 

659 return res 

660 utils.remove_unsafe_symlinks(targetdir) 

661 

662 path = utils.resolve_relative_path(targetdir, "debian/README.source") 

663 res = "" 

664 if os.path.exists(path): 664 ↛ 668line 664 didn't jump to line 668 because the condition on line 664 was always true

665 with open(path, "r") as fh: 

666 res += formatted_text(fh.read()) 

667 else: 

668 res += "No README.source in this package\n\n" 

669 

670 return res 

671 

672 

673def check_dsc(suite: str, dsc_filename: str, session=None) -> str: 

674 dsc = read_changes_or_dsc(suite, dsc_filename, session) 

675 dsc_basename = os.path.basename(dsc_filename) 

676 cdsc = ( 

677 foldable_output(dsc_filename, "dsc", dsc, norow=True) 

678 + "\n" 

679 + foldable_lintian_output("source-lintian", dsc_filename) 

680 + "\n" 

681 + foldable_output( 

682 "README.source for %s" % dsc_basename, 

683 "source-readmesource", 

684 get_readme_source(dsc_filename), 

685 ) 

686 ) 

687 return cdsc 

688 

689 

690def check_deb(suite: str, deb_filename: str, session=None) -> str: 

691 filename = os.path.basename(deb_filename) 

692 packagename = filename.split("_")[0] 

693 

694 if filename.endswith(".udeb"): 694 ↛ 695line 694 didn't jump to line 695 because the condition on line 694 was never true

695 is_a_udeb = 1 

696 else: 

697 is_a_udeb = 0 

698 

699 result = ( 

700 foldable_output( 

701 "control file for %s" % (filename), 

702 "binary-%s-control" % packagename, 

703 output_deb_info(suite, deb_filename, packagename, session), 

704 norow=True, 

705 ) 

706 + "\n" 

707 ) 

708 

709 if is_a_udeb: 709 ↛ 710line 709 didn't jump to line 710 because the condition on line 709 was never true

710 result += ( 

711 foldable_output( 

712 "skipping lintian check for udeb", "binary-%s-lintian" % packagename, "" 

713 ) 

714 + "\n" 

715 ) 

716 else: 

717 result += ( 

718 foldable_lintian_output(f"binary-{packagename}-lintian", deb_filename) 

719 + "\n" 

720 ) 

721 

722 result += ( 

723 foldable_output( 

724 "contents of %s" % (filename), 

725 "binary-%s-contents" % packagename, 

726 do_command(["dpkg", "-c", deb_filename]), 

727 ) 

728 + "\n" 

729 ) 

730 

731 if is_a_udeb: 731 ↛ 732line 731 didn't jump to line 732 because the condition on line 731 was never true

732 result += ( 

733 foldable_output( 

734 "skipping copyright for udeb", "binary-%s-copyright" % packagename, "" 

735 ) 

736 + "\n" 

737 ) 

738 else: 

739 result += ( 

740 foldable_output( 

741 "copyright of %s" % (filename), 

742 "binary-%s-copyright" % packagename, 

743 get_copyright(deb_filename), 

744 ) 

745 + "\n" 

746 ) 

747 

748 return result 

749 

750 

751# Read a file, strip the signature and return the modified contents as 

752# a string. 

753 

754 

755def strip_pgp_signature(filename: str) -> str: 

756 with open(filename, "rb") as f: 

757 data = f.read() 

758 signedfile = SignedFile(data, keyrings=(), require_signature=False) 

759 return signedfile.contents.decode() 

760 

761 

762def display_changes(suite: str, changes_filename: str) -> str: 

763 global printed 

764 changes = read_changes_or_dsc(suite, changes_filename) 

765 printed.copyrights = {} 

766 return foldable_output(changes_filename, "changes", changes, norow=True) 

767 

768 

769def check_changes(changes_filename: str) -> str: 

770 try: 

771 changes = utils.parse_changes(changes_filename) 

772 except UnicodeDecodeError: 

773 utils.warn("Encoding problem with changes file %s" % (changes_filename)) 

774 output = display_changes(changes["distribution"], changes_filename) 

775 

776 files = utils.build_file_list(changes) 

777 for f in files.keys(): 

778 if f.endswith((".deb", ".udeb")): 

779 output += check_deb(changes["distribution"], f) 

780 if f.endswith(".dsc"): 

781 output += check_dsc(changes["distribution"], f) 

782 # else: => byhand 

783 return output 

784 

785 

786def main() -> None: 

787 global Cnf, db_files, waste, excluded 

788 

789 # Cnf = utils.get_conf() 

790 

791 Arguments = [ 

792 ("h", "help", "Examine-Package::Options::Help"), 

793 ("H", "html-output", "Examine-Package::Options::Html-Output"), 

794 ] 

795 for i in ["Help", "Html-Output", "partial-html"]: 

796 key = "Examine-Package::Options::%s" % i 

797 if key not in Cnf: 797 ↛ 795line 797 didn't jump to line 795 because the condition on line 797 was always true

798 Cnf[key] = "" # type: ignore[index] 

799 

800 args = apt_pkg.parse_commandline(Cnf, Arguments, sys.argv) # type: ignore[attr-defined] 

801 Options = Cnf.subtree("Examine-Package::Options") # type: ignore[attr-defined] 

802 

803 if Options["Help"]: 803 ↛ 806line 803 didn't jump to line 806 because the condition on line 803 was always true

804 usage() 

805 

806 if Options["Html-Output"]: 

807 global use_html 

808 use_html = True 

809 

810 for f in args: 

811 try: 

812 my_fd: IO[str] 

813 if not Options["Html-Output"]: 

814 # Pipe output for each argument through less 

815 less_cmd = ("less", "-r", "-") 

816 less_process = subprocess.Popen( 

817 less_cmd, stdin=subprocess.PIPE, bufsize=0, text=True 

818 ) 

819 less_fd = less_process.stdin 

820 assert less_fd is not None 

821 # -R added to display raw control chars for colour 

822 my_fd = less_fd 

823 else: 

824 less_fd = None 

825 my_fd = sys.stdout 

826 

827 try: 

828 if f.endswith(".changes"): 

829 my_fd.write(check_changes(f)) 

830 elif f.endswith((".deb", ".udeb")): 

831 # default to unstable when we don't have a .changes file 

832 # perhaps this should be a command line option? 

833 my_fd.write(check_deb("unstable", f)) 

834 elif f.endswith(".dsc"): 

835 my_fd.write(check_dsc("unstable", f)) 

836 else: 

837 utils.fubar("Unrecognised file type: '%s'." % (f)) 

838 finally: 

839 my_fd.write(output_package_relations()) 

840 if less_fd is not None: 

841 # Reset stdout here so future less invocations aren't FUBAR 

842 less_fd.close() 

843 less_process.wait() 

844 except OSError as e: 

845 if e.errno == errno.EPIPE: 

846 utils.warn("[examine-package] Caught EPIPE; skipping.") 

847 else: 

848 raise 

849 except KeyboardInterrupt: 

850 utils.warn("[examine-package] Caught C-c; skipping.") 

851 

852 

853@cache 

854def get_lintian_version() -> str: 

855 # eg. "Lintian v2.5.100" 

856 val = subprocess.check_output(("lintian", "--version"), text=True) 

857 return val.split(" v")[-1].strip()