Coverage for dak/examine_package.py: 64%

431 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-03-14 12:19 +0000

1#! /usr/bin/env python3 

2 

3""" 

4Script to automate some parts of checking NEW packages 

5 

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

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

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

9 

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

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

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

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

14""" 

15 

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

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

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

19# (at your option) any later version. 

20 

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

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

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

24# GNU General Public License for more details. 

25 

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

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

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

29 

30################################################################################ 

31 

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

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

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

35# tapping furiously and giggling to himself 

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

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

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

39# breathe your last breath, he starts giggling again 

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

41# right? :) 

42 

43################################################################################ 

44 

45import errno 

46import hashlib 

47import html 

48import os 

49import re 

50import subprocess 

51import sys 

52import tarfile 

53import tempfile 

54import threading 

55from functools import cache 

56from typing import IO, NoReturn 

57 

58import apt_pkg 

59from sqlalchemy import sql 

60 

61from daklib import utils 

62from daklib.config import Config 

63from daklib.dbconn import DBConn, get_component_by_package_suite 

64from daklib.gpg import SignedFile 

65from daklib.regexes import ( 

66 re_contrib, 

67 re_file_binary, 

68 re_localhost, 

69 re_newlinespace, 

70 re_nonfree, 

71 re_spacestrip, 

72 re_version, 

73) 

74 

75################################################################################ 

76 

77Cnf = utils.get_conf() 

78 

79printed = threading.local() 

80printed.copyrights = {} 

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

82 {} 

83) #: Store relations of packages for later output 

84 

85# default is to not output html. 

86use_html = False 

87 

88################################################################################ 

89 

90 

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

92 print( 

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

94Check NEW package(s). 

95 

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

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

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

99 

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

101 ) 

102 

103 sys.exit(exit_code) 

104 

105 

106################################################################################ 

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

108 

109 

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

111 if use_html: 

112 return html.escape(s) 

113 else: 

114 return s 

115 

116 

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

118 if use_html: 

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

120 return """<thead> 

121 <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> 

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

123 "bodyelement": bodyelement, 

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

125 } 

126 else: 

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

128 else: 

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

130 

131 

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

133 

134ansi_colours = { 

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

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

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

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

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

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

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

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

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

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

145} 

146 

147html_colours = { 

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

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

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

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

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

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

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

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

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

157} 

158 

159 

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

161 if use_html: 

162 return "%s%s%s" % ( 

163 html_colours[colour][0], 

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

165 html_colours[colour][1], 

166 ) 

167 else: 

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

169 

170 

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

172 if use_html: 

173 if strip: 

174 s = s.strip() 

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

176 else: 

177 return s 

178 

179 

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

181 if use_html: 

182 if strip: 

183 s = s.strip() 

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

185 else: 

186 return s 

187 

188 

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

190 if use_html: 

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

192 else: 

193 return s 

194 

195 

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

197 if use_html: 

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

199 else: 

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

201 

202 

203def foldable_output( 

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

205) -> str: 

206 d = {"elementnameprefix": elementnameprefix} 

207 result = "" 

208 if use_html: 

209 result += ( 

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

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

212 % d 

213 ) 

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

215 if use_html: 

216 result += ( 

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

218 ) 

219 if norow: 

220 result += content + "\n" 

221 else: 

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

223 if use_html: 

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

225 return result 

226 

227 

228################################################################################ 

229 

230 

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

232 v_match = re_version.match(depend) 

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

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

235 else: 

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

237 return d_parts 

238 

239 

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

241 or_list = depend.split("|") 

242 return or_list 

243 

244 

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

246 dep_list = depend.split(",") 

247 return dep_list 

248 

249 

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

251 

252 

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

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

255 

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

257 return [ 

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

259 for comma_list in get_comma_list(d_str) 

260 ] 

261 

262 

263def read_control( 

264 filename: str, 

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

266 recommends: Depends = [] 

267 predepends: Depends = [] 

268 depends: Depends = [] 

269 section = "" 

270 maintainer = "" 

271 arch = "" 

272 

273 try: 

274 extracts = utils.deb_extract_control(filename) 

275 control = apt_pkg.TagSection(extracts) 

276 except: 

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

278 raise 

279 

280 control_keys = list(control.keys()) 

281 

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

283 predepends_str = control["Pre-Depends"] 

284 predepends = split_depends(predepends_str) 

285 

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

287 depends_str = control["Depends"] 

288 # create list of dependancy lists 

289 depends = split_depends(depends_str) 

290 

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

292 recommends_str = control["Recommends"] 

293 recommends = split_depends(recommends_str) 

294 

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

296 section_str = control["Section"] 

297 

298 c_match = re_contrib.search(section_str) 

299 nf_match = re_nonfree.search(section_str) 

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

301 # contrib colour 

302 section = colour_output(section_str, "contrib") 

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

304 # non-free colour 

305 section = colour_output(section_str, "nonfree") 

306 else: 

307 # main 

308 section = colour_output(section_str, "main") 

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

310 arch_str = control["Architecture"] 

311 arch = colour_output(arch_str, "arch") 

312 

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

314 maintainer = control["Maintainer"] 

315 localhost = re_localhost.search(maintainer) 

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

317 # highlight bad email 

318 maintainer = colour_output(maintainer, "maintainer") 

319 else: 

320 maintainer = escape_if_needed(maintainer) 

321 

322 return ( 

323 control, 

324 control_keys, 

325 section, 

326 predepends, 

327 depends, 

328 recommends, 

329 arch, 

330 maintainer, 

331 ) 

332 

333 

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

335 dsc = {} 

336 

337 try: 

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

339 except: 

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

341 

342 filecontents = strip_pgp_signature(filename) 

343 keysinorder = [] 

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

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

346 if m: 

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

348 

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

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

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

352 elif k == "architecture": 

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

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

355 elif k == "distribution": 

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

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

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

359 if use_html: 

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

361 else: 

362 dsc[k] = ( 

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

364 ).rstrip() 

365 else: 

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

367 

368 filecontents = ( 

369 "\n".join( 

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

371 for x in keysinorder 

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

373 ) 

374 + "\n" 

375 ) 

376 return filecontents 

377 

378 

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

380 provides: set[str] = set() 

381 session = DBConn().session() 

382 query = """SELECT DISTINCT value 

