Coverage for dak/examine_package.py: 65%
430 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
1"""
2Script to automate some parts of checking NEW packages
4Most functions are written in a functional programming style. They
5return a string avoiding the side effect of directly printing the string
6to stdout. Those functions can be used in multithreaded parts of dak.
8@contact: Debian FTP Master <ftpmaster@debian.org>
9@copyright: 2000, 2001, 2002, 2003, 2006 James Troup <james@nocrew.org>
10@copyright: 2009 Joerg Jaspert <joerg@debian.org>
11@license: GNU General Public License version 2 or later
12"""
14# This program is free software; you can redistribute it and/or modify
15# it under the terms of the GNU General Public License as published by
16# the Free Software Foundation; either version 2 of the License, or
17# (at your option) any later version.
19# This program is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22# GNU General Public License for more details.
24# You should have received a copy of the GNU General Public License
25# along with this program; if not, write to the Free Software
26# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
28################################################################################
30# <Omnic> elmo wrote docs?!!?!?!?!?!?!
31# <aj> as if he wasn't scary enough before!!
32# * aj imagines a little red furry toy sitting hunched over a computer
33# tapping furiously and giggling to himself
34# <aj> eventually he stops, and his heads slowly spins around and you
35# see this really evil grin and then he sees you, and picks up a
36# knife from beside the keyboard and throws it at you, and as you
37# breathe your last breath, he starts giggling again
38# <aj> but i should be telling this to my psychiatrist, not you guys,
39# right? :)
41################################################################################
43import errno
44import hashlib
45import html
46import os
47import re
48import subprocess
49import sys
50import tarfile
51import tempfile
52import threading
53from functools import cache
54from typing import IO, NoReturn
56import apt_pkg
57from sqlalchemy import sql
59from daklib import sandbox, utils
60from daklib.config import Config
61from daklib.dbconn import DBConn, get_component_by_package_suite
62from daklib.gpg import SignedFile
63from daklib.regexes import (
64 re_contrib,
65 re_file_binary,
66 re_localhost,
67 re_newlinespace,
68 re_nonfree,
69 re_spacestrip,
70 re_version,
71)
73################################################################################
75Cnf = utils.get_conf()
77printed = threading.local()
78printed.copyrights = {}
79package_relations: dict[str, dict[str, str]] = (
80 {}
81) #: Store relations of packages for later output
83# default is to not output html.
84use_html = False
86################################################################################
89def usage(exit_code=0) -> NoReturn:
90 print(
91 """Usage: dak examine-package [PACKAGE]...
92Check NEW package(s).
94 -h, --help show this help and exit
95 -H, --html-output output html page with inspection result
96 -f, --file-name filename for the html page
98PACKAGE can be a .changes, .dsc, .deb or .udeb filename."""
99 )
101 sys.exit(exit_code)
104################################################################################
105# probably xml.sax.saxutils would work as well
108def escape_if_needed(s: str) -> str:
109 if use_html:
110 return html.escape(s)
111 else:
112 return s
115def headline(s: str, level=2, bodyelement: str | None = None) -> str:
116 if use_html:
117 if bodyelement: 117 ↛ 125line 117 didn't jump to line 125 because the condition on line 117 was always true
118 return """<thead>
119 <tr><th colspan="2" class="title" onclick="toggle('%(bodyelement)s', 'table-row-group', 'table-row-group')">%(title)s <span class="toggle-msg">(click to toggle)</span></th></tr>
120 </thead>\n""" % {
121 "bodyelement": bodyelement,
122 "title": html.escape(os.path.basename(s), quote=False),
123 }
124 else:
125 return "<h%d>%s</h%d>\n" % (level, html.escape(s, quote=False), level)
126 else:
127 return "---- %s ----\n" % (s)
130# Colour definitions, 'end' isn't really for use
132ansi_colours = {
133 "main": "\033[36m",
134 "contrib": "\033[33m",
135 "nonfree": "\033[31m",
136 "provides": "\033[35m",
137 "arch": "\033[32m",
138 "end": "\033[0m",
139 "bold": "\033[1m",
140 "maintainer": "\033[32m",
141 "distro": "\033[1m\033[41m",
142 "error": "\033[1m\033[41m",
143}
145html_colours = {
146 "main": ('<span style="color: green">', "</span>"),
147 "contrib": ('<span style="color: orange">', "</span>"),
148 "nonfree": ('<span style="color: red">', "</span>"),
149 "provides": ('<span style="color: magenta">', "</span>"),
150 "arch": ('<span style="color: green">', "</span>"),
151 "bold": ('<span style="font-weight: bold">', "</span>"),
152 "maintainer": ('<span style="color: green">', "</span>"),
153 "distro": ('<span style="font-weight: bold; background-color: red">', "</span>"),
154 "error": ('<span style="font-weight: bold; background-color: red">', "</span>"),
155}
158def colour_output(s: str, colour: str) -> str:
159 if use_html:
160 return "%s%s%s" % (
161 html_colours[colour][0],
162 html.escape(s, quote=False),
163 html_colours[colour][1],
164 )
165 else:
166 return "%s%s%s" % (ansi_colours[colour], s, ansi_colours["end"])
169def escaped_text(s: str, strip=False) -> str:
170 if use_html:
171 if strip:
172 s = s.strip()
173 return "<pre>%s</pre>" % (s)
174 else:
175 return s
178def formatted_text(s: str, strip=False) -> str:
179 if use_html:
180 if strip:
181 s = s.strip()
182 return "<pre>%s</pre>" % (html.escape(s, quote=False))
183 else:
184 return s
187def output_row(s: str) -> str:
188 if use_html:
189 return """<tr><td>""" + s + """</td></tr>"""
190 else:
191 return s
194def format_field(k: str, v: str) -> str:
195 if use_html:
196 return """<tr><td class="key">%s:</td><td class="val">%s</td></tr>""" % (k, v)
197 else:
198 return "%s: %s" % (k, v)
201def foldable_output(
202 title: str, elementnameprefix: str, content: str, norow=False
203) -> str:
204 d = {"elementnameprefix": elementnameprefix}
205 result = ""
206 if use_html:
207 result += (
208 """<div id="%(elementnameprefix)s-wrap"><a name="%(elementnameprefix)s"></a>
209 <table class="infobox rfc822">\n"""
210 % d
211 )
212 result += headline(title, bodyelement="%(elementnameprefix)s-body" % d)
213 if use_html:
214 result += (
215 """ <tbody id="%(elementnameprefix)s-body" class="infobody">\n""" % d
216 )
217 if norow:
218 result += content + "\n"
219 else:
220 result += output_row(content) + "\n"
221 if use_html:
222 result += """</tbody></table></div>"""
223 return result
226################################################################################
229def get_depends_parts(depend: str) -> dict[str, str]:
230 v_match = re_version.match(depend)
231 if v_match: 231 ↛ 234line 231 didn't jump to line 234 because the condition on line 231 was always true
232 d_parts = {"name": v_match.group(1), "version": v_match.group(2)}
233 else:
234 d_parts = {"name": depend, "version": ""}
235 return d_parts
238def get_or_list(depend: str) -> list[str]:
239 or_list = depend.split("|")
240 return or_list
243def get_comma_list(depend: str) -> list[str]:
244 dep_list = depend.split(",")
245 return dep_list
248type Depends = list[list[dict[str, str]]]
251def split_depends(d_str: str) -> Depends:
252 # creates a list of lists of dictionaries of depends (package,version relation)
254 d_str = re_spacestrip.sub("", d_str)
255 return [
256 [get_depends_parts(or_list) for or_list in get_or_list(comma_list)]
257 for comma_list in get_comma_list(d_str)
258 ]
261def read_control(
262 filename: str,
263) -> tuple[apt_pkg.TagSection, list[str], str, Depends, Depends, Depends, str, str]:
264 recommends: Depends = []
265 predepends: Depends = []
266 depends: Depends = []
267 section = ""
268 maintainer = ""
269 arch = ""
271 try:
272 extracts = utils.deb_extract_control(filename)
273 control = apt_pkg.TagSection(extracts)
274 except:
275 print(formatted_text("can't parse control info"))
276 raise
278 control_keys = list(control.keys())
280 if "Pre-Depends" in control: 280 ↛ 281line 280 didn't jump to line 281 because the condition on line 280 was never true
281 predepends_str = control["Pre-Depends"]
282 predepends = split_depends(predepends_str)
284 if "Depends" in control: 284 ↛ 285line 284 didn't jump to line 285 because the condition on line 284 was never true
285 depends_str = control["Depends"]
286 # create list of dependancy lists
287 depends = split_depends(depends_str)
289 if "Recommends" in control: 289 ↛ 290line 289 didn't jump to line 290 because the condition on line 289 was never true
290 recommends_str = control["Recommends"]
291 recommends = split_depends(recommends_str)
293 if "Section" in control: 293 ↛ 307line 293 didn't jump to line 307 because the condition on line 293 was always true
294 section_str = control["Section"]
296 c_match = re_contrib.search(section_str)
297 nf_match = re_nonfree.search(section_str)
298 if c_match: 298 ↛ 300line 298 didn't jump to line 300 because the condition on line 298 was never true
299 # contrib colour
300 section = colour_output(section_str, "contrib")
301 elif nf_match: 301 ↛ 303line 301 didn't jump to line 303 because the condition on line 301 was never true
302 # non-free colour
303 section = colour_output(section_str, "nonfree")
304 else:
305 # main
306 section = colour_output(section_str, "main")
307 if "Architecture" in control: 307 ↛ 311line 307 didn't jump to line 311 because the condition on line 307 was always true
308 arch_str = control["Architecture"]
309 arch = colour_output(arch_str, "arch")
311 if "Maintainer" in control: 311 ↛ 320line 311 didn't jump to line 320 because the condition on line 311 was always true
312 maintainer = control["Maintainer"]
313 localhost = re_localhost.search(maintainer)
314 if localhost: 314 ↛ 316line 314 didn't jump to line 316 because the condition on line 314 was never true
315 # highlight bad email
316 maintainer = colour_output(maintainer, "maintainer")
317 else:
318 maintainer = escape_if_needed(maintainer)
320 return (
321 control,
322 control_keys,
323 section,
324 predepends,
325 depends,
326 recommends,
327 arch,
328 maintainer,
329 )
332def read_changes_or_dsc(suite: str, filename: str, session=None) -> str:
333 dsc = {}
335 try:
336 dsc = utils.parse_changes(filename, dsc_file=True)
337 except:
338 return formatted_text("can't parse .dsc control info")
340 filecontents = strip_pgp_signature(filename)
341 keysinorder = []
342 for line in filecontents.split("\n"):
343 m = re.match(r"([-a-zA-Z0-9]*):", line)
344 if m:
345 keysinorder.append(m.group(1))
347 for k in list(dsc.keys()):
348 if k in ("build-depends", "build-depends-indep"):
349 dsc[k] = create_depends_string(suite, split_depends(dsc[k]), session)
350 elif k == "architecture":
351 if dsc["architecture"] != "any": 351 ↛ 347line 351 didn't jump to line 347 because the condition on line 351 was always true
352 dsc["architecture"] = colour_output(dsc["architecture"], "arch")
353 elif k == "distribution":
354 if dsc["distribution"] not in ("unstable", "experimental"): 354 ↛ 355line 354 didn't jump to line 355 because the condition on line 354 was never true
355 dsc["distribution"] = colour_output(dsc["distribution"], "distro")
356 elif k in ("files", "changes", "description"):
357 if use_html:
358 dsc[k] = formatted_text(dsc[k], strip=True)
359 else:
360 dsc[k] = (
361 "\n" + "\n".join(" " + x for x in dsc[k].split("\n"))
362 ).rstrip()
363 else:
364 dsc[k] = escape_if_needed(dsc[k])
366 filecontents = (
367 "\n".join(
368 format_field(x, dsc[x.lower()])
369 for x in keysinorder
370 if not x.lower().startswith("checksums-")
371 )
372 + "\n"
373 )
374 return filecontents
377def get_provides(suite: str) -> set[str]:
378 provides: set[str] = set()
379 session = DBConn().session()
380 query = """SELECT DISTINCT value
381 FROM binaries_metadata m
382 JOIN bin_associations b
383 ON b.bin = m.bin_id
384 WHERE key_id = (
385 SELECT key_id
386 FROM metadata_keys
387 WHERE key = 'Provides' )
388 AND b.suite = (
389 SELECT id
390 FROM suite
391 WHERE suite_name = :suite
392 OR codename = :suite)"""
393 for p in session.execute(sql.text(query), {"suite": suite}).scalars(): 393 ↛ 394line 393 didn't jump to line 394 because the loop on line 393 never started
394 for e in p.split(","):
395 provides.add(e.strip())
396 session.close()
397 return provides
400def create_depends_string(suite: str, depends_tree: Depends, session=None) -> str:
401 result = ""
402 if suite == "experimental": 402 ↛ 403line 402 didn't jump to line 403 because the condition on line 402 was never true
403 suite_list = ["experimental", "unstable"]
404 else:
405 suite_list = [suite]
407 provides: set[str] = set()
408 comma_count = 1
409 for item in depends_tree:
410 if comma_count >= 2: 410 ↛ 411line 410 didn't jump to line 411 because the condition on line 410 was never true
411 result += ", "
412 or_count = 1
413 for d in item:
414 if or_count >= 2: 414 ↛ 415line 414 didn't jump to line 415 because the condition on line 414 was never true
415 result += " | "
416 # doesn't do version lookup yet.
418 component = get_component_by_package_suite(
419 d["name"], suite_list, session=session
420 )
421 if component is not None: 421 ↛ 422line 421 didn't jump to line 422 because the condition on line 421 was never true
422 adepends = d["name"]
423 if d["version"] != "":
424 adepends += " (%s)" % (d["version"])
426 if component == "contrib":
427 result += colour_output(adepends, "contrib")
428 elif component in ("non-free-firmware", "non-free"):
429 result += colour_output(adepends, "nonfree")
430 else:
431 result += colour_output(adepends, "main")
432 else:
433 adepends = d["name"]
434 if d["version"] != "": 434 ↛ 436line 434 didn't jump to line 436 because the condition on line 434 was always true
435 adepends += " (%s)" % (d["version"])
436 if not provides: 436 ↛ 438line 436 didn't jump to line 438 because the condition on line 436 was always true
437 provides = get_provides(suite)
438 if d["name"] in provides: 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true
439 result += colour_output(adepends, "provides")
440 else:
441 result += colour_output(adepends, "bold")
442 or_count += 1
443 comma_count += 1
444 return result
447def output_package_relations() -> str:
448 """
449 Output the package relations, if there is more than one package checked in this run.
450 """
452 if len(package_relations) < 2: 452 ↛ 457line 452 didn't jump to line 457 because the condition on line 452 was always true
453 # Only list something if we have more than one binary to compare
454 package_relations.clear()
455 result = ""
456 else:
457 to_print = ""
458 for package, relations in package_relations.items():
459 for relation, value in relations.items():
460 to_print += "%-15s: (%s) %s\n" % (
461 package,
462 relation,
463 value,
464 )
466 package_relations.clear()
467 result = foldable_output("Package relations", "relations", to_print)
468 package_relations.clear()
469 return result
472def output_deb_info(suite: str, filename: str, packagename: str, session=None) -> str:
473 (
474 control,
475 control_keys,
476 section,
477 predepends,
478 depends,
479 recommends,
480 arch,
481 maintainer,
482 ) = read_control(filename)
484 if control == "": 484 ↛ 485line 484 didn't jump to line 485 because the condition on line 484 was never true
485 return formatted_text("no control info")
486 to_print = ""
487 if packagename not in package_relations: 487 ↛ 489line 487 didn't jump to line 489 because the condition on line 487 was always true
488 package_relations[packagename] = {}
489 for key in control_keys:
490 if key == "Source": 490 ↛ 491line 490 didn't jump to line 491 because the condition on line 490 was never true
491 field_value = escape_if_needed(control.find(key))
492 if use_html:
493 field_value = '<a href="https://tracker.debian.org/pkg/{0}" rel="nofollow">{0}</a>'.format(
494 field_value
495 )
496 elif key == "Pre-Depends": 496 ↛ 497line 496 didn't jump to line 497 because the condition on line 496 was never true
497 field_value = create_depends_string(suite, predepends, session)
498 package_relations[packagename][key] = field_value
499 elif key == "Depends": 499 ↛ 500line 499 didn't jump to line 500 because the condition on line 499 was never true
500 field_value = create_depends_string(suite, depends, session)
501 package_relations[packagename][key] = field_value
502 elif key == "Recommends": 502 ↛ 503line 502 didn't jump to line 503 because the condition on line 502 was never true
503 field_value = create_depends_string(suite, recommends, session)
504 package_relations[packagename][key] = field_value
505 elif key == "Section":
506 field_value = section
507 elif key == "Architecture":
508 field_value = arch
509 elif key == "Maintainer":
510 field_value = maintainer
511 elif key in ("Homepage", "Vcs-Browser"): 511 ↛ 512line 511 didn't jump to line 512 because the condition on line 511 was never true
512 field_value = escape_if_needed(control.find(key))
513 if use_html:
514 field_value = '<a href="%s" rel="nofollow">%s</a>' % (
515 field_value,
516 field_value,
517 )
518 elif key == "Description":
519 if use_html:
520 field_value = formatted_text(control.find(key), strip=True)
521 else:
522 desc = control.find(key)
523 desc = re_newlinespace.sub("\n ", desc)
524 field_value = escape_if_needed(desc)
525 else:
526 field_value = escape_if_needed(control.find(key))
527 to_print += " " + format_field(key, field_value) + "\n"
528 return to_print
531def do_command(command: list[str], escaped=False) -> str:
532 result = subprocess.run(command, stdout=subprocess.PIPE, text=True)
533 if escaped: 533 ↛ 534line 533 didn't jump to line 534 because the condition on line 533 was never true
534 return escaped_text(result.stdout)
535 else:
536 return formatted_text(result.stdout)
539def do_lintian(filename: str) -> str:
540 cnf = Config()
541 if not cnf.find_b("Examine-Package::EnableLintian"):
542 return ""
544 cmd = []
546 user = cnf.get("Dinstall::UnprivUser") or None
547 if user is not None:
548 cmd.extend(["sudo", "-H", "-u", user])
550 color = "always"
551 if use_html:
552 color = "html"
554 cmd.extend(["lintian", "--show-overrides", "--color", color, "--", filename])
556 try:
557 return do_command(cmd, escaped=True)
558 except OSError as e:
559 return colour_output("Running lintian failed: %s" % (e), "error")
562def foldable_lintian_output(elementnameprefix: str, filename: str) -> str:
563 cnf = Config()
564 if not cnf.find_b("Examine-Package::EnableLintian"): 564 ↛ 567line 564 didn't jump to line 567 because the condition on line 564 was always true
565 return ""
567 title = f"lintian {get_lintian_version()} check for {os.path.basename(filename)}"
568 return foldable_output(title, elementnameprefix, do_lintian(filename))
571def extract_one_file_from_deb(
572 deb_filename: str, match: re.Pattern
573) -> tuple[str, bytes] | tuple[None, None]:
574 with tempfile.TemporaryFile() as tmpfh:
575 dpkg_cmd = ("dpkg-deb", "--fsys-tarfile", deb_filename)
576 subprocess.check_call(dpkg_cmd, stdout=tmpfh)
578 tmpfh.seek(0)
579 with tarfile.open(fileobj=tmpfh, mode="r") as tar:
580 matched_member = None
581 for member in tar: 581 ↛ 586line 581 didn't jump to line 586 because the loop on line 581 didn't complete
582 if member.isfile() and match.match(member.name):
583 matched_member = member
584 break
586 if not matched_member: 586 ↛ 587line 586 didn't jump to line 587 because the condition on line 586 was never true
587 return None, None
589 fh = tar.extractfile(matched_member)
590 assert fh is not None # checked for regular file with `member.isfile()`
591 matched_data = fh.read()
592 fh.close()
594 return matched_member.name, matched_data
597def get_copyright(deb_filename: str) -> str:
598 global printed
600 re_copyright = re.compile(r"\./usr(/share)?/doc/(?P<package>[^/]+)/copyright")
601 cright_path, cright = extract_one_file_from_deb(deb_filename, re_copyright)
603 if not cright_path: 603 ↛ 604line 603 didn't jump to line 604 because the condition on line 603 was never true
604 return formatted_text(
605 "WARNING: No copyright found, please check package manually."
606 )
607 assert cright is not None
609 package_match = re_file_binary.match(os.path.basename(deb_filename))
610 assert package_match is not None
611 package = package_match.group("package")
612 doc_directory_match = re_copyright.match(cright_path)
613 assert doc_directory_match is not None
614 doc_directory = doc_directory_match.group("package")
615 if package != doc_directory: 615 ↛ 616line 615 didn't jump to line 616 because the condition on line 615 was never true
616 return formatted_text(
617 "WARNING: wrong doc directory (expected %s, got %s)."
618 % (package, doc_directory)
619 )
621 copyrightmd5 = hashlib.md5(cright).hexdigest()
623 res = ""
624 if copyrightmd5 in printed.copyrights and printed.copyrights[ 624 ↛ 627line 624 didn't jump to line 627 because the condition on line 624 was never true
625 copyrightmd5
626 ] != "%s (%s)" % (package, os.path.basename(deb_filename)):
627 res += formatted_text(
628 "NOTE: Copyright is the same as %s.\n\n"
629 % (printed.copyrights[copyrightmd5])
630 )
631 else:
632 printed.copyrights[copyrightmd5] = "%s (%s)" % (
633 package,
634 os.path.basename(deb_filename),
635 )
636 return res + formatted_text(cright.decode())
639def get_readme_source(dsc_filename: str) -> str:
640 with tempfile.TemporaryDirectory(prefix="dak-examine-package") as tempdir: 640 ↛ exitline 640 didn't return from function 'get_readme_source' because the return on line 659 wasn't executed
641 targetdir = os.path.join(tempdir, "source")
643 cmd = ("dpkg-source", "--no-check", "--no-copy", "-x", dsc_filename, targetdir)
644 try:
645 sandbox.run(
646 cmd,
647 sandbox=sandbox.Sandbox(
648 extra_read_write_paths=[tempdir, os.environ.get("TMPDIR", "/tmp")],
649 ),
650 stdout=subprocess.PIPE,
651 stderr=subprocess.STDOUT,
652 check=True,
653 )
654 except subprocess.CalledProcessError as e:
655 res = "How is education supposed to make me feel smarter? Besides, every time I learn something new, it pushes some\n old stuff out of my brain. Remember when I took that home winemaking course, and I forgot how to drive?\n"
656 res += "Error, couldn't extract source, WTF?\n"
657 res += "'dpkg-source -x' failed. return code: %s.\n\n" % (e.returncode)
658 res += e.output
659 return res
660 utils.remove_unsafe_symlinks(targetdir)
662 path = utils.resolve_relative_path(targetdir, "debian/README.source")
663 res = ""
664 if os.path.exists(path): 664 ↛ 668line 664 didn't jump to line 668 because the condition on line 664 was always true
665 with open(path, "r") as fh:
666 res += formatted_text(fh.read())
667 else:
668 res += "No README.source in this package\n\n"
670 return res
673def check_dsc(suite: str, dsc_filename: str, session=None) -> str:
674 dsc = read_changes_or_dsc(suite, dsc_filename, session)
675 dsc_basename = os.path.basename(dsc_filename)
676 cdsc = (
677 foldable_output(dsc_filename, "dsc", dsc, norow=True)
678 + "\n"
679 + foldable_lintian_output("source-lintian", dsc_filename)
680 + "\n"
681 + foldable_output(
682 "README.source for %s" % dsc_basename,
683 "source-readmesource",
684 get_readme_source(dsc_filename),
685 )
686 )
687 return cdsc
690def check_deb(suite: str, deb_filename: str, session=None) -> str:
691 filename = os.path.basename(deb_filename)
692 packagename = filename.split("_")[0]
694 if filename.endswith(".udeb"): 694 ↛ 695line 694 didn't jump to line 695 because the condition on line 694 was never true
695 is_a_udeb = 1
696 else:
697 is_a_udeb = 0
699 result = (
700 foldable_output(
701 "control file for %s" % (filename),
702 "binary-%s-control" % packagename,
703 output_deb_info(suite, deb_filename, packagename, session),
704 norow=True,
705 )
706 + "\n"
707 )
709 if is_a_udeb: 709 ↛ 710line 709 didn't jump to line 710 because the condition on line 709 was never true
710 result += (
711 foldable_output(
712 "skipping lintian check for udeb", "binary-%s-lintian" % packagename, ""
713 )
714 + "\n"
715 )
716 else:
717 result += (
718 foldable_lintian_output(f"binary-{packagename}-lintian", deb_filename)
719 + "\n"
720 )
722 result += (
723 foldable_output(
724 "contents of %s" % (filename),
725 "binary-%s-contents" % packagename,
726 do_command(["dpkg", "-c", deb_filename]),
727 )
728 + "\n"
729 )
731 if is_a_udeb: 731 ↛ 732line 731 didn't jump to line 732 because the condition on line 731 was never true
732 result += (
733 foldable_output(
734 "skipping copyright for udeb", "binary-%s-copyright" % packagename, ""
735 )
736 + "\n"
737 )
738 else:
739 result += (
740 foldable_output(
741 "copyright of %s" % (filename),
742 "binary-%s-copyright" % packagename,
743 get_copyright(deb_filename),
744 )
745 + "\n"
746 )
748 return result
751# Read a file, strip the signature and return the modified contents as
752# a string.
755def strip_pgp_signature(filename: str) -> str:
756 with open(filename, "rb") as f:
757 data = f.read()
758 signedfile = SignedFile(data, keyrings=(), require_signature=False)
759 return signedfile.contents.decode()
762def display_changes(suite: str, changes_filename: str) -> str:
763 global printed
764 changes = read_changes_or_dsc(suite, changes_filename)
765 printed.copyrights = {}
766 return foldable_output(changes_filename, "changes", changes, norow=True)
769def check_changes(changes_filename: str) -> str:
770 try:
771 changes = utils.parse_changes(changes_filename)
772 except UnicodeDecodeError:
773 utils.warn("Encoding problem with changes file %s" % (changes_filename))
774 output = display_changes(changes["distribution"], changes_filename)
776 files = utils.build_file_list(changes)
777 for f in files.keys():
778 if f.endswith((".deb", ".udeb")):
779 output += check_deb(changes["distribution"], f)
780 if f.endswith(".dsc"):
781 output += check_dsc(changes["distribution"], f)
782 # else: => byhand
783 return output
786def main() -> None:
787 global Cnf, db_files, waste, excluded
789 # Cnf = utils.get_conf()
791 Arguments = [
792 ("h", "help", "Examine-Package::Options::Help"),
793 ("H", "html-output", "Examine-Package::Options::Html-Output"),
794 ]
795 for i in ["Help", "Html-Output", "partial-html"]:
796 key = "Examine-Package::Options::%s" % i
797 if key not in Cnf: 797 ↛ 795line 797 didn't jump to line 795 because the condition on line 797 was always true
798 Cnf[key] = "" # type: ignore[index]
800 args = apt_pkg.parse_commandline(Cnf, Arguments, sys.argv) # type: ignore[attr-defined]
801 Options = Cnf.subtree("Examine-Package::Options") # type: ignore[attr-defined]
803 if Options["Help"]: 803 ↛ 806line 803 didn't jump to line 806 because the condition on line 803 was always true
804 usage()
806 if Options["Html-Output"]:
807 global use_html
808 use_html = True
810 for f in args:
811 try:
812 my_fd: IO[str]
813 if not Options["Html-Output"]:
814 # Pipe output for each argument through less
815 less_cmd = ("less", "-r", "-")
816 less_process = subprocess.Popen(
817 less_cmd, stdin=subprocess.PIPE, bufsize=0, text=True
818 )
819 less_fd = less_process.stdin
820 assert less_fd is not None
821 # -R added to display raw control chars for colour
822 my_fd = less_fd
823 else:
824 less_fd = None
825 my_fd = sys.stdout
827 try:
828 if f.endswith(".changes"):
829 my_fd.write(check_changes(f))
830 elif f.endswith((".deb", ".udeb")):
831 # default to unstable when we don't have a .changes file
832 # perhaps this should be a command line option?
833 my_fd.write(check_deb("unstable", f))
834 elif f.endswith(".dsc"):
835 my_fd.write(check_dsc("unstable", f))
836 else:
837 utils.fubar("Unrecognised file type: '%s'." % (f))
838 finally:
839 my_fd.write(output_package_relations())
840 if less_fd is not None:
841 # Reset stdout here so future less invocations aren't FUBAR
842 less_fd.close()
843 less_process.wait()
844 except OSError as e:
845 if e.errno == errno.EPIPE:
846 utils.warn("[examine-package] Caught EPIPE; skipping.")
847 else:
848 raise
849 except KeyboardInterrupt:
850 utils.warn("[examine-package] Caught C-c; skipping.")
853@cache
854def get_lintian_version() -> str:
855 # eg. "Lintian v2.5.100"
856 val = subprocess.check_output(("lintian", "--version"), text=True)
857 return val.split(" v")[-1].strip()