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,478 @@
|
|
|
1
|
+
"""Reading an ASN.1 module as a document: header, IMPORTS, EXPORTS, and one section per assignment.
|
|
2
|
+
|
|
3
|
+
This is a structural reader, not a validator. It finds the boundaries the module itself draws - the module
|
|
4
|
+
reference and its object identifier, the IMPORTS clause, the EXPORTS clause, and where each `Name ::= ...`
|
|
5
|
+
assignment begins and ends - and hands back the original text of each piece. It never interprets types, so a
|
|
6
|
+
construct it does not know about costs nothing worse than a coarser section. `asn1core` decides what is valid.
|
|
7
|
+
|
|
8
|
+
The input is hostile by assumption: it came over the network from a third party, or from a file a user pointed
|
|
9
|
+
at. So every loop is bounded by the input length, nesting is a counter rather than recursion, and the limits in
|
|
10
|
+
`Limits` turn pathological input into `ModuleError`, never into a hang or an unbounded allocation.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import html
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
|
|
19
|
+
ROOT_ARCS = {
|
|
20
|
+
"itu-t": 0,
|
|
21
|
+
"ccitt": 0,
|
|
22
|
+
"iso": 1,
|
|
23
|
+
"joint-iso-itu-t": 2,
|
|
24
|
+
"joint-iso-ccitt": 2,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ModuleError(ValueError):
|
|
29
|
+
"""The text is not an ASN.1 module this reader can take apart, or it exceeds a limit."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Limits:
|
|
34
|
+
max_octets: int = 2 * 1024 * 1024
|
|
35
|
+
max_tokens: int = 400_000
|
|
36
|
+
max_depth: int = 64
|
|
37
|
+
max_assignments: int = 20_000
|
|
38
|
+
max_modules: int = 64
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
DEFAULT_LIMITS = Limits()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class Token:
|
|
46
|
+
text: str
|
|
47
|
+
start: int
|
|
48
|
+
end: int
|
|
49
|
+
line: int
|
|
50
|
+
first_on_line: bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Import:
|
|
55
|
+
symbols: tuple[str, ...]
|
|
56
|
+
module: str
|
|
57
|
+
oid_text: str | None
|
|
58
|
+
with_successors: bool = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class Assignment:
|
|
63
|
+
name: str
|
|
64
|
+
kind: str # type | class | value | set
|
|
65
|
+
parameterised: bool
|
|
66
|
+
start: int
|
|
67
|
+
end: int
|
|
68
|
+
line: int
|
|
69
|
+
text: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Module:
|
|
74
|
+
name: str
|
|
75
|
+
oid_text: str | None
|
|
76
|
+
oid: str | None
|
|
77
|
+
tagging: str | None
|
|
78
|
+
extensibility_implied: bool
|
|
79
|
+
exports: str | tuple[str, ...] | None # None: no EXPORTS clause (everything is exported)
|
|
80
|
+
imports: tuple[Import, ...]
|
|
81
|
+
assignments: tuple[Assignment, ...]
|
|
82
|
+
iri: str | None = None
|
|
83
|
+
edition_note: str | None = None
|
|
84
|
+
source: str = ""
|
|
85
|
+
warnings: list[str] = field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
def assignment(self, name: str) -> Assignment | None:
|
|
88
|
+
for item in self.assignments:
|
|
89
|
+
if item.name == name:
|
|
90
|
+
return item
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------------------------------- unwrap
|
|
95
|
+
|
|
96
|
+
_TAG = re.compile(r"<[^>]{0,2000}>")
|
|
97
|
+
_BREAK = re.compile(r"<br\s*/?>", re.IGNORECASE)
|
|
98
|
+
_ACTIVE = re.compile(r"<(script|style)\b.{0,200000}?</\1\s*>", re.IGNORECASE | re.DOTALL)
|
|
99
|
+
_HEADER_EDITION = re.compile(r"ITU-T\s+([A-Z]\.\d+(?:\.\d+)?)\s*\((\d{2})/(\d{4})\)")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def unwrap_html(raw: bytes, limits: Limits = DEFAULT_LIMITS) -> tuple[str, str | None]:
|
|
103
|
+
"""Source text of a module published inside an HTML wrapper, and the edition its header names.
|
|
104
|
+
|
|
105
|
+
ITU serves each module as `<html><body><pre>...</pre></body></html>` with one comment line linking to
|
|
106
|
+
the Recommendation. Tags are dropped without being interpreted, entities are decoded, and nothing in the
|
|
107
|
+
wrapper is ever executed or followed. Plain text with no wrapper passes through unchanged.
|
|
108
|
+
"""
|
|
109
|
+
if len(raw) > limits.max_octets:
|
|
110
|
+
raise ModuleError(f"{len(raw)} octets exceeds the {limits.max_octets} limit")
|
|
111
|
+
if b"\x00" in raw:
|
|
112
|
+
raise ModuleError("NUL octet in module text")
|
|
113
|
+
text = raw.decode("utf-8", errors="replace")
|
|
114
|
+
stripped = text.lstrip()
|
|
115
|
+
if stripped[:5].lower() in {"<html", "<!doc"} or stripped.lower().startswith("<pre"):
|
|
116
|
+
start = stripped.lower().find("<pre")
|
|
117
|
+
body = stripped[start:] if start >= 0 else stripped
|
|
118
|
+
end = body.lower().rfind("</pre>")
|
|
119
|
+
if end >= 0:
|
|
120
|
+
body = body[:end]
|
|
121
|
+
text = html.unescape(_TAG.sub("", _BREAK.sub("\n", _ACTIVE.sub("", body))))
|
|
122
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
123
|
+
found = _HEADER_EDITION.search(text[:4000])
|
|
124
|
+
note = f"{found.group(1)} ({found.group(2)}/{found.group(3)})" if found else None
|
|
125
|
+
return text, note
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ------------------------------------------------------------------------------------------------- tokenizer
|
|
129
|
+
|
|
130
|
+
_IDENT = re.compile(r"#?[A-Za-z](?:[A-Za-z0-9]|-(?!-))*")
|
|
131
|
+
_NUMBER = re.compile(r"\d+")
|
|
132
|
+
_QUOTED = re.compile(r"'[0-9A-Fa-f01\s]*'[BbHh]")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def tokenize(text: str, limits: Limits = DEFAULT_LIMITS) -> list[Token]:
|
|
136
|
+
"""Tokens with comments removed. Comments and strings are skipped, never recursed into."""
|
|
137
|
+
tokens: list[Token] = []
|
|
138
|
+
pos, line, n = 0, 1, len(text)
|
|
139
|
+
first = True
|
|
140
|
+
while pos < n:
|
|
141
|
+
ch = text[pos]
|
|
142
|
+
if ch == "\n":
|
|
143
|
+
line += 1
|
|
144
|
+
pos += 1
|
|
145
|
+
first = True
|
|
146
|
+
continue
|
|
147
|
+
if ch in " \t\f\v":
|
|
148
|
+
pos += 1
|
|
149
|
+
continue
|
|
150
|
+
if text.startswith("--", pos):
|
|
151
|
+
pos += 2
|
|
152
|
+
while pos < n and text[pos] != "\n" and not text.startswith("--", pos):
|
|
153
|
+
pos += 1
|
|
154
|
+
if text.startswith("--", pos):
|
|
155
|
+
pos += 2
|
|
156
|
+
continue
|
|
157
|
+
if text.startswith("/*", pos):
|
|
158
|
+
depth = 1
|
|
159
|
+
pos += 2
|
|
160
|
+
while pos < n and depth:
|
|
161
|
+
if text.startswith("/*", pos):
|
|
162
|
+
depth += 1
|
|
163
|
+
pos += 2
|
|
164
|
+
elif text.startswith("*/", pos):
|
|
165
|
+
depth -= 1
|
|
166
|
+
pos += 2
|
|
167
|
+
else:
|
|
168
|
+
if text[pos] == "\n":
|
|
169
|
+
line += 1
|
|
170
|
+
first = True
|
|
171
|
+
pos += 1
|
|
172
|
+
if depth:
|
|
173
|
+
raise ModuleError(f"unterminated /* comment (line {line})")
|
|
174
|
+
continue
|
|
175
|
+
start = pos
|
|
176
|
+
if ch == '"':
|
|
177
|
+
pos += 1
|
|
178
|
+
while True:
|
|
179
|
+
end = text.find('"', pos)
|
|
180
|
+
if end < 0:
|
|
181
|
+
raise ModuleError(f"unterminated string (line {line})")
|
|
182
|
+
pos = end + 1
|
|
183
|
+
if text.startswith('"', pos):
|
|
184
|
+
pos += 1
|
|
185
|
+
continue
|
|
186
|
+
break
|
|
187
|
+
line += text.count("\n", start, pos)
|
|
188
|
+
elif text.startswith("::=", pos) or text.startswith("...", pos):
|
|
189
|
+
pos += 3
|
|
190
|
+
elif text.startswith("..", pos):
|
|
191
|
+
pos += 2
|
|
192
|
+
elif (
|
|
193
|
+
(ch == "'" and (m := _QUOTED.match(text, pos)))
|
|
194
|
+
or (m := _IDENT.match(text, pos))
|
|
195
|
+
or (m := _NUMBER.match(text, pos))
|
|
196
|
+
):
|
|
197
|
+
pos = m.end()
|
|
198
|
+
else:
|
|
199
|
+
pos += 1
|
|
200
|
+
tokens.append(Token(text[start:pos], start, pos, line, first))
|
|
201
|
+
first = False
|
|
202
|
+
if len(tokens) > limits.max_tokens:
|
|
203
|
+
raise ModuleError(f"more than {limits.max_tokens} tokens")
|
|
204
|
+
return tokens
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
_OPEN = {"{": "}", "(": ")", "[": "]"}
|
|
208
|
+
_CLOSE = {"}", ")", "]"}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ----------------------------------------------------------------------------------------------------- parse
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _oid_numeric(arcs_text: str) -> str | None:
|
|
215
|
+
inner = arcs_text.strip()[1:-1]
|
|
216
|
+
parts = re.findall(r"[A-Za-z][A-Za-z0-9-]*\s*\(\s*\d+\s*\)|[A-Za-z][A-Za-z0-9-]*|\d+", inner)
|
|
217
|
+
numbers: list[str] = []
|
|
218
|
+
for index, part in enumerate(parts):
|
|
219
|
+
named = re.fullmatch(r"([A-Za-z][A-Za-z0-9-]*)\s*\(\s*(\d+)\s*\)", part)
|
|
220
|
+
if named:
|
|
221
|
+
numbers.append(named.group(2))
|
|
222
|
+
elif part.isdigit():
|
|
223
|
+
numbers.append(part)
|
|
224
|
+
elif index == 0 and part in ROOT_ARCS:
|
|
225
|
+
numbers.append(str(ROOT_ARCS[part]))
|
|
226
|
+
else:
|
|
227
|
+
return None
|
|
228
|
+
return ".".join(numbers) if numbers else None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _classify(left: list[Token], right: list[Token]) -> tuple[str, bool]:
|
|
232
|
+
name = left[0].text
|
|
233
|
+
# `Name {A, B} ::=` is a parameterised definition; `Name CLASS-NAME ::=` is an object set.
|
|
234
|
+
parameterised = len(left) > 1 and left[1].text == "{"
|
|
235
|
+
if name[0].islower():
|
|
236
|
+
return "value", False
|
|
237
|
+
if len(left) == 1:
|
|
238
|
+
return (
|
|
239
|
+
"class" if right and right[0].text in {"CLASS", "TYPE-IDENTIFIER", "ABSTRACT-SYNTAX"} else "type"
|
|
240
|
+
), False
|
|
241
|
+
if parameterised:
|
|
242
|
+
closer = _match(left, 1)
|
|
243
|
+
if closer == len(left) - 1:
|
|
244
|
+
return ("class" if right and right[0].text == "CLASS" else "type"), True
|
|
245
|
+
return "set", False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _match(tokens: list[Token], index: int) -> int:
|
|
249
|
+
depth = 0
|
|
250
|
+
for i in range(index, len(tokens)):
|
|
251
|
+
if tokens[i].text in _OPEN:
|
|
252
|
+
depth += 1
|
|
253
|
+
elif tokens[i].text in _CLOSE:
|
|
254
|
+
depth -= 1
|
|
255
|
+
if depth == 0:
|
|
256
|
+
return i
|
|
257
|
+
return len(tokens) - 1
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _braced(tokens: list[Token], index: int, text: str, limits: Limits) -> tuple[str, int]:
|
|
261
|
+
"""Text of the bracketed group starting at tokens[index], and the index after it."""
|
|
262
|
+
depth = 0
|
|
263
|
+
for i in range(index, len(tokens)):
|
|
264
|
+
t = tokens[i].text
|
|
265
|
+
if t in _OPEN:
|
|
266
|
+
depth += 1
|
|
267
|
+
if depth > limits.max_depth:
|
|
268
|
+
raise ModuleError(f"nesting deeper than {limits.max_depth} (line {tokens[i].line})")
|
|
269
|
+
elif t in _CLOSE:
|
|
270
|
+
depth -= 1
|
|
271
|
+
if depth == 0:
|
|
272
|
+
return text[tokens[index].start : tokens[i].end], i + 1
|
|
273
|
+
raise ModuleError(f"unbalanced bracket (line {tokens[index].line})")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _parse_imports(tokens: list[Token], i: int, text: str, limits: Limits) -> tuple[list[Import], int]:
|
|
277
|
+
"""tokens[i] is the first token after IMPORTS. Returns the clauses and the index after the `;`."""
|
|
278
|
+
result: list[Import] = []
|
|
279
|
+
symbols: list[str] = []
|
|
280
|
+
n = len(tokens)
|
|
281
|
+
while i < n:
|
|
282
|
+
t = tokens[i].text
|
|
283
|
+
if t == ";":
|
|
284
|
+
return result, i + 1
|
|
285
|
+
if t == ",":
|
|
286
|
+
i += 1
|
|
287
|
+
elif t == "FROM":
|
|
288
|
+
i += 1
|
|
289
|
+
if i >= n or not tokens[i].text.lstrip("#")[:1].isalpha():
|
|
290
|
+
raise ModuleError(f"IMPORTS: FROM without a module (line {tokens[i - 1].line})")
|
|
291
|
+
module = tokens[i].text
|
|
292
|
+
i += 1
|
|
293
|
+
oid = None
|
|
294
|
+
if i < n and tokens[i].text == "{":
|
|
295
|
+
oid, i = _braced(tokens, i, text, limits)
|
|
296
|
+
elif (
|
|
297
|
+
i + 1 < n
|
|
298
|
+
and tokens[i].text[:1].islower()
|
|
299
|
+
and tokens[i + 1].text not in {",", "FROM"}
|
|
300
|
+
and tokens[i + 1].text[:1] != ","
|
|
301
|
+
):
|
|
302
|
+
oid = tokens[i].text
|
|
303
|
+
i += 1
|
|
304
|
+
successors = False
|
|
305
|
+
if i + 1 < n and tokens[i].text == "WITH" and tokens[i + 1].text in {"SUCCESSORS", "DESCENDANTS"}:
|
|
306
|
+
successors = True
|
|
307
|
+
i += 2
|
|
308
|
+
result.append(Import(tuple(symbols), module, oid, successors))
|
|
309
|
+
symbols = []
|
|
310
|
+
elif t == "{":
|
|
311
|
+
# a parameterised import `Name{}`: keep the braces on the symbol
|
|
312
|
+
group, i = _braced(tokens, i, text, limits)
|
|
313
|
+
if symbols:
|
|
314
|
+
symbols[-1] += group
|
|
315
|
+
elif t[:1].isalpha() or t[:1] == "#":
|
|
316
|
+
symbols.append(t)
|
|
317
|
+
i += 1
|
|
318
|
+
else:
|
|
319
|
+
raise ModuleError(f"IMPORTS: unexpected {t!r} (line {tokens[i].line})")
|
|
320
|
+
if len(result) > limits.max_assignments:
|
|
321
|
+
raise ModuleError("IMPORTS: too many clauses")
|
|
322
|
+
raise ModuleError("IMPORTS clause has no terminating ';'")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def parse_modules(text: str, *, limits: Limits = DEFAULT_LIMITS) -> list[Module]:
|
|
326
|
+
"""Every module in `text`, in order. At least one is required."""
|
|
327
|
+
tokens = tokenize(text, limits)
|
|
328
|
+
modules: list[Module] = []
|
|
329
|
+
i, n = 0, len(tokens)
|
|
330
|
+
while i < n:
|
|
331
|
+
if len(modules) >= limits.max_modules:
|
|
332
|
+
raise ModuleError(f"more than {limits.max_modules} modules in one file")
|
|
333
|
+
if not tokens[i].text[:1].isupper():
|
|
334
|
+
i += 1
|
|
335
|
+
continue
|
|
336
|
+
# header: Name [{oid}] DEFINITIONS [tagging] [EXTENSIBILITY IMPLIED] ::= BEGIN
|
|
337
|
+
name = tokens[i].text
|
|
338
|
+
j = i + 1
|
|
339
|
+
oid_text = None
|
|
340
|
+
if j < n and tokens[j].text == "{":
|
|
341
|
+
oid_text, j = _braced(tokens, j, text, limits)
|
|
342
|
+
iri = None
|
|
343
|
+
if j < n and tokens[j].text.startswith('"'): # an IRI value after the OID (X.680 module header)
|
|
344
|
+
iri = tokens[j].text
|
|
345
|
+
j += 1
|
|
346
|
+
if not (j < n and tokens[j].text in {"DEFINITIONS", "ENCODING-DEFINITIONS"}):
|
|
347
|
+
i += 1
|
|
348
|
+
continue
|
|
349
|
+
j += 1
|
|
350
|
+
tagging = None
|
|
351
|
+
implied = False
|
|
352
|
+
while j < n and tokens[j].text != "::=":
|
|
353
|
+
word = tokens[j].text
|
|
354
|
+
if word in {"EXPLICIT", "IMPLICIT", "AUTOMATIC"}:
|
|
355
|
+
tagging = word
|
|
356
|
+
elif word == "EXTENSIBILITY":
|
|
357
|
+
implied = True
|
|
358
|
+
elif word not in {"TAGS", "IMPLIED"}:
|
|
359
|
+
raise ModuleError(f"module header: unexpected {word!r} (line {tokens[j].line})")
|
|
360
|
+
j += 1
|
|
361
|
+
if not (j + 1 < n and tokens[j].text == "::=" and tokens[j + 1].text == "BEGIN"):
|
|
362
|
+
raise ModuleError(f"module {name}: DEFINITIONS without ::= BEGIN")
|
|
363
|
+
j += 2
|
|
364
|
+
body_start = j
|
|
365
|
+
# body: everything to the END that closes it, tracking bracket depth
|
|
366
|
+
depth = 0
|
|
367
|
+
k = j
|
|
368
|
+
end_index = -1
|
|
369
|
+
unbalanced = False
|
|
370
|
+
while k < n:
|
|
371
|
+
t = tokens[k].text
|
|
372
|
+
if t in _OPEN:
|
|
373
|
+
depth += 1
|
|
374
|
+
if depth > limits.max_depth:
|
|
375
|
+
raise ModuleError(f"nesting deeper than {limits.max_depth} (line {tokens[k].line})")
|
|
376
|
+
elif t in _CLOSE:
|
|
377
|
+
depth = max(0, depth - 1)
|
|
378
|
+
elif t == "END" and (depth == 0 or tokens[k].first_on_line):
|
|
379
|
+
# END is reserved, so one starting a line closes the module even inside an unclosed bracket:
|
|
380
|
+
# ITU publishes at least one module with a missing brace (X.509 (2000) AttributeCertificate
|
|
381
|
+
# Definitions), and refusing the whole module for it would hide every other assignment.
|
|
382
|
+
if depth:
|
|
383
|
+
unbalanced = True
|
|
384
|
+
end_index = k
|
|
385
|
+
break
|
|
386
|
+
k += 1
|
|
387
|
+
if end_index < 0:
|
|
388
|
+
raise ModuleError(f"module {name}: no END")
|
|
389
|
+
module = _parse_body(name, oid_text, tagging, implied, tokens[body_start:end_index], text, limits)
|
|
390
|
+
module.iri = iri
|
|
391
|
+
if unbalanced:
|
|
392
|
+
module.warnings.append(
|
|
393
|
+
"unbalanced brackets in the published text; later assignments may be merged into one section"
|
|
394
|
+
)
|
|
395
|
+
modules.append(module)
|
|
396
|
+
i = end_index + 1
|
|
397
|
+
if not modules:
|
|
398
|
+
raise ModuleError("no module found: expected `Name {oid} DEFINITIONS ::= BEGIN ... END`")
|
|
399
|
+
return modules
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _parse_body(
|
|
403
|
+
name: str,
|
|
404
|
+
oid_text: str | None,
|
|
405
|
+
tagging: str | None,
|
|
406
|
+
implied: bool,
|
|
407
|
+
body: list[Token],
|
|
408
|
+
text: str,
|
|
409
|
+
limits: Limits,
|
|
410
|
+
) -> Module:
|
|
411
|
+
exports: str | tuple[str, ...] | None = None
|
|
412
|
+
imports: list[Import] = []
|
|
413
|
+
i, n = 0, len(body)
|
|
414
|
+
if i < n and body[i].text == "EXPORTS":
|
|
415
|
+
i += 1
|
|
416
|
+
names: list[str] = []
|
|
417
|
+
while i < n and body[i].text != ";":
|
|
418
|
+
if body[i].text != ",":
|
|
419
|
+
names.append(body[i].text)
|
|
420
|
+
i += 1
|
|
421
|
+
i += 1
|
|
422
|
+
exports = "all" if names == ["All"] else tuple(names)
|
|
423
|
+
if i < n and body[i].text == "IMPORTS":
|
|
424
|
+
imports, i = _parse_imports(body, i + 1, text, limits)
|
|
425
|
+
# assignments: depth-0 `::=` tokens; each assignment begins at the first token of its line
|
|
426
|
+
marks: list[int] = []
|
|
427
|
+
depth = 0
|
|
428
|
+
for k in range(i, n):
|
|
429
|
+
t = body[k].text
|
|
430
|
+
if t in _OPEN:
|
|
431
|
+
depth += 1
|
|
432
|
+
elif t in _CLOSE:
|
|
433
|
+
depth = max(0, depth - 1)
|
|
434
|
+
elif t == "::=" and depth == 0:
|
|
435
|
+
marks.append(k)
|
|
436
|
+
if len(marks) > limits.max_assignments:
|
|
437
|
+
raise ModuleError(f"more than {limits.max_assignments} assignments")
|
|
438
|
+
starts: list[int] = []
|
|
439
|
+
floor = i
|
|
440
|
+
for mark in marks:
|
|
441
|
+
s = mark - 1
|
|
442
|
+
while s > floor and not body[s].first_on_line:
|
|
443
|
+
s -= 1
|
|
444
|
+
# a left side that wraps: `Name\n {params} ::=` or `name\n Type ::=`
|
|
445
|
+
while s > floor and body[s].text in {"{", "::="}:
|
|
446
|
+
s -= 1
|
|
447
|
+
starts.append(max(s, floor))
|
|
448
|
+
floor = mark + 1
|
|
449
|
+
assignments: list[Assignment] = []
|
|
450
|
+
for index, mark in enumerate(marks):
|
|
451
|
+
s = starts[index]
|
|
452
|
+
e = starts[index + 1] if index + 1 < len(starts) else n
|
|
453
|
+
left, right = body[s:mark], body[mark + 1 : e]
|
|
454
|
+
if not left or not left[0].text[:1].isalpha():
|
|
455
|
+
continue
|
|
456
|
+
kind, parameterised = _classify(left, right)
|
|
457
|
+
end_char = body[e - 1].end if e > mark + 1 else body[mark].end
|
|
458
|
+
assignments.append(
|
|
459
|
+
Assignment(
|
|
460
|
+
name=left[0].text,
|
|
461
|
+
kind=kind,
|
|
462
|
+
parameterised=parameterised,
|
|
463
|
+
start=left[0].start,
|
|
464
|
+
end=end_char,
|
|
465
|
+
line=left[0].line,
|
|
466
|
+
text=text[left[0].start : end_char],
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
return Module(
|
|
470
|
+
name=name,
|
|
471
|
+
oid_text=oid_text,
|
|
472
|
+
oid=_oid_numeric(oid_text) if oid_text else None,
|
|
473
|
+
tagging=tagging,
|
|
474
|
+
extensibility_implied=implied,
|
|
475
|
+
exports=exports,
|
|
476
|
+
imports=tuple(imports),
|
|
477
|
+
assignments=tuple(assignments),
|
|
478
|
+
)
|