primarysource 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- primarysource/__init__.py +5 -0
- primarysource/bbf/__init__.py +56 -0
- primarysource/bbf/_xml.py +63 -0
- primarysource/bbf/catalogue.json +777 -0
- primarysource/bbf/catalogue.py +137 -0
- primarysource/bbf/cli.py +79 -0
- primarysource/bbf/datamodel.py +281 -0
- primarysource/bbf/importer.py +151 -0
- primarysource/bbf/library.py +380 -0
- primarysource/bbf/server.py +72 -0
- primarysource/budget.py +63 -0
- primarysource/c2sp/__init__.py +53 -0
- primarysource/c2sp/_fetch.py +5 -0
- primarysource/c2sp/catalogue.json +231 -0
- primarysource/c2sp/catalogue.py +95 -0
- primarysource/c2sp/cli.py +62 -0
- primarysource/c2sp/importer.py +90 -0
- primarysource/c2sp/library.py +309 -0
- primarysource/c2sp/markdown.py +31 -0
- primarysource/c2sp/server.py +68 -0
- primarysource/cavp/__init__.py +42 -0
- primarysource/cavp/cli.py +84 -0
- primarysource/cavp/data/vectors.toml +47 -0
- primarysource/cavp/importer.py +156 -0
- primarysource/cavp/model.py +279 -0
- primarysource/cavp/parse.py +269 -0
- primarysource/cavp/paths.py +17 -0
- primarysource/cavp/server.py +67 -0
- primarysource/cavp/tools.py +169 -0
- primarysource/cli.py +107 -0
- primarysource/cursor.py +82 -0
- primarysource/cve/__init__.py +5 -0
- primarysource/cve/__main__.py +6 -0
- primarysource/cve/cli.py +144 -0
- primarysource/cve/collection.py +20 -0
- primarysource/cve/config.py +58 -0
- primarysource/cve/errors.py +16 -0
- primarysource/cve/kernel.py +232 -0
- primarysource/cve/mcp.py +139 -0
- primarysource/cve/parse.py +318 -0
- primarysource/cve/py.typed +0 -0
- primarysource/cve/service.py +760 -0
- primarysource/cve/store.py +214 -0
- primarysource/cve/sync.py +438 -0
- primarysource/cve/views.py +304 -0
- primarysource/debpolicy/__init__.py +16 -0
- primarysource/debpolicy/catalogue.py +263 -0
- primarysource/debpolicy/cli.py +12 -0
- primarysource/debpolicy/server.py +11 -0
- primarysource/draft/__init__.py +11 -0
- primarysource/draft/catalogue.py +117 -0
- primarysource/draft/cli.py +149 -0
- primarysource/draft/collection.py +21 -0
- primarysource/draft/datatracker.py +158 -0
- primarysource/draft/importer.py +163 -0
- primarysource/draft/licence.py +89 -0
- primarysource/draft/names.py +64 -0
- primarysource/draft/net.py +133 -0
- primarysource/draft/segment.py +192 -0
- primarysource/draft/server.py +91 -0
- primarysource/draft/status.py +146 -0
- primarysource/draft/store.py +199 -0
- primarysource/draft/tracked.json +104 -0
- primarysource/draft/views.py +430 -0
- primarysource/draft/xmlmeta.py +74 -0
- primarysource/ecma/__init__.py +52 -0
- primarysource/ecma/_pdf.py +143 -0
- primarysource/ecma/catalogue.json +22 -0
- primarysource/ecma/catalogue.py +100 -0
- primarysource/ecma/cli.py +67 -0
- primarysource/ecma/identify.py +25 -0
- primarysource/ecma/importer.py +108 -0
- primarysource/ecma/library.py +240 -0
- primarysource/ecma/server.py +67 -0
- primarysource/errors.py +29 -0
- primarysource/fetch.py +360 -0
- primarysource/fsutil.py +150 -0
- primarysource/gitdocs/__init__.py +15 -0
- primarysource/gitdocs/catalogue.py +116 -0
- primarysource/gitdocs/cli.py +12 -0
- primarysource/gitdocs/server.py +8 -0
- primarysource/home.py +65 -0
- primarysource/iana/__init__.py +32 -0
- primarysource/iana/catalogue.json +244 -0
- primarysource/iana/catalogue.py +69 -0
- primarysource/iana/cli.py +69 -0
- primarysource/iana/importer.py +157 -0
- primarysource/iana/model.py +325 -0
- primarysource/iana/reader.py +315 -0
- primarysource/iana/server.py +67 -0
- primarysource/iana/store.py +83 -0
- primarysource/iana/xmlsafe.py +47 -0
- primarysource/identify.py +186 -0
- primarysource/ieee/__init__.py +58 -0
- primarysource/ieee/_html.py +49 -0
- primarysource/ieee/catalogue.json +74 -0
- primarysource/ieee/catalogue.py +123 -0
- primarysource/ieee/cli.py +79 -0
- primarysource/ieee/identify.py +24 -0
- primarysource/ieee/ieee802.py +47 -0
- primarysource/ieee/importer.py +305 -0
- primarysource/ieee/posix.py +233 -0
- primarysource/ieee/server.py +79 -0
- primarysource/index.py +446 -0
- primarysource/iso/__init__.py +51 -0
- primarysource/iso/cli.py +98 -0
- primarysource/iso/designation.py +95 -0
- primarysource/iso/identify.py +100 -0
- primarysource/iso/importer.py +224 -0
- primarysource/iso/library.py +249 -0
- primarysource/iso/server.py +69 -0
- primarysource/itu/__init__.py +41 -0
- primarysource/itu/__main__.py +8 -0
- primarysource/itu/asn1mod.py +478 -0
- primarysource/itu/catalog.py +472 -0
- primarysource/itu/cli.py +445 -0
- primarysource/itu/collection.py +20 -0
- primarysource/itu/corpus.py +118 -0
- primarysource/itu/errata.py +16 -0
- primarysource/itu/formal.py +565 -0
- primarysource/itu/identify.py +26 -0
- primarysource/itu/index.py +490 -0
- primarysource/itu/mcp.py +474 -0
- primarysource/itu/py.typed +0 -0
- primarysource/itu/sources.py +600 -0
- primarysource/itu/text.py +609 -0
- primarysource/kconfig/__init__.py +16 -0
- primarysource/kconfig/catalogue.py +98 -0
- primarysource/kconfig/cli.py +12 -0
- primarysource/kconfig/server.py +8 -0
- primarysource/model.py +63 -0
- primarysource/nist/__init__.py +5 -0
- primarysource/nist/collection.py +46 -0
- primarysource/nist/data/acquired-nist.json +1657 -0
- primarysource/nist/data/nist-catalog.json +12090 -0
- primarysource/nist/data/sources.toml +1996 -0
- primarysource/nist/identify.py +41 -0
- primarysource/nist/ingest/__init__.py +1 -0
- primarysource/nist/ingest/acquire_nist.py +128 -0
- primarysource/nist/ingest/controls.py +82 -0
- primarysource/nist/ingest/discover_nist.py +218 -0
- primarysource/nist/ingest/extract.py +48 -0
- primarysource/nist/ingest/fetch.py +211 -0
- primarysource/nist/ingest/index.py +14 -0
- primarysource/nist/ingest/oscal.py +480 -0
- primarysource/nist/ingest/prose_layouts.py +236 -0
- primarysource/nist/ingest/sources.py +94 -0
- primarysource/nist/ingest/status.py +255 -0
- primarysource/nist/models.py +7 -0
- primarysource/nist/paths.py +33 -0
- primarysource/nist/server/__init__.py +3 -0
- primarysource/nist/server/corpus.py +38 -0
- primarysource/nist/server/mcp.py +19 -0
- primarysource/nist/server/tools.py +35 -0
- primarysource/nist/spec.py +38 -0
- primarysource/pdfcorpus/__init__.py +16 -0
- primarysource/pdfcorpus/corpus.py +159 -0
- primarysource/pdfcorpus/fts.py +175 -0
- primarysource/pdfcorpus/index.py +70 -0
- primarysource/pdfcorpus/models.py +30 -0
- primarysource/pdfcorpus/server.py +96 -0
- primarysource/pdfcorpus/spec.py +37 -0
- primarysource/pdfcorpus/split.py +391 -0
- primarysource/pdfcorpus/tools.py +244 -0
- primarysource/pep/__init__.py +15 -0
- primarysource/pep/catalogue.py +191 -0
- primarysource/pep/cli.py +12 -0
- primarysource/pep/server.py +8 -0
- primarysource/pkgdocs/__init__.py +19 -0
- primarysource/pkgdocs/fetch.py +356 -0
- primarysource/pkgdocs/index.py +89 -0
- primarysource/pkgdocs/model.py +76 -0
- primarysource/pkgdocs/readers/__init__.py +1 -0
- primarysource/pkgdocs/readers/_common.py +134 -0
- primarysource/pkgdocs/readers/asciidoc.py +160 -0
- primarysource/pkgdocs/readers/latex.py +315 -0
- primarysource/pkgdocs/readers/markdown.py +173 -0
- primarysource/pkgdocs/readers/pod.py +132 -0
- primarysource/pkgdocs/readers/rst.py +239 -0
- primarysource/pkgdocs/readers/scdoc.py +108 -0
- primarysource/pkgdocs/runtime.py +552 -0
- primarysource/pkgdocs/serving.py +158 -0
- primarysource/pkgdocs/store.py +167 -0
- primarysource/pms/__init__.py +16 -0
- primarysource/pms/catalogue.py +88 -0
- primarysource/pms/cli.py +12 -0
- primarysource/pms/server.py +8 -0
- primarysource/py.typed +0 -0
- primarysource/readers/__init__.py +10 -0
- primarysource/readers/bookmarks.py +192 -0
- primarysource/readers/html.py +1312 -0
- primarysource/readers/markdown.py +189 -0
- primarysource/readers/pdf.py +187 -0
- primarysource/readers/xml.py +230 -0
- primarysource/refs.py +87 -0
- primarysource/registry.py +161 -0
- primarysource/render.py +49 -0
- primarysource/rfc/__init__.py +3 -0
- primarysource/rfc/__main__.py +6 -0
- primarysource/rfc/cite.py +204 -0
- primarysource/rfc/cli.py +903 -0
- primarysource/rfc/collection.py +19 -0
- primarysource/rfc/config.py +138 -0
- primarysource/rfc/errata.py +203 -0
- primarysource/rfc/fsutil.py +82 -0
- primarysource/rfc/graph.py +213 -0
- primarysource/rfc/index/__init__.py +1 -0
- primarysource/rfc/index/chunker.py +281 -0
- primarysource/rfc/index/embed.py +166 -0
- primarysource/rfc/index/hybrid.py +305 -0
- primarysource/rfc/index/lexical.py +223 -0
- primarysource/rfc/index/rfcxml.py +673 -0
- primarysource/rfc/index/search.py +508 -0
- primarysource/rfc/index/txtseg.py +585 -0
- primarysource/rfc/library.py +294 -0
- primarysource/rfc/mcp/__init__.py +1 -0
- primarysource/rfc/mcp/server.py +381 -0
- primarysource/rfc/packs/__init__.py +1 -0
- primarysource/rfc/packs/builder.py +228 -0
- primarysource/rfc/packs/collections/all-current-txt.json +10 -0
- primarysource/rfc/packs/collections/essentials.json +19 -0
- primarysource/rfc/packs/collections/fyi-current.json +10 -0
- primarysource/rfc/packs/collections/standards-current.json +10 -0
- primarysource/rfc/packs/collections/year-2025.json +10 -0
- primarysource/rfc/packs/collections/year-2026.json +10 -0
- primarysource/rfc/packs/collections.py +163 -0
- primarysource/rfc/packs/docsetexport.py +163 -0
- primarysource/rfc/packs/epub.py +159 -0
- primarysource/rfc/packs/export.py +306 -0
- primarysource/rfc/packs/htmlexport.py +82 -0
- primarysource/rfc/packs/serve.py +143 -0
- primarysource/rfc/packs/zimexport.py +171 -0
- primarysource/rfc/query.py +466 -0
- primarysource/rfc/subjects.py +457 -0
- primarysource/rfc/sync/__init__.py +1 -0
- primarysource/rfc/sync/downloader.py +132 -0
- primarysource/rfc/sync/manifest.py +162 -0
- primarysource/rfc/sync/scanner.py +49 -0
- primarysource/rfc/sync/state.py +464 -0
- primarysource/rfc/views.py +1067 -0
- primarysource/rpmdocs/__init__.py +15 -0
- primarysource/rpmdocs/catalogue.py +117 -0
- primarysource/rpmdocs/cli.py +12 -0
- primarysource/rpmdocs/server.py +8 -0
- primarysource/scaffold.py +223 -0
- primarysource/serve.py +75 -0
- primarysource/settings.py +152 -0
- primarysource/usb/__init__.py +5 -0
- primarysource/usb/collection.py +37 -0
- primarysource/usb/data/acquired.json +2025 -0
- primarysource/usb/data/sources-usb.toml +852 -0
- primarysource/usb/data/sources.toml +10 -0
- primarysource/usb/data/usb-catalog.json +1663 -0
- primarysource/usb/data/usb-classification.json +1986 -0
- primarysource/usb/data/usb-corpus-build.json +608 -0
- primarysource/usb/data/usb-pdf-choice.json +839 -0
- primarysource/usb/data/usb-split-measure.json +1803 -0
- primarysource/usb/ingest/__init__.py +1 -0
- primarysource/usb/ingest/acquire.py +130 -0
- primarysource/usb/ingest/discover_usb.py +88 -0
- primarysource/usb/ingest/extract.py +48 -0
- primarysource/usb/ingest/fetch.py +197 -0
- primarysource/usb/ingest/index.py +14 -0
- primarysource/usb/ingest/sources.py +85 -0
- primarysource/usb/models.py +7 -0
- primarysource/usb/paths.py +33 -0
- primarysource/usb/server/__init__.py +3 -0
- primarysource/usb/server/corpus.py +38 -0
- primarysource/usb/server/mcp.py +19 -0
- primarysource/usb/server/tools.py +35 -0
- primarysource/usb/spec.py +20 -0
- primarysource/usbids/__init__.py +56 -0
- primarysource/usbids/cli.py +65 -0
- primarysource/usbids/importer.py +272 -0
- primarysource/usbids/parse.py +283 -0
- primarysource/usbids/server.py +55 -0
- primarysource/usbids/store.py +80 -0
- primarysource/usbids/tools.py +174 -0
- primarysource/zim/__init__.py +5 -0
- primarysource/zim/__main__.py +8 -0
- primarysource/zim/_libzim.py +37 -0
- primarysource/zim/catalog.py +594 -0
- primarysource/zim/cli.py +555 -0
- primarysource/zim/collection.py +41 -0
- primarysource/zim/errors.py +100 -0
- primarysource/zim/groups.py +30 -0
- primarysource/zim/library.py +668 -0
- primarysource/zim/manage.py +636 -0
- primarysource/zim/markdown.py +1247 -0
- primarysource/zim/mcp.py +201 -0
- primarysource/zim/purl.py +130 -0
- primarysource/zim/py.typed +0 -0
- primarysource/zim/refs.py +90 -0
- primarysource/zim/service.py +1182 -0
- primarysource/zim/snapshot.py +197 -0
- primarysource/zim/sync.py +269 -0
- primarysource/zim/views.py +230 -0
- primarysource-0.1.0.dist-info/METADATA +118 -0
- primarysource-0.1.0.dist-info/RECORD +303 -0
- primarysource-0.1.0.dist-info/WHEEL +4 -0
- primarysource-0.1.0.dist-info/entry_points.txt +2 -0
- primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
- primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Text views of `service`'s answers: what a tool or a command prints (PLAN.md §5.9).
|
|
2
|
+
|
|
3
|
+
`service` returns plain dicts, which is what `--json` prints. These functions turn the same dicts into `render()`
|
|
4
|
+
text: the answer as lines, then the fixed footer. What a dict carries as a field either stays a line of the body
|
|
5
|
+
(`kev:`, `epss:`, the verdict) or becomes a footer line:
|
|
6
|
+
|
|
7
|
+
- `cite:` the sources that supplied the answer, as `service` names them;
|
|
8
|
+
- `unit:` is `block` when the lines are the numbered items of a list section (a reference, an affected entry, a
|
|
9
|
+
kernel range): each carries its own ref, `CVE-Y-N§references¶9`, and `¶n` on a read means "from item n on";
|
|
10
|
+
- `next:` a ref string for a read (`CVE-Y-N§references¶9`, what `cve_read` takes back as `ref`), `start=<n>` for a
|
|
11
|
+
list (`render.next_start`);
|
|
12
|
+
- `relaxed:` the search matched only some of its terms;
|
|
13
|
+
- `partial:` every source that is missing, failed or stale, because an absence means less when one is (a CVE
|
|
14
|
+
"not in KEV" is a different fact from a KEV catalog that was never synced);
|
|
15
|
+
- `warning:` a hedge the reader must not drop: a truncated description, a version string with a distribution
|
|
16
|
+
suffix.
|
|
17
|
+
|
|
18
|
+
Record text (titles, descriptions, references, KEV notes) is data from other people. It is printed as it came and
|
|
19
|
+
never interpreted; the server instructions say so.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from primarysource.render import next_start, render
|
|
29
|
+
|
|
30
|
+
PARTIAL_JOIN = " | "
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _footer(payload: Mapping[str, Any], **fixed: str) -> dict[str, str]:
|
|
34
|
+
"""The footer lines `payload` implies, plus `fixed` ones. A multi-line value is cut to one line."""
|
|
35
|
+
footer = {key: " ".join(str(value).split()) for key, value in fixed.items() if value}
|
|
36
|
+
gaps = payload.get("partial")
|
|
37
|
+
if gaps:
|
|
38
|
+
footer["partial"] = PARTIAL_JOIN.join(" ".join(str(gap).split()) for gap in gaps)
|
|
39
|
+
return footer
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _next(payload: Mapping[str, Any]) -> str:
|
|
43
|
+
"""A read's `next` is already a ref; a list's is `{"start": n}`."""
|
|
44
|
+
value = payload.get("next")
|
|
45
|
+
if isinstance(value, Mapping):
|
|
46
|
+
return next_start(int(value["start"]))
|
|
47
|
+
return str(value) if value else ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _score(metric: Mapping[str, Any] | None) -> str:
|
|
51
|
+
if not metric:
|
|
52
|
+
return "none"
|
|
53
|
+
severity = f" {metric['severity']}" if metric.get("severity") else ""
|
|
54
|
+
return f"{metric['score']}{severity} (CVSS {metric['version']}, {metric['label']})"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _json_line(value: Any) -> str:
|
|
58
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _kev_line(kev: Mapping[str, Any]) -> str:
|
|
62
|
+
if kev["status"] == "listed":
|
|
63
|
+
return (
|
|
64
|
+
f"listed, added {kev['date_added']}, due {kev['due_date']}, ransomware use {kev['ransomware']}; "
|
|
65
|
+
f"action: {kev['action']}"
|
|
66
|
+
)
|
|
67
|
+
if kev["status"] == "not_listed":
|
|
68
|
+
return f"not listed ({kev['source']})"
|
|
69
|
+
return f"unknown: {kev.get('why', 'not consulted')}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _epss_line(epss: Mapping[str, Any]) -> str:
|
|
73
|
+
return f"{epss['score']} (percentile {epss['percentile']}) {epss['source']}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _fixed_in(fixed: Mapping[str, str]) -> str:
|
|
77
|
+
return ", ".join(f"{branch} -> {fix}" for branch, fix in fixed.items()) or "none recorded"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# --- info ----------------------------------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def info(payload: Mapping[str, Any]) -> str:
|
|
84
|
+
if payload.get("status") == "not_synced":
|
|
85
|
+
body = f"home: {payload['home']}\nindex: not synced yet"
|
|
86
|
+
return render(body, {"warning": f"{payload['error']}; {payload['hint']}; run: {payload['action']}"})
|
|
87
|
+
if "counts" not in payload:
|
|
88
|
+
lines = [f"{payload['ref']} {payload['state']} {payload['title']}"]
|
|
89
|
+
lines.append(f"cvss: {_score(payload['cvss'])}")
|
|
90
|
+
lines.append(f"kev: {payload['kev']}")
|
|
91
|
+
if payload.get("epss"):
|
|
92
|
+
lines.append(f"epss: {_epss_line(payload['epss'])}")
|
|
93
|
+
lines.append(f"kernel CNA data: {'yes' if payload['kernel_cna'] else 'no'}")
|
|
94
|
+
return render("\n".join(lines), _footer(payload, next=str(payload["next"])))
|
|
95
|
+
lines = [f"home: {payload['home']}", f"index: {payload['index']} ({payload['index_mb']} MB)"]
|
|
96
|
+
lines += [f"{name}: {count}" for name, count in payload["counts"].items()]
|
|
97
|
+
lines.append("sources:")
|
|
98
|
+
for name, row in payload["sources"].items():
|
|
99
|
+
lines.append(
|
|
100
|
+
f" {name} {row['state']} version {row['version'] or '-'} upstream {row['upstream_date'] or '-'} "
|
|
101
|
+
f"synced {row['synced_at'] or '-'} records {row['records'] if row['records'] is not None else '-'}"
|
|
102
|
+
+ (f" detail: {row['detail']}" if row["detail"] else "")
|
|
103
|
+
)
|
|
104
|
+
return render("\n".join(lines), _footer(payload))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- search --------------------------------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def search(payload: Mapping[str, Any]) -> str:
|
|
111
|
+
lines: list[str] = []
|
|
112
|
+
for hit in payload["hits"]:
|
|
113
|
+
flags = "".join(
|
|
114
|
+
[
|
|
115
|
+
f" cvss={hit['cvss']}" if "cvss" in hit else "",
|
|
116
|
+
" KEV" if hit.get("kev") else "",
|
|
117
|
+
f" epss={hit['epss']:.3f}" if "epss" in hit else "",
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
lines.append(f"{hit['ref']} [{hit['source']}]{flags} {hit['title']}")
|
|
121
|
+
if hit.get("snippet"):
|
|
122
|
+
lines.append(f" {' '.join(str(hit['snippet']).split())}")
|
|
123
|
+
body = "\n".join(lines) or f"no CVE record matches {payload['query']!r}"
|
|
124
|
+
relaxed = "no record holds every term; these hold some of them" if payload.get("relaxed") and lines else ""
|
|
125
|
+
return render(body, _footer(payload, next=_next(payload), relaxed=relaxed))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# --- read ----------------------------------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _items(payload: Mapping[str, Any], label: str, lines: list[str]) -> list[str]:
|
|
132
|
+
"""The numbered items of a list section, each with its own ref so it can be cited."""
|
|
133
|
+
section = str(payload["ref"])
|
|
134
|
+
head = f"{section}: items {payload['start']}-{payload['start'] + len(payload['items']) - 1} of {payload['total']}"
|
|
135
|
+
if not payload["items"]:
|
|
136
|
+
head = f"{section}: no {label}"
|
|
137
|
+
numbered = [f"{section}¶{payload['start'] + offset} {item}" for offset, item in enumerate(payload["items"])]
|
|
138
|
+
return [head, *lines, *numbered]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _affected_line(entry: Any) -> str:
|
|
142
|
+
if not isinstance(entry, Mapping):
|
|
143
|
+
return str(entry)
|
|
144
|
+
head = f"{entry.get('vendor', '')} {entry.get('product', '')}".strip()
|
|
145
|
+
ranges: list[str] = []
|
|
146
|
+
for version in entry.get("versions") or []:
|
|
147
|
+
if not isinstance(version, Mapping):
|
|
148
|
+
continue
|
|
149
|
+
bits = [str(version.get("status", "")), str(version.get("version", ""))]
|
|
150
|
+
bits += [f"{key} {version[key]}" for key in ("lessThan", "lessThanOrEqual") if version.get(key)]
|
|
151
|
+
ranges.append(" ".join(bit for bit in bits if bit))
|
|
152
|
+
if ranges:
|
|
153
|
+
return f"{head}: {'; '.join(ranges)}"
|
|
154
|
+
return head or _json_line(entry)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def read(payload: Mapping[str, Any]) -> str:
|
|
158
|
+
ref = str(payload["ref"])
|
|
159
|
+
if payload.get("state") == "UNKNOWN_TO_CVE_SOURCES":
|
|
160
|
+
lines = [f"{ref} {payload['note']}", f"kev: {_kev_line(payload['kev'])}"]
|
|
161
|
+
if payload.get("epss"):
|
|
162
|
+
lines.append(f"epss: {_epss_line(payload['epss'])}")
|
|
163
|
+
return render("\n".join(lines), _footer(payload))
|
|
164
|
+
cite = str(payload.get("cite", ""))
|
|
165
|
+
if "§" in ref:
|
|
166
|
+
section = ref.split("§", 1)[1]
|
|
167
|
+
if section == "references":
|
|
168
|
+
body = "\n".join(_items(payload, "references", []))
|
|
169
|
+
return render(body, _footer(payload, cite=cite, unit="block", next=_next(payload)))
|
|
170
|
+
if section == "affected":
|
|
171
|
+
lines = _items(
|
|
172
|
+
{**payload, "items": [_affected_line(i) for i in payload["items"]]}, "affected entries", []
|
|
173
|
+
)
|
|
174
|
+
return render("\n".join(lines), _footer(payload, cite=cite, unit="block", next=_next(payload)))
|
|
175
|
+
if section == "cvss":
|
|
176
|
+
lines = [f"{ref} preferred: {_score(payload['preferred'])}"]
|
|
177
|
+
for metric in payload["items"]:
|
|
178
|
+
lines.append(f"{metric['label']} {metric['version']} {_score(metric)} {metric['vector']}")
|
|
179
|
+
if not payload["items"]:
|
|
180
|
+
lines.append("no CVSS score in any synced source")
|
|
181
|
+
return render("\n".join(lines), _footer(payload, cite=cite))
|
|
182
|
+
if section == "kernel":
|
|
183
|
+
if "items" not in payload:
|
|
184
|
+
return render(f"{ref} {payload['note']}", _footer(payload, cite=cite))
|
|
185
|
+
lines = [
|
|
186
|
+
f"fixed in: {_fixed_in(payload['fixed_in'])}",
|
|
187
|
+
f"unfixed on start: {', '.join(payload['unfixed_on_start']) or 'none'}",
|
|
188
|
+
f"source: {payload['source']}",
|
|
189
|
+
]
|
|
190
|
+
body = "\n".join(_items(payload, "kernel ranges", lines))
|
|
191
|
+
return render(body, _footer(payload, cite=cite, unit="block", next=_next(payload)))
|
|
192
|
+
lines = [f"{ref}"]
|
|
193
|
+
for name, row in payload["sources"].items():
|
|
194
|
+
lines.append(
|
|
195
|
+
f"{name} {row['state']} version {row['version'] or '-'} upstream {row['upstream_date'] or '-'} "
|
|
196
|
+
f"synced {row['synced_at'] or '-'}"
|
|
197
|
+
)
|
|
198
|
+
return render("\n".join(lines), _footer(payload, cite=cite))
|
|
199
|
+
return _summary(payload)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _summary(payload: Mapping[str, Any]) -> str:
|
|
203
|
+
lines = [f"{payload['ref']} {payload['state']}"]
|
|
204
|
+
if payload["title"]:
|
|
205
|
+
lines.append(f"title: {payload['title']}")
|
|
206
|
+
lines.append(
|
|
207
|
+
f"assigner: {payload['assigner'] or '-'} published: {payload['published']} updated: {payload['updated']}"
|
|
208
|
+
)
|
|
209
|
+
lines.append(f"cwe: {', '.join(payload['cwe']) or '-'}")
|
|
210
|
+
lines.append(
|
|
211
|
+
f"cvss: {_score(payload['cvss']['preferred'])}; {payload['cvss']['count']} score(s) in {payload['ref']}§cvss"
|
|
212
|
+
)
|
|
213
|
+
lines.append(f"kev: {_kev_line(payload['kev'])}")
|
|
214
|
+
if payload.get("epss"):
|
|
215
|
+
lines.append(f"epss: {_epss_line(payload['epss'])}")
|
|
216
|
+
if payload.get("ssvc"):
|
|
217
|
+
lines.append("ssvc: " + ", ".join(f"{key}={value}" for key, value in payload["ssvc"].items()))
|
|
218
|
+
kernel = payload.get("kernel")
|
|
219
|
+
if kernel:
|
|
220
|
+
lines.append(f"kernel: {kernel['source']}; fixed in {_fixed_in(kernel['fixed_in'])}")
|
|
221
|
+
if kernel["unfixed_on_start"]:
|
|
222
|
+
lines.append(f"kernel unfixed on start: {', '.join(kernel['unfixed_on_start'])}")
|
|
223
|
+
if kernel["files"]:
|
|
224
|
+
more = kernel["files_total"] - len(kernel["files"])
|
|
225
|
+
lines.append(f"kernel files: {' '.join(kernel['files'])}" + (f" (+{more} more)" if more > 0 else ""))
|
|
226
|
+
lines.append(f"kernel question: cve_kernel('{payload['ref']}', '<version>')")
|
|
227
|
+
lines.append(f"description ({payload['description_source']}):")
|
|
228
|
+
lines.append(str(payload["description"]))
|
|
229
|
+
lines.append(f"references ({len(payload['references'])} of {payload['references_total']}):")
|
|
230
|
+
lines += [f" {url}" for url in payload["references"]]
|
|
231
|
+
lines.append("sections: " + ", ".join(payload["sections"]))
|
|
232
|
+
warning = "description and references cut to fit max_tokens; raise max_tokens" if payload.get("truncated") else ""
|
|
233
|
+
return render("\n".join(lines), _footer(payload, cite=str(payload["cite"]), next=_next(payload), warning=warning))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# --- kev -----------------------------------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def kev(payload: Mapping[str, Any]) -> str:
|
|
240
|
+
if "status" in payload:
|
|
241
|
+
if payload["status"] == "not_listed":
|
|
242
|
+
body = f"{payload['ref']} is not in the CISA KEV catalog"
|
|
243
|
+
return render(body, _footer(payload, cite=str(payload["source"])))
|
|
244
|
+
lines = [
|
|
245
|
+
f"{payload['ref']} listed in CISA KEV",
|
|
246
|
+
f"vendor: {payload['vendor']} product: {payload['product']}",
|
|
247
|
+
f"name: {payload['name']}",
|
|
248
|
+
f"added: {payload['date_added']} due: {payload['due_date']} ransomware use: {payload['ransomware']}",
|
|
249
|
+
f"action: {payload['action']}",
|
|
250
|
+
f"description: {payload['description']}",
|
|
251
|
+
]
|
|
252
|
+
if payload.get("notes"):
|
|
253
|
+
lines.append(f"notes: {payload['notes']}")
|
|
254
|
+
if payload.get("cwes"):
|
|
255
|
+
lines.append(f"cwe: {', '.join(payload['cwes'])}")
|
|
256
|
+
if payload.get("forensic_triage"):
|
|
257
|
+
lines.append(f"forensic triage: {payload['forensic_triage']}")
|
|
258
|
+
return render("\n".join(lines), _footer(payload, cite=str(payload["source"])))
|
|
259
|
+
lines = [f"{payload['total']} KEV entries, newest first"]
|
|
260
|
+
lines += [
|
|
261
|
+
f"{row['cve']} added {row['date_added']} due {row['due_date']} {row['vendor']}/{row['product']} "
|
|
262
|
+
f"{row['name']}" + (f" ransomware: {row['ransomware']}" if row["ransomware"] else "")
|
|
263
|
+
for row in payload["items"]
|
|
264
|
+
]
|
|
265
|
+
return render("\n".join(lines), _footer(payload, cite=str(payload["source"]), next=_next(payload)))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# --- kernel --------------------------------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _caveats(payload: Mapping[str, Any]) -> str:
|
|
272
|
+
return PARTIAL_JOIN.join(payload.get("caveats", []))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def kernel_check(payload: Mapping[str, Any]) -> str:
|
|
276
|
+
confidence = f" (confidence: {payload['confidence']})" if payload.get("confidence") else ""
|
|
277
|
+
lines = [f"{payload['ref']} on kernel {payload['version']}: {payload['verdict']}{confidence}"]
|
|
278
|
+
if payload.get("reason"):
|
|
279
|
+
lines.append(f"reason: {payload['reason']}")
|
|
280
|
+
if payload.get("detail"):
|
|
281
|
+
lines.append(f"detail: {payload['detail']}")
|
|
282
|
+
lines += [f"evidence: {item}" for item in payload.get("evidence", [])]
|
|
283
|
+
if payload.get("fixed_in"):
|
|
284
|
+
lines.append(f"fixed in: {_fixed_in(payload['fixed_in'])}")
|
|
285
|
+
if payload.get("note"):
|
|
286
|
+
lines.append(f"note: {payload['note']}")
|
|
287
|
+
return render("\n".join(lines), _footer(payload, cite=str(payload.get("source", "")), warning=_caveats(payload)))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def kernel_list(payload: Mapping[str, Any]) -> str:
|
|
291
|
+
lines = [f"{payload['version']}: {payload['affected_total']} of {payload['assessed']} kernel CNA CVEs affect it"]
|
|
292
|
+
for item in payload["items"]:
|
|
293
|
+
marks = "".join([" KEV" if item.get("kev") else "", f" epss={item['epss']:.3f}" if "epss" in item else ""])
|
|
294
|
+
files = f" files: {' '.join(item['files'])}" if item.get("files") else ""
|
|
295
|
+
lines.append(f"{item['ref']}{marks} {item['title']} ({item['why']}){files}")
|
|
296
|
+
lines.append(f"note: {payload['note']}")
|
|
297
|
+
return render(
|
|
298
|
+
"\n".join(lines),
|
|
299
|
+
_footer(payload, cite=str(payload["source"]), next=_next(payload), warning=_caveats(payload)),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def path(home: str, index: str) -> str:
|
|
304
|
+
return render(f"home: {home}\nindex: {index}\nsetting: CVELIB_HOME or PRIMARYSOURCE_CVE_HOME", {})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""`debpolicy`: Debian Policy and the dpkg manual pages, pinned releases, read as reStructuredText and POD.
|
|
2
|
+
Served under `debpolicy_`.
|
|
3
|
+
|
|
4
|
+
Stdlib only. `mcp` is imported by `server.py` when a server is built; `primarysource debpolicy ...` runs `cli.py`.
|
|
5
|
+
`docs/debpolicy.md` is the description; `catalogue.py` holds the pin, the licence and the build.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from primarysource.debpolicy.catalogue import SPEC
|
|
11
|
+
from primarysource.pkgdocs.runtime import make_collection
|
|
12
|
+
|
|
13
|
+
COLLECTION = make_collection(SPEC, "primarysource.debpolicy")
|
|
14
|
+
"""Refs: `debpolicy:5.6.1`; see `pkgdocs.runtime`."""
|
|
15
|
+
|
|
16
|
+
__all__ = ["COLLECTION", "SPEC"]
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""The `debpolicy` collection: Debian Policy (reStructuredText) and the dpkg manual pages (POD).
|
|
2
|
+
|
|
3
|
+
Two pinned trees, so two editions that move independently:
|
|
4
|
+
|
|
5
|
+
- `policy`: dbnpolicy/policy on salsa.debian.org at the commit of the tag `debian/4.7.4.1`. The documents are
|
|
6
|
+
`policy/*.rst` (chapters `ch-*`, appendices `ap-*`, `upgrading-checklist`, `index`) and `autopkgtest.md`;
|
|
7
|
+
- `dpkg`: dpkg-team/dpkg at the tag `1.23.12`. The documents are the 63 `man/*.pod` pages, with their manual
|
|
8
|
+
section taken from `man/Makefile.am` (`man_MANS`). In 1.23.12 the pages are POD, not roff; roff appears only
|
|
9
|
+
after `pod2man`.
|
|
10
|
+
|
|
11
|
+
Licence (read from the repositories on 2026-11-13, see `docs/debpolicy.md`): the policy tree's `debian/copyright`
|
|
12
|
+
(DEP-5) gives `Files: *` as GPL-2+ and names the exceptions (`copyright-format*.xml`, `debconf*`, `fhs-3.0*` and
|
|
13
|
+
some diagrams), none of which is imported; the build reads that file and puts the matching `License:` of each
|
|
14
|
+
imported document in its meta. Each dpkg page carries its own header, "either version 2 of the License, or (at
|
|
15
|
+
your option) any later version", which the POD build reads the same way. Placement is **layer**.
|
|
16
|
+
|
|
17
|
+
Numbering: a chapter's number is its place in the first `toctree` of `index.rst`, so `ch-controlfields` is
|
|
18
|
+
chapter 5 and its subsections are `5.1`, `5.6.1`. `debpolicy:5.6.1` works without a document id for that
|
|
19
|
+
reason. Appendices are numbered locally inside their own document (the build does not claim Sphinx's appendix
|
|
20
|
+
numbering, which was not checked).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import fnmatch
|
|
26
|
+
import re
|
|
27
|
+
from collections import Counter
|
|
28
|
+
from collections.abc import Mapping
|
|
29
|
+
|
|
30
|
+
from primarysource.errors import LibError
|
|
31
|
+
from primarysource.pkgdocs.fetch import Source
|
|
32
|
+
from primarysource.pkgdocs.model import Doc
|
|
33
|
+
from primarysource.pkgdocs.readers._common import decode
|
|
34
|
+
from primarysource.pkgdocs.readers.markdown import read_markdown
|
|
35
|
+
from primarysource.pkgdocs.readers.pod import read_pod
|
|
36
|
+
from primarysource.pkgdocs.readers.rst import read_rst
|
|
37
|
+
from primarysource.pkgdocs.runtime import Build, Licence, Pin, Spec, TreeSpec, matching
|
|
38
|
+
|
|
39
|
+
POLICY_COMMIT = "aceb1daf7305adc0b336ee2ebbdf0bc0340d3b68"
|
|
40
|
+
DPKG_COMMIT = "f78cedcdcbbf25b27f3104b99ac3d2832e19d0cc"
|
|
41
|
+
|
|
42
|
+
POLICY_PIN = Pin(
|
|
43
|
+
commit=POLICY_COMMIT,
|
|
44
|
+
label="4.7.4.1",
|
|
45
|
+
date="2026-03-31",
|
|
46
|
+
files_sha256="bcf7ce3c75512efe7274fedf88a8c615483e50872c9fd644c678a816f8b3319b",
|
|
47
|
+
sources=(
|
|
48
|
+
Source(
|
|
49
|
+
f"https://salsa.debian.org/dbnpolicy/policy/-/archive/{POLICY_COMMIT}/policy-{POLICY_COMMIT}.tar.gz",
|
|
50
|
+
strip=1,
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
DPKG_PIN = Pin(
|
|
55
|
+
commit=DPKG_COMMIT,
|
|
56
|
+
label="1.23.12",
|
|
57
|
+
date="2026-11-13",
|
|
58
|
+
files_sha256="22d4c9ed26c64e136510ae6d7f002b7afb309613223bc88ef025413b8b47e610",
|
|
59
|
+
sources=(
|
|
60
|
+
Source(f"https://salsa.debian.org/dpkg-team/dpkg/-/archive/{DPKG_COMMIT}/dpkg-{DPKG_COMMIT}.tar.gz", strip=1),
|
|
61
|
+
Source(
|
|
62
|
+
f"https://github.com/guillemj/dpkg/archive/{DPKG_COMMIT}.tar.gz",
|
|
63
|
+
strip=1,
|
|
64
|
+
hosts=("codeload.github.com",),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
LICENCE = Licence(
|
|
70
|
+
spdx="GPL-2.0-or-later",
|
|
71
|
+
redistribute="layer",
|
|
72
|
+
attribution="Debian Policy: Ian Jackson, Christian Schwarz and many other Debian contributors; "
|
|
73
|
+
"dpkg: Guillem Jover and others",
|
|
74
|
+
evidence="debian/copyright (`Files: *`, License: GPL-2+) in the policy tree; the header of each dpkg .pod",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
_GPL2_PLUS = "GPL-2.0-or-later"
|
|
78
|
+
_MAN_MANS = re.compile(r"man_MANS\s*\+?=\s*((?:[^\n]*\\\n)*[^\n]*)")
|
|
79
|
+
_PAGE = re.compile(r"([\w.+-]+)\.(\d)")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _copyright_stanzas(text: str) -> list[tuple[list[str], str]]:
|
|
83
|
+
"""(file patterns, licence short name) for each DEP-5 `Files:` stanza of a `debian/copyright`."""
|
|
84
|
+
stanzas: list[tuple[list[str], str]] = []
|
|
85
|
+
for block in re.split(r"\n\s*\n", text):
|
|
86
|
+
files: list[str] = []
|
|
87
|
+
licence = ""
|
|
88
|
+
in_files = False
|
|
89
|
+
for line in block.split("\n"):
|
|
90
|
+
if line.startswith("Files:"):
|
|
91
|
+
in_files = True
|
|
92
|
+
files.extend(line[len("Files:") :].split())
|
|
93
|
+
elif in_files and line[:1] in (" ", "\t"):
|
|
94
|
+
files.extend(line.split())
|
|
95
|
+
else:
|
|
96
|
+
in_files = False
|
|
97
|
+
if line.startswith("License:"):
|
|
98
|
+
licence = line[len("License:") :].strip()
|
|
99
|
+
if files and licence:
|
|
100
|
+
stanzas.append((files, licence))
|
|
101
|
+
return stanzas
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def licence_of(path: str, stanzas: list[tuple[list[str], str]]) -> str:
|
|
105
|
+
"""The licence DEP-5 gives `path`: the last `Files:` stanza that matches it (`*` also matches `/`)."""
|
|
106
|
+
found = ""
|
|
107
|
+
for patterns, licence in stanzas:
|
|
108
|
+
if any(fnmatch.fnmatchcase(path, pattern) for pattern in patterns):
|
|
109
|
+
found = licence
|
|
110
|
+
return found
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _chapter_order(index: bytes | None) -> list[str]:
|
|
114
|
+
"""Document stems in the first `toctree` of `index.rst`, in order."""
|
|
115
|
+
if index is None:
|
|
116
|
+
return []
|
|
117
|
+
stems: list[str] = []
|
|
118
|
+
in_tree = False
|
|
119
|
+
for line in decode(index, "policy/index.rst").split("\n"):
|
|
120
|
+
if line.startswith(".. toctree::"):
|
|
121
|
+
if stems:
|
|
122
|
+
break
|
|
123
|
+
in_tree = True
|
|
124
|
+
elif in_tree and line.strip() and line.startswith((" ", "\t")) and not line.strip().startswith(":"):
|
|
125
|
+
stems.append(line.strip())
|
|
126
|
+
elif in_tree and line.strip() and not line.startswith((" ", "\t")):
|
|
127
|
+
break
|
|
128
|
+
return stems
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_policy(files: Mapping[str, bytes], pin: Pin) -> Build:
|
|
132
|
+
stanzas = (
|
|
133
|
+
_copyright_stanzas(decode(files["debian/copyright"], "debian/copyright"))
|
|
134
|
+
if "debian/copyright" in files
|
|
135
|
+
else []
|
|
136
|
+
)
|
|
137
|
+
order = _chapter_order(files.get("policy/index.rst"))
|
|
138
|
+
docs: list[Doc] = []
|
|
139
|
+
aliases: dict[str, str] = {}
|
|
140
|
+
skipped: Counter[str] = Counter()
|
|
141
|
+
if not stanzas:
|
|
142
|
+
skipped["debian/copyright not found or has no Files stanza: licences unchecked"] += 1
|
|
143
|
+
for path in sorted(files):
|
|
144
|
+
if path == "debian/copyright":
|
|
145
|
+
continue
|
|
146
|
+
stem = path.rsplit("/", 1)[-1].rsplit(".", 1)[0]
|
|
147
|
+
try:
|
|
148
|
+
if path.startswith("policy/") and path.endswith(".rst"):
|
|
149
|
+
number = str(order.index(stem) + 1) if stem.startswith("ch-") and stem in order else ""
|
|
150
|
+
doc = read_rst(files[path], stem, base=number)
|
|
151
|
+
if number:
|
|
152
|
+
aliases[number] = stem
|
|
153
|
+
doc = Doc(
|
|
154
|
+
doc.id,
|
|
155
|
+
f"Debian Policy {pin.label} chapter {number}: {doc.title}",
|
|
156
|
+
doc.unit,
|
|
157
|
+
doc.sections,
|
|
158
|
+
doc.meta,
|
|
159
|
+
doc.warnings,
|
|
160
|
+
)
|
|
161
|
+
elif stem != "index":
|
|
162
|
+
doc = Doc(
|
|
163
|
+
doc.id,
|
|
164
|
+
f"Debian Policy {pin.label}: {doc.title}",
|
|
165
|
+
doc.unit,
|
|
166
|
+
doc.sections,
|
|
167
|
+
doc.meta,
|
|
168
|
+
doc.warnings,
|
|
169
|
+
)
|
|
170
|
+
elif path.endswith(".md"):
|
|
171
|
+
doc = read_markdown(files[path], stem)
|
|
172
|
+
else:
|
|
173
|
+
skipped["not a policy document"] += 1
|
|
174
|
+
continue
|
|
175
|
+
except LibError:
|
|
176
|
+
skipped["not UTF-8 text"] += 1
|
|
177
|
+
continue
|
|
178
|
+
found = licence_of(path, stanzas)
|
|
179
|
+
docs.append(doc.with_meta(path=path, licence=_GPL2_PLUS if found == "GPL-2+" else (found or "unchecked")))
|
|
180
|
+
return Build(tuple(docs), aliases, skipped)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def man_sections(makefile: bytes | None) -> dict[str, str]:
|
|
184
|
+
"""`{page: section}` from the `man_MANS` lists of `man/Makefile.am` (`deb-control.5` -> `deb-control: 5`)."""
|
|
185
|
+
if makefile is None:
|
|
186
|
+
return {}
|
|
187
|
+
sections: dict[str, str] = {}
|
|
188
|
+
for match in _MAN_MANS.finditer(decode(makefile, "man/Makefile.am")):
|
|
189
|
+
for token in match.group(1).replace("\\\n", " ").split():
|
|
190
|
+
found = _PAGE.fullmatch(token)
|
|
191
|
+
if found:
|
|
192
|
+
sections[found.group(1)] = found.group(2)
|
|
193
|
+
return sections
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_dpkg(files: Mapping[str, bytes], pin: Pin) -> Build:
|
|
197
|
+
sections = man_sections(files.get("man/Makefile.am"))
|
|
198
|
+
docs: list[Doc] = []
|
|
199
|
+
aliases: dict[str, str] = {}
|
|
200
|
+
skipped: Counter[str] = Counter()
|
|
201
|
+
for path in sorted(files):
|
|
202
|
+
if not (path.startswith("man/") and path.endswith(".pod")):
|
|
203
|
+
continue
|
|
204
|
+
page = path[len("man/") : -len(".pod")]
|
|
205
|
+
try:
|
|
206
|
+
doc = read_pod(files[path], page)
|
|
207
|
+
except LibError:
|
|
208
|
+
skipped["not UTF-8 text"] += 1
|
|
209
|
+
continue
|
|
210
|
+
number = sections.get(page, "")
|
|
211
|
+
if not number:
|
|
212
|
+
skipped["page not listed in man_MANS: no manual section"] += 1
|
|
213
|
+
_, _, description = doc.title.partition(" - ")
|
|
214
|
+
title = f"{page}({number})" + (f" - {description}" if description else "") if number else doc.title
|
|
215
|
+
text = decode(files[path], path)
|
|
216
|
+
licence = (
|
|
217
|
+
_GPL2_PLUS
|
|
218
|
+
if "either version 2 of the License, or" in text and "any later version" in text
|
|
219
|
+
else "see file"
|
|
220
|
+
)
|
|
221
|
+
doc = Doc(doc.id, title, doc.unit, doc.sections, doc.meta, doc.warnings)
|
|
222
|
+
docs.append(doc.with_meta(path=path, licence=licence, man_section=number))
|
|
223
|
+
if number:
|
|
224
|
+
aliases[f"{page}.{number}"] = page
|
|
225
|
+
return Build(tuple(docs), aliases, skipped)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
SPEC = Spec(
|
|
229
|
+
name="debpolicy",
|
|
230
|
+
title="Debian Policy and the dpkg manual pages",
|
|
231
|
+
licence=LICENCE,
|
|
232
|
+
trees=(
|
|
233
|
+
TreeSpec(
|
|
234
|
+
name="policy",
|
|
235
|
+
title="Debian Policy Manual (policy/*.rst, autopkgtest.md)",
|
|
236
|
+
pins=(POLICY_PIN,),
|
|
237
|
+
include=matching(r"policy/[a-z0-9-]+\.rst|autopkgtest\.md|debian/copyright"),
|
|
238
|
+
build=build_policy,
|
|
239
|
+
),
|
|
240
|
+
TreeSpec(
|
|
241
|
+
name="dpkg",
|
|
242
|
+
title="dpkg manual pages (man/*.pod)",
|
|
243
|
+
pins=(DPKG_PIN,),
|
|
244
|
+
include=matching(r"man/[A-Za-z0-9.+-]+\.pod|man/Makefile\.am"),
|
|
245
|
+
build=build_dpkg,
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
instructions=(
|
|
249
|
+
"Debian Policy (chapters 1-12 and the appendices) and the dpkg manual pages (deb(5), deb-control(5), "
|
|
250
|
+
"dpkg-deb(1), ...) from the user's imported copies at pinned releases. Loop: debpolicy_search('<words>') -> "
|
|
251
|
+
"debpolicy_outline('<doc>') -> debpolicy_read('<ref>'). Policy section numbers work as refs: '5.6.1' is "
|
|
252
|
+
"the Source field of chapter 5 (ch-controlfields); chapters are the documents ch-* and appendices ap-*. "
|
|
253
|
+
"A dpkg page's document id is its name ('deb-control'); 'deb-control.5' works too. A section is an id, an "
|
|
254
|
+
"anchor (Policy's s-f-Source), a heading or a unique part of one. If nothing is imported the error says "
|
|
255
|
+
"how."
|
|
256
|
+
),
|
|
257
|
+
example_refs=(
|
|
258
|
+
"debpolicy:5.6.1",
|
|
259
|
+
"debpolicy:ch-controlfields§s-f-Source",
|
|
260
|
+
"debpolicy:deb-control§DESCRIPTION",
|
|
261
|
+
"debpolicy:5.6.1@aceb1daf¶2",
|
|
262
|
+
),
|
|
263
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""`primarysource debpolicy import|status|docs|outline|read|search|cite`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from primarysource.debpolicy.catalogue import SPEC
|
|
8
|
+
from primarysource.pkgdocs.serving import run
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(argv: Sequence[str]) -> int:
|
|
12
|
+
return run(SPEC, argv)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""The MCP server for `debpolicy` (`primarysource mcp --collection debpolicy`).
|
|
2
|
+
|
|
3
|
+
Imports `mcp` when this module loads.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from primarysource.debpolicy.catalogue import SPEC
|
|
9
|
+
from primarysource.pkgdocs.serving import build_server
|
|
10
|
+
|
|
11
|
+
server = build_server(SPEC)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""IETF Internet-Drafts as a primarysource collection: `primarysource mcp --collection draft`.
|
|
2
|
+
|
|
3
|
+
The portfolio tracks documents that exist only as drafts (RTR version 2 and the ASPA PDU, ASPA verification,
|
|
4
|
+
LibrePGP, the TURN REST API), which the RFC library cannot cite. This collection imports named drafts from the
|
|
5
|
+
IETF's own servers into `$PRIMARYSOURCE_HOME/draft/` and serves them like the `rfc` collection does, with one
|
|
6
|
+
difference that matters: a draft has revisions and a life cycle, so every result says which revision it read and
|
|
7
|
+
whether that draft is still alive.
|
|
8
|
+
|
|
9
|
+
`docs/draft.md` is the user-facing description. Nothing here needs a dependency beyond the standard library;
|
|
10
|
+
serving needs the `[mcp]` extra. `primarysource draft ...` runs `cli.py`.
|
|
11
|
+
"""
|