Coverage for dak/examine_package.py: 69%

423 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-01-04 16:18 +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: 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true

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: 

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 cmd = [] 

544 

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

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

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

548 

549 color = "always" 

550 if use_html: 

551 color = "html" 

552 

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

554 

555 try: 

556 return do_command(cmd, escaped=True) 

557 except OSError as e: 

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

559 

560 

561def extract_one_file_from_deb( 

562 deb_filename: str, match: re.Pattern 

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

564 with tempfile.TemporaryFile() as tmpfh: 

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

566 subprocess.check_call(dpkg_cmd, stdout=tmpfh) 

567 

568 tmpfh.seek(0) 

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

570 matched_member = None 

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

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

573 matched_member = member 

574 break 

575 

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

577 return None, None 

578 

579 fh = tar.extractfile(matched_member) 

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

581 matched_data = fh.read() 

582 fh.close() 

583 

584 return matched_member.name, matched_data 

585 

586 

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

588 global printed 

589 

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

591 cright_path, cright = extract_one_file_from_deb(deb_filename, re_copyright) 

592 

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

594 return formatted_text( 

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

596 ) 

597 assert cright is not None 

598 

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

600 assert package_match is not None 

601 package = package_match.group("package") 

602 doc_directory_match = re_copyright.match(cright_path) 

603 assert doc_directory_match is not None 

604 doc_directory = doc_directory_match.group("package") 

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

606 return formatted_text( 

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

608 % (package, doc_directory) 

609 ) 

610 

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

612 

613 res = "" 

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

615 copyrightmd5 

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

617 res += formatted_text( 

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

619 % (printed.copyrights[copyrightmd5]) 

620 ) 

621 else: 

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

623 package, 

624 os.path.basename(deb_filename), 

625 ) 

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

627 

628 

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

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

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

632 

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

634 try: 

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

636 except subprocess.CalledProcessError as e: 

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

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

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

640 res += e.output 

641 return res 

642 

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

644 res = "" 

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

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

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

648 else: 

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

650 

651 return res 

652 

653 

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

655 dsc = read_changes_or_dsc(suite, dsc_filename, session) 

656 dsc_basename = os.path.basename(dsc_filename) 

657 cdsc = ( 

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

659 + "\n" 

660 + foldable_output( 

661 "lintian {} check for {}".format(get_lintian_version(), dsc_basename), 

662 "source-lintian", 

663 do_lintian(dsc_filename), 

664 ) 

665 + "\n" 

666 + foldable_output( 

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

668 "source-readmesource", 

669 get_readme_source(dsc_filename), 

670 ) 

671 ) 

672 return cdsc 

673 

674 

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

676 filename = os.path.basename(deb_filename) 

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

678 

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

680 is_a_udeb = 1 

681 else: 

682 is_a_udeb = 0 

683 

684 result = ( 

685 foldable_output( 

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

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

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

689 norow=True, 

690 ) 

691 + "\n" 

692 ) 

693 

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

695 result += ( 

696 foldable_output( 

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

698 ) 

699 + "\n" 

700 ) 

701 else: 

702 result += ( 

703 foldable_output( 

704 "lintian {} check for {}".format(get_lintian_version(), filename), 

705 "binary-%s-lintian" % packagename, 

706 do_lintian(deb_filename), 

707 ) 

708 + "\n" 

709 ) 

710 

711 result += ( 

712 foldable_output( 

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

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

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

716 ) 

717 + "\n" 

718 ) 

719 

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

721 result += ( 

722 foldable_output( 

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

724 ) 

725 + "\n" 

726 ) 

727 else: 

728 result += ( 

729 foldable_output( 

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

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

732 get_copyright(deb_filename), 

733 ) 

734 + "\n" 

735 ) 

736 

737 return result 

738 

739 

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

741# a string. 

742 

743 

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

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

746 data = f.read() 

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

748 return signedfile.contents.decode() 

749 

750 

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

752 global printed 

753 changes = read_changes_or_dsc(suite, changes_filename) 

754 printed.copyrights = {} 

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

756 

757 

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

759 try: 

760 changes = utils.parse_changes(changes_filename) 

761 except UnicodeDecodeError: 

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

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

764 

765 files = utils.build_file_list(changes) 

766 for f in files.keys(): 

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

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

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

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

771 # else: => byhand 

772 return output 

773 

774 

775def main() -> None: 

776 global Cnf, db_files, waste, excluded 

777 

778 # Cnf = utils.get_conf() 

779 

780 Arguments = [ 

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

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

783 ] 

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

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

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

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

788 

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

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

791 

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

793 usage() 

794 

795 if Options["Html-Output"]: 

796 global use_html 

797 use_html = True 

798 

799 for f in args: 

800 try: 

801 my_fd: IO[str] 

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

803 # Pipe output for each argument through less 

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

805 less_process = subprocess.Popen( 

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

807 ) 

808 less_fd = less_process.stdin 

809 assert less_fd is not None 

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

811 my_fd = less_fd 

812 else: 

813 less_fd = None 

814 my_fd = sys.stdout 

815 

816 try: 

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

818 my_fd.write(check_changes(f)) 

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

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

821 # perhaps this should be a command line option? 

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

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

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

825 else: 

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

827 finally: 

828 my_fd.write(output_package_relations()) 

829 if less_fd is not None: 

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

831 less_fd.close() 

832 less_process.wait() 

833 except OSError as e: 

834 if e.errno == errno.EPIPE: 

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

836 pass 

837 else: 

838 raise 

839 except KeyboardInterrupt: 

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

841 pass 

842 

843 

844@cache 

845def get_lintian_version() -> str: 

846 # eg. "Lintian v2.5.100" 

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

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

849 

850 

851####################################################################################### 

852 

853if __name__ == "__main__": 

854 main()