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
« prev ^ index » next coverage.py v7.6.0, created at 2026-01-04 16:18 +0000
1#! /usr/bin/env python3
3"""
4Script to automate some parts of checking NEW packages
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.
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"""
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.
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.
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
30################################################################################
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? :)
43################################################################################
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
58import apt_pkg
59from sqlalchemy import sql
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)
75################################################################################
77Cnf = utils.get_conf()
79printed = threading.local()
80printed.copyrights = {}
81package_relations: dict[str, dict[str, str]] = (
82 {}
83) #: Store relations of packages for later output
85# default is to not output html.
86use_html = False
88################################################################################
91def usage(exit_code=0) -> NoReturn:
92 print(
93 """Usage: dak examine-package [PACKAGE]...
94Check NEW package(s).
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
100PACKAGE can be a .changes, .dsc, .deb or .udeb filename."""
101 )
103 sys.exit(exit_code)
106################################################################################
107# probably xml.sax.saxutils would work as well
110def escape_if_needed(s: str) -> str:
111 if use_html:
112 return html.escape(s)
113 else:
114 return s
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)
132# Colour definitions, 'end' isn't really for use
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}
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}
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"])
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
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
189def output_row(s: str) -> str:
190 if use_html:
191 return """<tr><td>""" + s + """</td></tr>"""
192 else:
193 return s
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)
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
228################################################################################
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
240def get_or_list(depend: str) -> list[str]:
241 or_list = depend.split("|")
242 return or_list
245def get_comma_list(depend: str) -> list[str]:
246 dep_list = depend.split(",")
247 return dep_list
250type Depends = list[list[dict[str, str]]]
253def split_depends(d_str: str) -> Depends:
254 # creates a list of lists of dictionaries of depends (package,version relation)
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 ]
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 = ""
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
280 control_keys = list(control.keys())
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)
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)
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)
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"]
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")
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)
322 return (
323 control,
324 control_keys,
325 section,
326 predepends,
327 depends,
328 recommends,
329 arch,
330 maintainer,
331 )
334def read_changes_or_dsc(suite: str, filename: str, session=None) -> str:
335 dsc = {}
337 try:
338 dsc = utils.parse_changes(filename, dsc_file=True)
339 except:
340 return formatted_text("can't parse .dsc control info")
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))
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])
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
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
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]
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.
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"])
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
449def output_package_relations() -> str:
450 """
451 Output the package relations, if there is more than one package checked in this run.
452 """
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 )
468 package_relations.clear()
469 result = foldable_output("Package relations", "relations", to_print)
470 package_relations.clear()
471 return result
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)
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
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)
541def do_lintian(filename: str) -> str:
542 cnf = Config()
543 cmd = []
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])
549 color = "always"
550 if use_html:
551 color = "html"
553 cmd.extend(["lintian", "--show-overrides", "--color", color, "--", filename])
555 try:
556 return do_command(cmd, escaped=True)
557 except OSError as e:
558 return colour_output("Running lintian failed: %s" % (e), "error")
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)
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
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
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()
584 return matched_member.name, matched_data
587def get_copyright(deb_filename: str) -> str:
588 global printed
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)
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
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 )
611 copyrightmd5 = hashlib.md5(cright).hexdigest()
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())
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")
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
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"
651 return res
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
675def check_deb(suite: str, deb_filename: str, session=None) -> str:
676 filename = os.path.basename(deb_filename)
677 packagename = filename.split("_")[0]
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
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 )
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 )
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 )
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 )
737 return result
740# Read a file, strip the signature and return the modified contents as
741# a string.
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()
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)
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)
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
775def main() -> None:
776 global Cnf, db_files, waste, excluded
778 # Cnf = utils.get_conf()
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]
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]
792 if Options["Help"]: 792 ↛ 795line 792 didn't jump to line 795 because the condition on line 792 was always true
793 usage()
795 if Options["Html-Output"]:
796 global use_html
797 use_html = True
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
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
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()
851#######################################################################################
853if __name__ == "__main__":
854 main()