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.
Files changed (303) hide show
  1. primarysource/__init__.py +5 -0
  2. primarysource/bbf/__init__.py +56 -0
  3. primarysource/bbf/_xml.py +63 -0
  4. primarysource/bbf/catalogue.json +777 -0
  5. primarysource/bbf/catalogue.py +137 -0
  6. primarysource/bbf/cli.py +79 -0
  7. primarysource/bbf/datamodel.py +281 -0
  8. primarysource/bbf/importer.py +151 -0
  9. primarysource/bbf/library.py +380 -0
  10. primarysource/bbf/server.py +72 -0
  11. primarysource/budget.py +63 -0
  12. primarysource/c2sp/__init__.py +53 -0
  13. primarysource/c2sp/_fetch.py +5 -0
  14. primarysource/c2sp/catalogue.json +231 -0
  15. primarysource/c2sp/catalogue.py +95 -0
  16. primarysource/c2sp/cli.py +62 -0
  17. primarysource/c2sp/importer.py +90 -0
  18. primarysource/c2sp/library.py +309 -0
  19. primarysource/c2sp/markdown.py +31 -0
  20. primarysource/c2sp/server.py +68 -0
  21. primarysource/cavp/__init__.py +42 -0
  22. primarysource/cavp/cli.py +84 -0
  23. primarysource/cavp/data/vectors.toml +47 -0
  24. primarysource/cavp/importer.py +156 -0
  25. primarysource/cavp/model.py +279 -0
  26. primarysource/cavp/parse.py +269 -0
  27. primarysource/cavp/paths.py +17 -0
  28. primarysource/cavp/server.py +67 -0
  29. primarysource/cavp/tools.py +169 -0
  30. primarysource/cli.py +107 -0
  31. primarysource/cursor.py +82 -0
  32. primarysource/cve/__init__.py +5 -0
  33. primarysource/cve/__main__.py +6 -0
  34. primarysource/cve/cli.py +144 -0
  35. primarysource/cve/collection.py +20 -0
  36. primarysource/cve/config.py +58 -0
  37. primarysource/cve/errors.py +16 -0
  38. primarysource/cve/kernel.py +232 -0
  39. primarysource/cve/mcp.py +139 -0
  40. primarysource/cve/parse.py +318 -0
  41. primarysource/cve/py.typed +0 -0
  42. primarysource/cve/service.py +760 -0
  43. primarysource/cve/store.py +214 -0
  44. primarysource/cve/sync.py +438 -0
  45. primarysource/cve/views.py +304 -0
  46. primarysource/debpolicy/__init__.py +16 -0
  47. primarysource/debpolicy/catalogue.py +263 -0
  48. primarysource/debpolicy/cli.py +12 -0
  49. primarysource/debpolicy/server.py +11 -0
  50. primarysource/draft/__init__.py +11 -0
  51. primarysource/draft/catalogue.py +117 -0
  52. primarysource/draft/cli.py +149 -0
  53. primarysource/draft/collection.py +21 -0
  54. primarysource/draft/datatracker.py +158 -0
  55. primarysource/draft/importer.py +163 -0
  56. primarysource/draft/licence.py +89 -0
  57. primarysource/draft/names.py +64 -0
  58. primarysource/draft/net.py +133 -0
  59. primarysource/draft/segment.py +192 -0
  60. primarysource/draft/server.py +91 -0
  61. primarysource/draft/status.py +146 -0
  62. primarysource/draft/store.py +199 -0
  63. primarysource/draft/tracked.json +104 -0
  64. primarysource/draft/views.py +430 -0
  65. primarysource/draft/xmlmeta.py +74 -0
  66. primarysource/ecma/__init__.py +52 -0
  67. primarysource/ecma/_pdf.py +143 -0
  68. primarysource/ecma/catalogue.json +22 -0
  69. primarysource/ecma/catalogue.py +100 -0
  70. primarysource/ecma/cli.py +67 -0
  71. primarysource/ecma/identify.py +25 -0
  72. primarysource/ecma/importer.py +108 -0
  73. primarysource/ecma/library.py +240 -0
  74. primarysource/ecma/server.py +67 -0
  75. primarysource/errors.py +29 -0
  76. primarysource/fetch.py +360 -0
  77. primarysource/fsutil.py +150 -0
  78. primarysource/gitdocs/__init__.py +15 -0
  79. primarysource/gitdocs/catalogue.py +116 -0
  80. primarysource/gitdocs/cli.py +12 -0
  81. primarysource/gitdocs/server.py +8 -0
  82. primarysource/home.py +65 -0
  83. primarysource/iana/__init__.py +32 -0
  84. primarysource/iana/catalogue.json +244 -0
  85. primarysource/iana/catalogue.py +69 -0
  86. primarysource/iana/cli.py +69 -0
  87. primarysource/iana/importer.py +157 -0
  88. primarysource/iana/model.py +325 -0
  89. primarysource/iana/reader.py +315 -0
  90. primarysource/iana/server.py +67 -0
  91. primarysource/iana/store.py +83 -0
  92. primarysource/iana/xmlsafe.py +47 -0
  93. primarysource/identify.py +186 -0
  94. primarysource/ieee/__init__.py +58 -0
  95. primarysource/ieee/_html.py +49 -0
  96. primarysource/ieee/catalogue.json +74 -0
  97. primarysource/ieee/catalogue.py +123 -0
  98. primarysource/ieee/cli.py +79 -0
  99. primarysource/ieee/identify.py +24 -0
  100. primarysource/ieee/ieee802.py +47 -0
  101. primarysource/ieee/importer.py +305 -0
  102. primarysource/ieee/posix.py +233 -0
  103. primarysource/ieee/server.py +79 -0
  104. primarysource/index.py +446 -0
  105. primarysource/iso/__init__.py +51 -0
  106. primarysource/iso/cli.py +98 -0
  107. primarysource/iso/designation.py +95 -0
  108. primarysource/iso/identify.py +100 -0
  109. primarysource/iso/importer.py +224 -0
  110. primarysource/iso/library.py +249 -0
  111. primarysource/iso/server.py +69 -0
  112. primarysource/itu/__init__.py +41 -0
  113. primarysource/itu/__main__.py +8 -0
  114. primarysource/itu/asn1mod.py +478 -0
  115. primarysource/itu/catalog.py +472 -0
  116. primarysource/itu/cli.py +445 -0
  117. primarysource/itu/collection.py +20 -0
  118. primarysource/itu/corpus.py +118 -0
  119. primarysource/itu/errata.py +16 -0
  120. primarysource/itu/formal.py +565 -0
  121. primarysource/itu/identify.py +26 -0
  122. primarysource/itu/index.py +490 -0
  123. primarysource/itu/mcp.py +474 -0
  124. primarysource/itu/py.typed +0 -0
  125. primarysource/itu/sources.py +600 -0
  126. primarysource/itu/text.py +609 -0
  127. primarysource/kconfig/__init__.py +16 -0
  128. primarysource/kconfig/catalogue.py +98 -0
  129. primarysource/kconfig/cli.py +12 -0
  130. primarysource/kconfig/server.py +8 -0
  131. primarysource/model.py +63 -0
  132. primarysource/nist/__init__.py +5 -0
  133. primarysource/nist/collection.py +46 -0
  134. primarysource/nist/data/acquired-nist.json +1657 -0
  135. primarysource/nist/data/nist-catalog.json +12090 -0
  136. primarysource/nist/data/sources.toml +1996 -0
  137. primarysource/nist/identify.py +41 -0
  138. primarysource/nist/ingest/__init__.py +1 -0
  139. primarysource/nist/ingest/acquire_nist.py +128 -0
  140. primarysource/nist/ingest/controls.py +82 -0
  141. primarysource/nist/ingest/discover_nist.py +218 -0
  142. primarysource/nist/ingest/extract.py +48 -0
  143. primarysource/nist/ingest/fetch.py +211 -0
  144. primarysource/nist/ingest/index.py +14 -0
  145. primarysource/nist/ingest/oscal.py +480 -0
  146. primarysource/nist/ingest/prose_layouts.py +236 -0
  147. primarysource/nist/ingest/sources.py +94 -0
  148. primarysource/nist/ingest/status.py +255 -0
  149. primarysource/nist/models.py +7 -0
  150. primarysource/nist/paths.py +33 -0
  151. primarysource/nist/server/__init__.py +3 -0
  152. primarysource/nist/server/corpus.py +38 -0
  153. primarysource/nist/server/mcp.py +19 -0
  154. primarysource/nist/server/tools.py +35 -0
  155. primarysource/nist/spec.py +38 -0
  156. primarysource/pdfcorpus/__init__.py +16 -0
  157. primarysource/pdfcorpus/corpus.py +159 -0
  158. primarysource/pdfcorpus/fts.py +175 -0
  159. primarysource/pdfcorpus/index.py +70 -0
  160. primarysource/pdfcorpus/models.py +30 -0
  161. primarysource/pdfcorpus/server.py +96 -0
  162. primarysource/pdfcorpus/spec.py +37 -0
  163. primarysource/pdfcorpus/split.py +391 -0
  164. primarysource/pdfcorpus/tools.py +244 -0
  165. primarysource/pep/__init__.py +15 -0
  166. primarysource/pep/catalogue.py +191 -0
  167. primarysource/pep/cli.py +12 -0
  168. primarysource/pep/server.py +8 -0
  169. primarysource/pkgdocs/__init__.py +19 -0
  170. primarysource/pkgdocs/fetch.py +356 -0
  171. primarysource/pkgdocs/index.py +89 -0
  172. primarysource/pkgdocs/model.py +76 -0
  173. primarysource/pkgdocs/readers/__init__.py +1 -0
  174. primarysource/pkgdocs/readers/_common.py +134 -0
  175. primarysource/pkgdocs/readers/asciidoc.py +160 -0
  176. primarysource/pkgdocs/readers/latex.py +315 -0
  177. primarysource/pkgdocs/readers/markdown.py +173 -0
  178. primarysource/pkgdocs/readers/pod.py +132 -0
  179. primarysource/pkgdocs/readers/rst.py +239 -0
  180. primarysource/pkgdocs/readers/scdoc.py +108 -0
  181. primarysource/pkgdocs/runtime.py +552 -0
  182. primarysource/pkgdocs/serving.py +158 -0
  183. primarysource/pkgdocs/store.py +167 -0
  184. primarysource/pms/__init__.py +16 -0
  185. primarysource/pms/catalogue.py +88 -0
  186. primarysource/pms/cli.py +12 -0
  187. primarysource/pms/server.py +8 -0
  188. primarysource/py.typed +0 -0
  189. primarysource/readers/__init__.py +10 -0
  190. primarysource/readers/bookmarks.py +192 -0
  191. primarysource/readers/html.py +1312 -0
  192. primarysource/readers/markdown.py +189 -0
  193. primarysource/readers/pdf.py +187 -0
  194. primarysource/readers/xml.py +230 -0
  195. primarysource/refs.py +87 -0
  196. primarysource/registry.py +161 -0
  197. primarysource/render.py +49 -0
  198. primarysource/rfc/__init__.py +3 -0
  199. primarysource/rfc/__main__.py +6 -0
  200. primarysource/rfc/cite.py +204 -0
  201. primarysource/rfc/cli.py +903 -0
  202. primarysource/rfc/collection.py +19 -0
  203. primarysource/rfc/config.py +138 -0
  204. primarysource/rfc/errata.py +203 -0
  205. primarysource/rfc/fsutil.py +82 -0
  206. primarysource/rfc/graph.py +213 -0
  207. primarysource/rfc/index/__init__.py +1 -0
  208. primarysource/rfc/index/chunker.py +281 -0
  209. primarysource/rfc/index/embed.py +166 -0
  210. primarysource/rfc/index/hybrid.py +305 -0
  211. primarysource/rfc/index/lexical.py +223 -0
  212. primarysource/rfc/index/rfcxml.py +673 -0
  213. primarysource/rfc/index/search.py +508 -0
  214. primarysource/rfc/index/txtseg.py +585 -0
  215. primarysource/rfc/library.py +294 -0
  216. primarysource/rfc/mcp/__init__.py +1 -0
  217. primarysource/rfc/mcp/server.py +381 -0
  218. primarysource/rfc/packs/__init__.py +1 -0
  219. primarysource/rfc/packs/builder.py +228 -0
  220. primarysource/rfc/packs/collections/all-current-txt.json +10 -0
  221. primarysource/rfc/packs/collections/essentials.json +19 -0
  222. primarysource/rfc/packs/collections/fyi-current.json +10 -0
  223. primarysource/rfc/packs/collections/standards-current.json +10 -0
  224. primarysource/rfc/packs/collections/year-2025.json +10 -0
  225. primarysource/rfc/packs/collections/year-2026.json +10 -0
  226. primarysource/rfc/packs/collections.py +163 -0
  227. primarysource/rfc/packs/docsetexport.py +163 -0
  228. primarysource/rfc/packs/epub.py +159 -0
  229. primarysource/rfc/packs/export.py +306 -0
  230. primarysource/rfc/packs/htmlexport.py +82 -0
  231. primarysource/rfc/packs/serve.py +143 -0
  232. primarysource/rfc/packs/zimexport.py +171 -0
  233. primarysource/rfc/query.py +466 -0
  234. primarysource/rfc/subjects.py +457 -0
  235. primarysource/rfc/sync/__init__.py +1 -0
  236. primarysource/rfc/sync/downloader.py +132 -0
  237. primarysource/rfc/sync/manifest.py +162 -0
  238. primarysource/rfc/sync/scanner.py +49 -0
  239. primarysource/rfc/sync/state.py +464 -0
  240. primarysource/rfc/views.py +1067 -0
  241. primarysource/rpmdocs/__init__.py +15 -0
  242. primarysource/rpmdocs/catalogue.py +117 -0
  243. primarysource/rpmdocs/cli.py +12 -0
  244. primarysource/rpmdocs/server.py +8 -0
  245. primarysource/scaffold.py +223 -0
  246. primarysource/serve.py +75 -0
  247. primarysource/settings.py +152 -0
  248. primarysource/usb/__init__.py +5 -0
  249. primarysource/usb/collection.py +37 -0
  250. primarysource/usb/data/acquired.json +2025 -0
  251. primarysource/usb/data/sources-usb.toml +852 -0
  252. primarysource/usb/data/sources.toml +10 -0
  253. primarysource/usb/data/usb-catalog.json +1663 -0
  254. primarysource/usb/data/usb-classification.json +1986 -0
  255. primarysource/usb/data/usb-corpus-build.json +608 -0
  256. primarysource/usb/data/usb-pdf-choice.json +839 -0
  257. primarysource/usb/data/usb-split-measure.json +1803 -0
  258. primarysource/usb/ingest/__init__.py +1 -0
  259. primarysource/usb/ingest/acquire.py +130 -0
  260. primarysource/usb/ingest/discover_usb.py +88 -0
  261. primarysource/usb/ingest/extract.py +48 -0
  262. primarysource/usb/ingest/fetch.py +197 -0
  263. primarysource/usb/ingest/index.py +14 -0
  264. primarysource/usb/ingest/sources.py +85 -0
  265. primarysource/usb/models.py +7 -0
  266. primarysource/usb/paths.py +33 -0
  267. primarysource/usb/server/__init__.py +3 -0
  268. primarysource/usb/server/corpus.py +38 -0
  269. primarysource/usb/server/mcp.py +19 -0
  270. primarysource/usb/server/tools.py +35 -0
  271. primarysource/usb/spec.py +20 -0
  272. primarysource/usbids/__init__.py +56 -0
  273. primarysource/usbids/cli.py +65 -0
  274. primarysource/usbids/importer.py +272 -0
  275. primarysource/usbids/parse.py +283 -0
  276. primarysource/usbids/server.py +55 -0
  277. primarysource/usbids/store.py +80 -0
  278. primarysource/usbids/tools.py +174 -0
  279. primarysource/zim/__init__.py +5 -0
  280. primarysource/zim/__main__.py +8 -0
  281. primarysource/zim/_libzim.py +37 -0
  282. primarysource/zim/catalog.py +594 -0
  283. primarysource/zim/cli.py +555 -0
  284. primarysource/zim/collection.py +41 -0
  285. primarysource/zim/errors.py +100 -0
  286. primarysource/zim/groups.py +30 -0
  287. primarysource/zim/library.py +668 -0
  288. primarysource/zim/manage.py +636 -0
  289. primarysource/zim/markdown.py +1247 -0
  290. primarysource/zim/mcp.py +201 -0
  291. primarysource/zim/purl.py +130 -0
  292. primarysource/zim/py.typed +0 -0
  293. primarysource/zim/refs.py +90 -0
  294. primarysource/zim/service.py +1182 -0
  295. primarysource/zim/snapshot.py +197 -0
  296. primarysource/zim/sync.py +269 -0
  297. primarysource/zim/views.py +230 -0
  298. primarysource-0.1.0.dist-info/METADATA +118 -0
  299. primarysource-0.1.0.dist-info/RECORD +303 -0
  300. primarysource-0.1.0.dist-info/WHEEL +4 -0
  301. primarysource-0.1.0.dist-info/entry_points.txt +2 -0
  302. primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
  303. primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