383 FROM binaries_metadata m 

384 JOIN bin_associations b 

385 ON b.bin = m.bin_id 

386 WHERE key_id = ( 

387 SELECT key_id 

388 FROM metadata_keys 

389 WHERE key = 'Provides' ) 

390 AND b.suite = ( 

391 SELECT id 

392 FROM suite 

393 WHERE suite_name = :suite 

394 OR codename = :suite)""" 

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

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

397 provides.add(e.strip()) 

398 session.close() 

399 return provides 

400 

401 

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

403 result = "" 

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

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

406 else: 

407 suite_list = [suite] 

408 

409 provides: set[str] = set() 

410 comma_count = 1 

411 for item in depends_tree: 

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

413 result += ", " 

414 or_count = 1 

415 for d in item: 

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

417 result += " | " 

418 # doesn't do version lookup yet. 

419 

420 component = get_component_by_package_suite( 

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

422 ) 

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

424 adepends = d["name"] 

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

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

427 

428 if component == "contrib": 

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

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

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

432 else: 

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

434 else: 

435 adepends = d["name"] 

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

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

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

439 provides = get_provides(suite) 

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

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

442 else: 

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

444 or_count += 1 

445 comma_count += 1 

446 return result 

447 

448 

449def output_package_relations() -> str: 

450 """ 

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

