Coverage for dak/examine_package.py: 64%
431 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-03-14 12:19 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-03-14 12:19 +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:
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: 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)
541def do_lintian(filename: str) -> str:
542 cnf = Config()
543 if not cnf.find_b("Examine-Package::EnableLintian"):
544 return ""
546 cmd = []
548 user = cnf.get("Dinstall::UnprivUser") or None
549 if user is not None:
550 cmd.extend(["sudo", "-H", "-u", user])
552 color = "always"
553 if use_html:
554 color = "html"
556 cmd.extend(["lintian", "--show-overrides", "--color", color, "--", filename])
558 try:
559 return do_command(cmd, escaped=True)
560 except OSError as e:
561 return colour_output("Running lintian failed: %s" % (e), "error")
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 ""
569 title = f"lintian {get_lintian_version()} check for {os.path.basename(filename)}"
570 return foldable_output(title, elementnameprefix, do_lintian(filename))
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)
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
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
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()
596 return matched_member.name, matched_data
599def get_copyright(deb_filename: str) -> str:
600 global printed
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)
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
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 )
623 copyrightmd5 = hashlib.md5(cright).hexdigest()
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())
641def get_readme_source(dsc_filename: str) -> str:
642 with tempfile.TemporaryDirectory(prefix="dak-examine-package") as tempdir: 642 ↛ exitline 642 didn't return from function 'get_readme_source' because the return on line 653 wasn't executed
643 targetdir = os.path.join(tempdir, "source")
645 cmd = ("dpkg-source", "--no-check", "--no-copy", "-x", dsc_filename, targetdir)
646 try:
647 subprocess.check_output(cmd, stderr=subprocess.STDOUT)
648 except subprocess.CalledProcessError as e:
649 res = "How is education supposed to make me feel smarter? Besides, every time I learn something new, it pushes some\n old stuff out of my brain. Remember when I took that home winemaking course, and I forgot how to drive?\n"
650 res += "Error, couldn't extract source, WTF?\n"
651 res += "'dpkg-source -x' failed. return code: %s.\n\n" % (e.returncode)
652 res += e.output
653 return res
655 path = os.path.join(targetdir, "debian/README.source")
656 res = ""
657 if os.path.exists(path): 657 ↛ 661line 657 didn't jump to line 661 because the condition on line 657 was always true
658 with open(path, "r") as fh:
659 res += formatted_text(fh.read())
660 else:
661 res += "No README.source in this package\n\n"
663 return res
666def check_dsc(suite: str, dsc_filename: str, session=None) -> str:
667 dsc = read_changes_or_dsc(suite, dsc_filename, session)
668 dsc_basename = os.path.basename(dsc_filename)
669 cdsc = (
670 foldable_output(dsc_filename, "dsc", dsc, norow=True)
671 + "\n"
672 + foldable_lintian_output("source-lintian", dsc_filename)
673 + "\n"
674 + foldable_output(
675 "README.source for %s" % dsc_basename,
676 "source-readmesource",
677 get_readme_source(dsc_filename),
678 )
679 )
680 return cdsc
683def check_deb(suite: str, deb_filename: str, session=None) -> str:
684 filename = os.path.basename(deb_filename)
685 packagename = filename.split("_")[0]
687 if filename.endswith(".udeb"): 687 ↛ 688line 687 didn't jump to line 688 because the condition on line 687 was never true
688 is_a_udeb = 1
689 else:
690 is_a_udeb = 0
692 result = (
693 foldable_output(
694 "control file for %s" % (filename),
695 "binary-%s-control" % packagename,
696 output_deb_info(suite, deb_filename, packagename, session),
697 norow=True,
698 )
699 + "\n"
700 )
702 if is_a_udeb: 702 ↛ 703line 702 didn't jump to line 703 because the condition on line 702 was never true
703 result += (
704 foldable_output(
705 "skipping lintian check for udeb", "binary-%s-lintian" % packagename, ""
706 )
707 + "\n"
708 )
709 else:
710 result += (
711 foldable_lintian_output(f"binary-{packagename}-lintian", deb_filename)
712 + "\n"
713 )
715 result += (
716 foldable_output(
717 "contents of %s" % (filename),
718 "binary-%s-contents" % packagename,
719 do_command(["dpkg", "-c", deb_filename]),
720 )
721 + "\n"
722 )
724 if is_a_udeb: 724 ↛ 725line 724 didn't jump to line 725 because the condition on line 724 was never true
725 result += (
726 foldable_output(
727 "skipping copyright for udeb", "binary-%s-copyright" % packagename, ""
728 )
729 + "\n"
730 )
731 else:
732 result += (
733 foldable_output(
734 "copyright of %s" % (filename),
735 "binary-%s-copyright" % packagename,
736 get_copyright(deb_filename),
737 )
738 + "\n"
739 )
741 return result
744# Read a file, strip the signature and return the modified contents as
745# a string.
748def strip_pgp_signature(filename: str) -> str:
749 with open(filename, "rb") as f:
750 data = f.read()
751 signedfile = SignedFile(data, keyrings=(), require_signature=False)
752 return signedfile.contents.decode()
755def display_changes(suite: str, changes_filename: str) -> str:
756 global printed
757 changes = read_changes_or_dsc(suite, changes_filename)
758 printed.copyrights = {}
759 return foldable_output(changes_filename, "changes", changes, norow=True)
762def check_changes(changes_filename: str) -> str:
763 try:
764 changes = utils.parse_changes(changes_filename)
765 except UnicodeDecodeError:
766 utils.warn("Encoding problem with changes file %s" % (changes_filename))
767 output = display_changes(changes["distribution"], changes_filename)
769 files = utils.build_file_list(changes)
770 for f in files.keys():
771 if f.endswith(".deb") or f.endswith(".udeb"):
772 output += check_deb(changes["distribution"], f)
773 if f.endswith(".dsc"):
774 output += check_dsc(changes["distribution"], f)
775 # else: => byhand
776 return output
779def main() -> None:
780 global Cnf, db_files, waste, excluded
782 # Cnf = utils.get_conf()
784 Arguments = [
785 ("h", "help", "Examine-Package::Options::Help"),
786 ("H", "html-output", "Examine-Package::Options::Html-Output"),
787 ]
788 for i in ["Help", "Html-Output", "partial-html"]:
789 key = "Examine-Package::Options::%s" % i
790 if key not in Cnf: 790 ↛ 788line 790 didn't jump to line 788 because the condition on line 790 was always true
791 Cnf[key] = "" # type: ignore[index]
793 args = apt_pkg.parse_commandline(Cnf, Arguments, sys.argv) # type: ignore[attr-defined]
794 Options = Cnf.subtree("Examine-Package::Options") # type: ignore[attr-defined]
796 if Options["Help"]: 796 ↛ 799line 796 didn't jump to line 799 because the condition on line 796 was always true
797 usage()
799 if Options["Html-Output"]:
800 global use_html
801 use_html = True
803 for f in args:
804 try:
805 my_fd: IO[str]
806 if not Options["Html-Output"]:
807 # Pipe output for each argument through less
808 less_cmd = ("less", "-r", "-")
809 less_process = subprocess.Popen(
810 less_cmd, stdin=subprocess.PIPE, bufsize=0, text=True
811 )
812 less_fd = less_process.stdin
813 assert less_fd is not None
814 # -R added to display raw control chars for colour
815 my_fd = less_fd
816 else:
817 less_fd = None
818 my_fd = sys.stdout
820 try:
821 if f.endswith(".changes"):
822 my_fd.write(check_changes(f))
823 elif f.endswith(".deb") or f.endswith(".udeb"):
824 # default to unstable when we don't have a .changes file
825 # perhaps this should be a command line option?
826 my_fd.write(check_deb("unstable", f))
827 elif f.endswith(".dsc"):
828 my_fd.write(check_dsc("unstable", f))
829 else:
830 utils.fubar("Unrecognised file type: '%s'." % (f))
831 finally:
832 my_fd.write(output_package_relations())
833 if less_fd is not None:
834 # Reset stdout here so future less invocations aren't FUBAR
835 less_fd.close()
836 less_process.wait()
837 except OSError as e:
838 if e.errno == errno.EPIPE:
839 utils.warn("[examine-package] Caught EPIPE; skipping.")
840 pass
841 else:
842 raise
843 except KeyboardInterrupt:
844 utils.warn("[examine-package] Caught C-c; skipping.")
845 pass
848@cache
849def get_lintian_version() -> str:
850 # eg. "Lintian v2.5.100"
851 val = subprocess.check_output(("lintian", "--version"), text=True)
852 return val.split(" v")[-1].strip()
855#######################################################################################
857if __name__ == "__main__":
858 main()