@@ -0,0 +1,156 @@
1
+ """Import vector sets named in `vectors.toml`: download, check the pinned sha256, parse, store.
2
+
3
+ Every entry pins the sha256 of what it downloads (the zip, or the ACVP `prompt.json`), so a first fetch never
4
+ defines the truth. An entry without a pin is refused, and `primarysource cavp pin <file>` prints the digest to
5
+ write. Nothing here unpacks an archive to disk.
6
+
7
+ [[vector]]
8
+ id = "cavp/ecdsa/SigVer.rsp"
9
+ kind = "cavp"
10
+ algorithm = "ECDSA"
11
+ url = "https://csrc.nist.gov/.../186-4ecdsatestvectors.zip"
12
+ sha256 = "..." # of the zip
13
+ member = "186-4ecdsatestvectors/SigVer.rsp"
14
+
15
+ [[vector]]
16
+ id = "acvp/ECDSA-SigVer-1.0"
17
+ kind = "acvp"
18
+ url = ".../prompt.json" # pin a commit, not a branch
19
+ sha256 = "..."
20
+ expected_url = ".../expectedResults.json"
21
+ expected_sha256 = "..."
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import tomllib
27
+ import urllib.error
28
+ import urllib.request
29
+ from collections.abc import Callable
30
+ from dataclasses import dataclass
31
+ from pathlib import Path
32
+ from typing import IO, Any
33
+
34
+ from primarysource.cavp.model import MAX_SOURCE_BYTES, IndexEntry, Store, VectorSet, sha256_bytes
35
+ from primarysource.cavp.parse import check_set_id, parse_acvp, parse_rsp, read_member
36
+ from primarysource.errors import LibError
37
+
38
+ PACKAGE_DATA = Path(__file__).resolve().parent / "data"
39
+ USER_AGENT = "primarysource-cavp/0.1 (pinned test-vector fetch)"
40
+ TIMEOUT_SECONDS = 120
41
+ _FIELDS = {"id", "kind", "algorithm", "mode", "url", "sha256", "member", "expected_url", "expected_sha256"}
42
+
43
+ Opener = Callable[[urllib.request.Request, float], IO[bytes]]
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class Source:
48
+ id: str
49
+ kind: str
50
+ algorithm: str
51
+ url: str
52
+ sha256: str
53
+ member: str = ""
54
+ mode: str = ""
55
+ expected_url: str = ""
56
+ expected_sha256: str = ""
57
+
58
+
59
+ def load_sources(path: Path | None = None) -> tuple[Source, ...]:
60
+ """The entries of `vectors.toml`: the file given, else the one shipped in the package."""
61
+ file = path if path is not None else PACKAGE_DATA / "vectors.toml"
62
+ try:
63
+ raw: dict[str, Any] = tomllib.loads(file.read_text(encoding="utf-8"))
64
+ except (OSError, ValueError) as exc:
65
+ raise LibError(f"cannot read {file}: {exc}", "") from None
66
+ out: list[Source] = []
67
+ for row in raw.get("vector", []):
68
+ unknown = sorted(set(row) - _FIELDS)
69
+ if unknown:
70
+ raise LibError(f"{file}: unknown field(s) {', '.join(unknown)} in {row.get('id')!r}", "")
71
+ try:
72
+ source = Source(**row)
73
+ except TypeError as exc:
74
+ raise LibError(f"{file}: bad entry {row.get('id')!r} ({exc})", "") from None
75
+ check_set_id(source.id, source.kind)
76
+ if source.kind == "acvp" and bool(source.expected_url) != bool(source.expected_sha256):
77
+ raise LibError(f"{source.id}: expected_url and expected_sha256 go together", "")
78
+ out.append(source)
79
+ ids = [s.id for s in out]
80
+ if len(set(ids)) != len(ids):
81
+ raise LibError(f"{file}: duplicate vector ids", "")
82
+ return tuple(out)
83
+
84
+
85
+ def download(url: str, opener: Opener | None = None) -> bytes:
86
+ """GET `url`, at most `MAX_SOURCE_BYTES`. https only (a test may pass its own opener)."""
87
+ if opener is None and not url.startswith("https://"):
88
+ raise LibError(f"refusing {url!r}: only https downloads", "")
89
+ request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) # noqa: S310
90
+ try:
91
+ if opener is None:
92
+ stream = urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS) # noqa: S310
93
+ else:
94
+ stream = opener(request, TIMEOUT_SECONDS)
95
+ with stream:
96
+ body: bytes = stream.read(MAX_SOURCE_BYTES + 1)
97
+ except (urllib.error.URLError, TimeoutError, OSError) as exc:
98
+ raise LibError(f"download failed for {url}: {exc}", "check the network and the url") from None
99
+ if len(body) > MAX_SOURCE_BYTES:
100
+ raise LibError(f"{url}: larger than {MAX_SOURCE_BYTES} bytes, refusing", "")
101
+ return body
102
+
103
+
104
+ def check_pin(what: str, data: bytes, pinned: str) -> str:
105
+ """The digest of `data`, or a `LibError` when it is not the pinned one or nothing is pinned."""
106
+ actual = sha256_bytes(data)
107
+ if not pinned:
108
+ raise LibError(f"{what}: no sha256 pinned (this file is {actual})", "add sha256 to its vectors.toml entry")
109
+ if actual != pinned.lower():
110
+ raise LibError(
111
+ f"{what}: sha256 {actual} is not the pinned {pinned}", "the upstream file changed: review, then re-pin"
112
+ )
113
+ return actual
114
+
115
+
116
+ def build(source: Source, artifact: bytes, expected: bytes | None = None) -> VectorSet:
117
+ """Check pins and parse. `artifact` is the downloaded file (zip or json); `expected` the ACVP answers file."""
118
+ digest = check_pin(source.id, artifact, source.sha256)
119
+ if source.kind == "cavp":
120
+ member = read_member(artifact, source.member) if source.member else artifact
121
+ return parse_rsp(
122
+ member,
123
+ set_id=source.id,
124
+ algorithm=source.algorithm,
125
+ source_url=source.url,
126
+ sha256=digest,
127
+ member_sha256=sha256_bytes(member),
128
+ mode=source.mode,
129
+ )
130
+ if source.expected_url:
131
+ if expected is None:
132
+ raise LibError(f"{source.id}: the expectedResults file is needed", "")
133
+ check_pin(f"{source.id} expectedResults", expected, source.expected_sha256)
134
+ return parse_acvp(
135
+ artifact,
136
+ expected if source.expected_url else None,
137
+ set_id=source.id,
138
+ source_url=source.url,
139
+ sha256=digest,
140
+ member_sha256=digest,
141
+ )
142
+
143
+
144
+ def import_files(store: Store, source: Source, artifact: Path, expected: Path | None = None) -> IndexEntry:
145
+ """Import from files already on disk (the same pins apply)."""
146
+ size = artifact.stat().st_size
147
+ if size > MAX_SOURCE_BYTES:
148
+ raise LibError(f"{artifact}: {size} bytes is over the {MAX_SOURCE_BYTES} byte cap", "")
149
+ return store.write(build(source, artifact.read_bytes(), expected.read_bytes() if expected else None))
150
+
151
+
152
+ def fetch(store: Store, source: Source, opener: Opener | None = None) -> IndexEntry:
153
+ """Download `source`, verify its pins, parse and store it."""
154
+ artifact = download(source.url, opener)
155
+ expected = download(source.expected_url, opener) if source.expected_url else None
156
+ return store.write(build(source, artifact, expected))
@@ -0,0 +1,279 @@
1
+ """Test vectors: the data model, the ref grammar and the on-disk store.
2
+
3
+ A *set* is one vector file: a CAVP `.rsp` response file, or one ACVP vector set (`prompt.json` joined with
4
+ `expectedResults.json`). A set holds *groups* (CAVP `[...]` header lines, ACVP `testGroups`) and a group holds
5
+ *cases*. Refs, with `nist:` optional on input:
6
+
7
+ nist:cavp/ecdsa/SigVer.rsp a set
8
+ nist:cavp/ecdsa/SigVer.rsp#[P-256,SHA-256] a group
9
+ nist:cavp/ecdsa/SigVer.rsp#[P-256,SHA-256]/3 a case
10
+ nist:acvp/ECDSA-SigVer-1.0#1/3 ACVP: the group is the tgId, the case the tcId
11
+
12
+ `docs/cavp.md` has the full grammar.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import re
20
+ from dataclasses import asdict, dataclass, field
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from primarysource.errors import LibError
25
+ from primarysource.home import atomic_write
26
+
27
+ SCHEMA_VERSION = 1
28
+ REF_PREFIX = "nist:"
29
+ INDEX_FILE = "index.json"
30
+ SETS_DIR = "sets"
31
+
32
+ # Bounds. A source over these is refused, never read.
33
+ MAX_SOURCE_BYTES = 64 * 1024 * 1024
34
+ MAX_LINE_BYTES = 1024 * 1024
35
+ MAX_GROUPS = 100_000
36
+ MAX_CASES = 2_000_000
37
+ MAX_FIELDS_PER_CASE = 64
38
+ MAX_HEADER_LINES = 100
39
+
40
+ SET_ID = re.compile(r"(cavp|acvp)(/[A-Za-z0-9][A-Za-z0-9._+-]*)+")
41
+ KINDS = ("cavp", "acvp")
42
+
43
+ CREDIT = (
44
+ "Source: NIST Cryptographic Algorithm Validation Program (CAVP/ACVP). Created by employees of the National "
45
+ "Institute of Standards and Technology (NIST), not subject to copyright in the United States (17 U.S.C. 105); "
46
+ "provided AS IS. https://www.nist.gov/open/copyright-fair-use-and-licensing-statements-srd-data-software-"
47
+ "and-technical-series-publications"
48
+ )
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class Case:
53
+ """`id` is COUNT or tcId when the source has one, else the 1-based position in the group. `fields` are the
54
+ inputs (and, in CAVP, the expected result as well); `expected` is the joined ACVP expectedResults."""
55
+
56
+ id: str
57
+ fields: dict[str, Any]
58
+ expected: dict[str, Any] = field(default_factory=dict)
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class Group:
63
+ label: str
64
+ params: dict[str, Any]
65
+ cases: tuple[Case, ...]
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class VectorSet:
70
+ id: str
71
+ kind: str
72
+ algorithm: str
73
+ mode: str
74
+ source_url: str
75
+ sha256: str # of the downloaded artifact (the zip, or prompt.json)
76
+ member_sha256: str # of the file inside the zip; equal to sha256 when the artifact is the file
77
+ header: tuple[str, ...]
78
+ groups: tuple[Group, ...]
79
+ revision: str = ""
80
+
81
+ @property
82
+ def case_count(self) -> int:
83
+ return sum(len(g.cases) for g in self.groups)
84
+
85
+
86
+ @dataclass(frozen=True, slots=True)
87
+ class VectorRef:
88
+ set_id: str
89
+ group: str | None = None
90
+ case: str | None = None
91
+
92
+ def __str__(self) -> str:
93
+ return format_ref(self.set_id, self.group, self.case)
94
+
95
+
96
+ def sha256_bytes(data: bytes) -> str:
97
+ return hashlib.sha256(data).hexdigest()
98
+
99
+
100
+ def format_ref(set_id: str, group: str | None = None, case: str | None = None) -> str:
101
+ out = REF_PREFIX + set_id
102
+ if group is not None:
103
+ out += "#" + (f"[{group}]" if set_id.startswith("cavp/") else group)
104
+ if case is not None:
105
+ out += "/" + case
106
+ elif case is not None:
107
+ raise LibError("a case ref needs its group", "nist:<set>#<group>/<case>")
108
+ return out
109
+
110
+
111
+ def parse_ref(text: str) -> VectorRef:
112
+ """`nist:<set>[#<group>[/<case>]]`. The case is what follows the last `/`, so a group label may hold one."""
113
+ shown = text.strip().removeprefix(REF_PREFIX)
114
+ set_id, sep, rest = shown.partition("#")
115
+ hint = "e.g. nist:cavp/ecdsa/SigVer.rsp#[P-256,SHA-256]/3 or nist:acvp/ECDSA-SigVer-1.0#1/3"
116
+ if not SET_ID.fullmatch(set_id):
117
+ raise LibError(f"not a vector ref: {text!r}", hint)
118
+ if not sep:
119
+ return VectorRef(set_id)
120
+ group, slash, case = rest.rpartition("/")
121
+ if not slash:
122
+ group, case = rest, ""
123
+ if not group:
124
+ raise LibError(f"not a vector ref: {text!r} (empty group)", hint)
125
+ if set_id.startswith("cavp/"):
126
+ if not (group.startswith("[") and group.endswith("]") and len(group) > 2):
127
+ raise LibError(f"not a vector ref: {text!r} (a CAVP group is written in brackets)", hint)
128
+ group = group[1:-1]
129
+ return VectorRef(set_id, group, case or None)
130
+
131
+
132
+ # ---- serialisation -----------------------------------------------------------------------------------------
133
+
134
+
135
+ def set_to_json(s: VectorSet) -> dict[str, Any]:
136
+ return {
137
+ "schema": SCHEMA_VERSION,
138
+ "id": s.id,
139
+ "kind": s.kind,
140
+ "algorithm": s.algorithm,
141
+ "mode": s.mode,
142
+ "revision": s.revision,
143
+ "source_url": s.source_url,
144
+ "sha256": s.sha256,
145
+ "member_sha256": s.member_sha256,
146
+ "header": list(s.header),
147
+ "groups": [
148
+ {
149
+ "label": g.label,
150
+ "params": g.params,
151
+ "cases": [{"id": c.id, "fields": c.fields, "expected": c.expected} for c in g.cases],
152
+ }
153
+ for g in s.groups
154
+ ],
155
+ }
156
+
157
+
158
+ def _text(where: str, value: object) -> str:
159
+ if not isinstance(value, str):
160
+ raise LibError(f"{where}: expected a string", "", "primarysource cavp fetch")
161
+ return value
162
+
163
+
164
+ def set_from_json(raw: object, where: str) -> VectorSet:
165
+ if not isinstance(raw, dict) or raw.get("schema") != SCHEMA_VERSION:
166
+ raise LibError(f"{where}: not a schema {SCHEMA_VERSION} vector set", "", "primarysource cavp fetch")
167
+ groups: list[Group] = []
168
+ for g in raw.get("groups", []):
169
+ cases = tuple(
170
+ Case(_text("id", c["id"]), dict(c["fields"]), dict(c.get("expected") or {})) for c in g["cases"]
171
+ )
172
+ groups.append(Group(_text("label", g["label"]), dict(g.get("params") or {}), cases))
173
+ return VectorSet(
174
+ id=_text("id", raw.get("id")),
175
+ kind=_text("kind", raw.get("kind")),
176
+ algorithm=_text("algorithm", raw.get("algorithm")),
177
+ mode=_text("mode", raw.get("mode")),
178
+ source_url=_text("source_url", raw.get("source_url")),
179
+ sha256=_text("sha256", raw.get("sha256")),
180
+ member_sha256=_text("member_sha256", raw.get("member_sha256")),
181
+ header=tuple(_text("header", h) for h in raw.get("header", [])),
182
+ groups=tuple(groups),
183
+ revision=_text("revision", raw.get("revision", "")),
184
+ )
185
+
186
+
187
+ # ---- the store -----------------------------------------------------------------------------------------------
188
+
189
+
190
+ @dataclass(frozen=True, slots=True)
191
+ class IndexEntry:
192
+ id: str
193
+ kind: str
194
+ algorithm: str
195
+ mode: str
196
+ revision: str
197
+ source_url: str
198
+ sha256: str
199
+ groups: int
200
+ cases: int
201
+ file: str
202
+
203
+
204
+ class Store:
205
+ """A directory of imported sets: `index.json` plus `sets/<id with / as __>.json`."""
206
+
207
+ def __init__(self, root: Path) -> None:
208
+ self.root = root
209
+
210
+ def entries(self) -> list[IndexEntry]:
211
+ path = self.root / INDEX_FILE
212
+ if not path.is_file():
213
+ return []
214
+ try:
215
+ data = json.loads(path.read_text(encoding="utf-8"))
216
+ if data["schema"] != SCHEMA_VERSION:
217
+ raise KeyError("schema")
218
+ return [IndexEntry(**e) for e in data["sets"]]
219
+ except (ValueError, KeyError, TypeError) as exc:
220
+ raise LibError(
221
+ f"{path} is not a schema {SCHEMA_VERSION} index ({exc})", "", "primarysource cavp fetch"
222
+ ) from None
223
+
224
+ def require(self) -> list[IndexEntry]:
225
+ """The entries, or an error that names the import command when there are none."""
226
+ found = self.entries()
227
+ if not found:
228
+ raise LibError(f"no vector sets imported under {self.root}", "", "primarysource cavp fetch")
229
+ return found
230
+
231
+ def load(self, set_id: str) -> VectorSet:
232
+ found = self.require()
233
+ entry = next((e for e in found if e.id == set_id), None)
234
+ if entry is None:
235
+ names = ", ".join(e.id for e in found)
236
+ raise LibError(f"no vector set {set_id!r} (imported: {names})", "cavp_sets lists what is imported")
237
+ path = (self.root / entry.file).resolve()
238
+ if not path.is_relative_to(self.root.resolve()):
239
+ raise LibError(f"{set_id}: {entry.file!r} escapes the store", "", "primarysource cavp fetch")
240
+ try:
241
+ return set_from_json(json.loads(path.read_text(encoding="utf-8")), set_id)
242
+ except FileNotFoundError:
243
+ raise LibError(f"{set_id}: {entry.file} is missing", "", "primarysource cavp fetch") from None
244
+ except (ValueError, KeyError, TypeError) as exc:
245
+ raise LibError(f"{path} is damaged ({exc})", "", "primarysource cavp fetch") from None
246
+
247
+ def write(self, s: VectorSet) -> IndexEntry:
248
+ rel = f"{SETS_DIR}/{s.id.replace('/', '__')}.json"
249
+ body = json.dumps(set_to_json(s), ensure_ascii=False, separators=(",", ":"))
250
+ atomic_write(self.root / rel, body.encode("utf-8"))
251
+ entry = IndexEntry(
252
+ s.id, s.kind, s.algorithm, s.mode, s.revision, s.source_url, s.sha256, len(s.groups), s.case_count, rel
253
+ )
254
+ entries = sorted([e for e in self.entries() if e.id != s.id] + [entry], key=lambda e: e.id)
255
+ index = {"schema": SCHEMA_VERSION, "sets": [asdict(e) for e in entries]}
256
+ atomic_write(self.root / INDEX_FILE, (json.dumps(index, indent=2) + "\n").encode("utf-8"))
257
+ return entry
258
+
259
+
260
+ def norm_value(value: object) -> str:
261
+ """Compare as hex is compared: case-insensitive, without spaces or a `0x` prefix. CAVP writes lower case,
262
+ ACVP upper case."""
263
+ s = str(value).strip().lower().replace(" ", "")
264
+ return s[2:] if s.startswith("0x") else s
265
+
266
+
267
+ def find_group(s: VectorSet, label: str) -> Group:
268
+ for g in s.groups:
269
+ if g.label == label:
270
+ return g
271
+ shown = ", ".join(g.label for g in s.groups[:10]) + (", ..." if len(s.groups) > 10 else "")
272
+ raise LibError(f"{s.id} has no group {label!r} (groups: {shown})", f"cavp_groups('nist:{s.id}') lists them")
273
+
274
+
275
+ def find_case(g: Group, case_id: str) -> Case:
276
+ for c in g.cases:
277
+ if c.id == case_id:
278
+ return c
279
+ raise LibError(f"group {g.label!r} has no case {case_id!r} (it has {len(g.cases)} cases)", "")