452 """ 

453 

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

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

456 package_relations.clear() 

457 result = "" 

458 else: 

459 to_print = "" 

460 for package in package_relations: 

461 for relation in package_relations[package]: 

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

463 package, 

464 relation, 

465 package_relations[package][relation], 

466 ) 

467 

468 package_relations.clear() 

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

470 package_relations.clear() 

471 return result 

472 

473 

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

475 ( 

476 control, 

477 control_keys, 

478 section, 

479 predepends, 

480 depends, 

481 recommends, 

482 arch, 

483 maintainer, 

484 ) = read_control(filename) 

485 

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

487 return formatted_text("no control info") 

488 to_print = "" 

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

490 package_relations[packagename] = {} 

491 for key in control_keys: 

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

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

494 if use_html: 

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

496 field_value 

497 ) 

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

499 field_value = create_depends_string(suite, predepends, session) 

500 package_relations[packagename][key] = field_value 

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

502 field_value = create_depends_string(suite, depends, session) 

503 package_relations[packagename][key] = field_value 

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

505 field_value = create_depends_string(suite, recommends, session) 

506 package_relations[packagename][key] = field_value 

507 elif key == "Section": 

508 field_value = section 

509 elif key == "Architecture": 

510 field_value = arch 

511 elif key == "Maintainer": 

512 field_value = maintainer 

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

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

515 if use_html: 

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

517 field_value, 

518 field_value, 

519 ) 

520 elif key == "Description": 

521 if use_html: 

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

523 else: 

524 desc = control.find(key) 

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

526 field_value = escape_if_needed(desc) 

527 else: 

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

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

530 return to_print 

531 

532 

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

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

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

536 return escaped_text(result.stdout) 

537 else: 

538 return formatted_text(result.stdout) 

539 

540 

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

542 cnf = Config() 

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

544 return "" 

545 

546 cmd = [] 

547 

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

549 if user is not None: 

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

551 

552 color = "always" 

553 if use_html: 

554 color = "html" 

555 

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

557 

558 try: 

559 return do_command(cmd, escaped=True) 

560 except OSError as e: 

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

562 

563 

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

565 cnf = Config() 

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

567 return "" 

568 

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

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

571 

572 

573def extract_one_file_from_deb( 

574 deb_filename: str, match: re.Pattern 

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

576 with tempfile.TemporaryFile() as tmpfh: 

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

578 subprocess.check_call(dpkg_cmd, stdout=tmpfh) 

579 

580 tmpfh.seek(0) 

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

582 matched_member = None 

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

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

585 matched_member = member 

586 break 

587 

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

589 return None, None 

590 

591 fh = tar.extractfile(matched_member) 

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

593 matched_data = fh.read() 

594 fh.close() 

595 

596 return matched_member.name, matched_data 

597 

598 

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

600 global printed 

601 

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

603 cright_path, cright = extract_one_file_from_deb(deb_filename, re_copyright) 

604 

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

606 return formatted_text( 

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

608 ) 

609 assert cright is not None 

610 

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

612 assert package_match is not None 

613 package = package_match.group("package") 

614 doc_directory_match = re_copyright.match(cright_path) 

615 assert doc_directory_match is not None 

616 doc_directory = doc_directory_match.group("package") 

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

618 return formatted_text( 

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

620 % (package, doc_directory) 

621 ) 

622 

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

624 

625 res = "" 

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

627 copyrightmd5 

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

629 res += formatted_text( 

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

631 % (printed.copyrights[copyrightmd5]) 

632 ) 

633 else: 

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

635 package, 

636 os.path.basename(deb_filename), 

637 ) 

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

639 

640 

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

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

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

644 

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

646 try: 

647 subprocess.check_output(cmd, stderr=subprocess.STDOUT) 

648 except subprocess.CalledProcessError as e: 

649 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" 

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

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

652 res += e.output 

653 return res 

654 

655 path = os.path.join(targetdir, "debian/README.source") 

656 res = "" 

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

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

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

660 else: 

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

662 

663 return res 

664 

665 

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

667 dsc = read_changes_or_dsc(suite, dsc_filename, session) 

668 dsc_basename = os.path.basename(dsc_filename) 

669 cdsc = ( 

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

671 + "\n" 

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

673 + "\n" 

674 + foldable_output( 

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

676 "source-readmesource", 

677 get_readme_source(dsc_filename), 

678 ) 

679 ) 

680 return cdsc 

681 

682 

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

684 filename = os.path.basename(deb_filename) 

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

686 

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

688 is_a_udeb = 1 

689 else: 

690 is_a_udeb = 0 

691 

692 result = ( 

693 foldable_output( 

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

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

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

697 norow=True, 

698 ) 

699 + "\n" 

700 ) 

701 

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

703 result += ( 

704 foldable_output( 

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

706 ) 

707 + "\n" 

708 ) 

709 else: 

710 result += ( 

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

712 + "\n" 

713 ) 

714 

715 result += ( 

716 foldable_output( 

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

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

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

720 ) 

721 + "\n" 

722 ) 

723 

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

725 result += ( 

726 foldable_output( 

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

728 ) 

729 + "\n" 

730 ) 

731 else: 

732 result += ( 

733 foldable_output( 

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

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

736 get_copyright(deb_filename), 

737 ) 

738 + "\n" 

739 ) 

740 

741 return result 

742 

743 

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

745# a string. 

746 

747 

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

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

750 data = f.read() 

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

752 return signedfile.contents.decode() 

753 

754 

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

756 global printed 

757 changes = read_changes_or_dsc(suite, changes_filename) 

758 printed.copyrights = {} 

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

760 

761 

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

763 try: 

764 changes = utils.parse_changes(changes_filename) 

765 except UnicodeDecodeError: 

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

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

768 

769 files = utils.build_file_list(changes) 

770 for f in files.keys(): 

771 if f.endswith(".deb") or f.endswith(".udeb"): 

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

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

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

775 # else: => byhand 

776 return output 

777 

778 

779def main() -> None: 

780 global Cnf, db_files, waste, excluded 

781 

782 # Cnf = utils.get_conf() 

783 

784 Arguments = [ 

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

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

787 ] 

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

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

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

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

792 

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

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

795 

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

797 usage() 

798 

799 if Options["Html-Output"]: 

800 global use_html 

801 use_html = True 

802 

803 for f in args: 

804 try: 

805 my_fd: IO[str] 

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

807 # Pipe output for each argument through less 

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

809 less_process = subprocess.Popen( 

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

811 ) 

812 less_fd = less_process.stdin 

813 assert less_fd is not None 

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

815 my_fd = less_fd 

816 else: 

817 less_fd = None 

818 my_fd = sys.stdout 

819 

820 try: 

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

822 my_fd.write(check_changes(f)) 

823 elif f.endswith(".deb") or f.endswith(".udeb"): 

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

825 # perhaps this should be a command line option? 

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

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

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

829 else: 

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

831 finally: 

832 my_fd.write(output_package_relations()) 

833 if less_fd is not None: 

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

835 less_fd.close() 

836 less_process.wait() 

837 except OSError as e: 

838 if e.errno == errno.EPIPE: 

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

840 pass 

841 else: 

842 raise 

843 except KeyboardInterrupt: 

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

845 pass 

846 

847 

848@cache 

849def get_lintian_version() -> str: 

850 # eg. "Lintian v2.5.100" 

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

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

853 

854 

855####################################################################################### 

856 

857if __name__ == "__main__": 

858 main()