Coverage for dak/examine_package.py: 64%

432 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-05-10 21:38 +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 sandbox, 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 661 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 sandbox.run( 

648 cmd, 

649 sandbox=sandbox.Sandbox( 

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

651 ), 

652 stdout=subprocess.PIPE, 

653 stderr=subprocess.STDOUT, 

654 check=True, 

655 ) 

656 except subprocess.CalledProcessError as e: 

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

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

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

660 res += e.output 

661 return res 

662 utils.remove_unsafe_symlinks(targetdir) 

663 

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

665 res = "" 

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

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

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

669 else: 

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

671 

672 return res 

673 

674 

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

676 dsc = read_changes_or_dsc(suite, dsc_filename, session) 

677 dsc_basename = os.path.basename(dsc_filename) 

678 cdsc = ( 

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

680 + "\n" 

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

682 + "\n" 

683 + foldable_output( 

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

685 "source-readmesource", 

686 get_readme_source(dsc_filename), 

687 ) 

688 ) 

689 return cdsc 

690 

691 

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

693 filename = os.path.basename(deb_filename) 

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

695 

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

697 is_a_udeb = 1 

698 else: 

699 is_a_udeb = 0 

700 

701 result = ( 

702 foldable_output( 

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

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

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

706 norow=True, 

707 ) 

708 + "\n" 

709 ) 

710 

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

712 result += ( 

713 foldable_output( 

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

715 ) 

716 + "\n" 

717 ) 

718 else: 

719 result += ( 

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

721 + "\n" 

722 ) 

723 

724 result += ( 

725 foldable_output( 

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

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

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

729 ) 

730 + "\n" 

731 ) 

732 

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

734 result += ( 

735 foldable_output( 

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

737 ) 

738 + "\n" 

739 ) 

740 else: 

741 result += ( 

742 foldable_output( 

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

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

745 get_copyright(deb_filename), 

746 ) 

747 + "\n" 

748 ) 

749 

750 return result 

751 

752 

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

754# a string. 

755 

756 

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

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

759 data = f.read() 

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

761 return signedfile.contents.decode() 

762 

763 

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

765 global printed 

766 changes = read_changes_or_dsc(suite, changes_filename) 

767 printed.copyrights = {} 

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

769 

770 

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

772 try: 

773 changes = utils.parse_changes(changes_filename) 

774 except UnicodeDecodeError: 

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

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

777 

778 files = utils.build_file_list(changes) 

779 for f in files.keys(): 

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

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

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

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

784 # else: => byhand 

785 return output 

786 

787 

788def main() -> None: 

789 global Cnf, db_files, waste, excluded 

790 

791 # Cnf = utils.get_conf() 

792 

793 Arguments = [ 

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

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

796 ] 

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

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

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

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

801 

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

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

804 

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

806 usage() 

807 

808 if Options["Html-Output"]: 

809 global use_html 

810 use_html = True 

811 

812 for f in args: 

813 try: 

814 my_fd: IO[str] 

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

816 # Pipe output for each argument through less 

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

818 less_process = subprocess.Popen( 

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

820 ) 

821 less_fd = less_process.stdin 

822 assert less_fd is not None 

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

824 my_fd = less_fd 

825 else: 

826 less_fd = None 

827 my_fd = sys.stdout 

828 

829 try: 

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

831 my_fd.write(check_changes(f)) 

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

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

834 # perhaps this should be a command line option? 

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

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

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

838 else: 

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

840 finally: 

841 my_fd.write(output_package_relations()) 

842 if less_fd is not None: 

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

844 less_fd.close() 

845 less_process.wait() 

846 except OSError as e: 

847 if e.errno == errno.EPIPE: 

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

849 pass 

850 else: 

851 raise 

852 except KeyboardInterrupt: 

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

854 pass 

855 

856 

857@cache 

858def get_lintian_version() -> str: 

859 # eg. "Lintian v2.5.100" 

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

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

862 

863 

864####################################################################################### 

865 

866if __name__ == "__main__": 

867 main()