Skip to content

fix(vendor): retire the IPX socket scrape, its data source is gone (#507) - #511

Merged
JarryShaw merged 4 commits into
mainfrom
fix/507-ipx-socket-retire-scrape
Sep 19, 2026
Merged

JarryShaw merged 4 commits into
mainfrom
fix/507-ipx-socket-retire-scrape

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Closes #507.

Both halves of the issue re-verified against the live page

fetch result
default requests User-Agent 403, 126 bytes
descriptive User-Agent 200, 114117 bytes

403 body: Please set a user-agent and respect our robot policy https://w.wiki/4wJS.

Live page (revid 1372814585) carries 1 wikitable, headers ['Octets', 'Field'] — the IPX header format table. 0x0451 and NetWare Core Protocol appear nowhere in the HTML. The Socket_number section still exists but now holds two sentences of prose and no table.

Removal revision is 1371327031 (2026-08-25), comment "…deleted some irrelevant technical tables".

One correction to the issue's framing

#507 says the next regeneration "will still lose every socket the scrape used to supply". Measured, it does not — it fails outright (IndexError at vendor/ipx/socket.py:82), and vendor/__main__.py:59 catches and warns, so the constant file is simply never rewritten. Silent loss would require the page to regain a fourth wikitable.

Why retirement rather than pinning an archived revision

The archived revision was verified viable on the data side — oldid=1368657333 returns 200 with 4 wikitables, table[3] headers ['Socket number', 'Protocol'], 19 body rows, and running the old process() against it reproduces today's constant file exactly.

It is not viable on the code side. The User-Agent is applied in pcapkit/vendor/default.py:404page = requests.get(self.LINK), no headers — so any non-None LINK, archived or not, takes the same 403. Fixing that belongs in the shared base class and affects three other crawlers (see below), so it is deliberately out of scope here.

The policy argument settles it, and it is recorded in the script: the registry was Novell's and is closed. Novell's own documentation still says "developers can contact Novell to reserve well-known sockets" — and there is no longer a Novell to contact, so a crawler has nothing to pick up on its next run.

Per-entry sourcing

  • Novell, IPX Addressing, Table 2 (novell.com/documentation/nw6p/ipx_enu/data/hvvqznoa.html, 200, still live) — 0x451 NCP, 0x452 SAP, 0x453 RIP, 0x455 NetBIOS, 0x456 Diagnostics. This is also the citation the Wikipedia article itself used.
  • RFC 1132 line 104 — 0x8060
  • RFC 17910x9091 TCP, 0x9092 UDP, 0x9093 IPXF

Stated honestly in the script: 0x0001, 0x0002, 0x0003, 0x0457, 0x4003 rest on the archived revision alone. RFCs 1132, 1362, 1377, 1551, 1552, 1553, 1634, 1791, 1973 and 2043 were swept for those codes — 0 hits in all ten. 0x0000 is not a registry entry but IPX's own field default, per #492/#503.

No loss — proven by byte identity

pcapkit/const/ipx/socket.py regenerates byte-identically: same md5, 15 members, same names, values and order. Socket(0x0000)Unspecified, so #503's member is intact.

Byte identity was chosen deliberately as the strongest available proof, which meant keeping the LLC_4 member name and its #: LLC [ 4 ] comment — a Wikipedia footnote artefact (<ref name="RFC1132" /> rendering as [4]). Cleaning that up would rename a public member, so it is flagged with a # NOTE: instead.

Verification

Regeneration completes with both requests.get and requests.Session.request patched to raise, confirming no network path remains.

tests/vendor/test_ipx_socket_unit.py — 9 tests, 16 subtests — pins LINK is None, byte-for-byte reproduction of the committed constant, the full 15-member list, registry/enum agreement, Socket(0), RANGES order, and rejection of out-of-range values.

The tests are mutation-tested, because the first attempt was vacuouspurge_modules re-imports from disk, so in-memory mutations were discarded and every mutation "passed". Redone by mutating the file on disk with a hash-checked restore: dropping 0x9093 fails 2 tests, dropping 0x0000 fails 3, renaming LLC_4 fails 2, reordering RANGES fails 2.

suite result
unit tier, before 934 passed, 8 skipped, 1952 subtests
unit tier, after 943 passed, 8 skipped, 1968 subtests

Delta is exactly the +9 tests / +16 subtests added. isort -l100 -ppcapkit --check-only clean, matching what cron-vendor.yml runs.

Follow-ups this surfaced, deliberately not fixed here

  • pcapkit/vendor/ipx/packet.py is broken identically — same article, same removal revision, find_all('table', class_='wikitable')[1] at :82 against a page that now has one table.
  • Three other crawlers take the same 403: vendor/vlan/priority_level.py, vendor/ftp/return_code.py, vendor/ipx/packet.py. The fix is a descriptive User-Agent at vendor/default.py:404.
  • The three pcapng/* crawlers point at a dead URLwww.ietf.org/staging/draft-tuexen-opsawg-pcapng-02.html 404s regardless of User-Agent. Working replacements: ietf.org/archive/id/… and datatracker.ietf.org/doc/html/….

)

Wikipedia removed the well-known-socket-number table in revision 1371327031
(2026-08-25), and rejects `requests`' default User-Agent with 403 besides. So
the crawler could not fetch the page, and there was nothing left to parse if
it could.

- drop `LINK`, so `_request()` short-circuits and never touches the network;
  `bs4` and `re` imports go with it
- add `DATA` (15 rows) and `RANGES` (5 rows), each entry carrying its source
  in-line: Novell's own *IPX Addressing* Table 2 for the 0x045x sockets, and
  RFC 1132 / RFC 1791 for 0x8060 / 0x9091-0x9093
- record in a `# NOTE:` why this is retirement rather than a re-point: the
  registry was Novell's and is closed, so a crawler has nothing to pick up
- add `tests/vendor/test_ipx_socket_unit.py`, which pins byte-for-byte
  reproduction of the committed constant file

`pcapkit/const/ipx/socket.py` regenerates byte-identically -- 15 members,
same names, values and order, `Socket(0x0000)` intact. 9 passed / 16 subtests;
unit tier 943 passed / 1968 subtests, +9/+16 against baseline.
@JarryShaw
JarryShaw force-pushed the fix/507-ipx-socket-retire-scrape branch from ee020b3 to 010dc03 Compare September 19, 2026 16:08
@JarryShaw

Copy link
Copy Markdown
Owner Author

Review: CHANGES NEEDED — prose and citations, not code

The three load-bearing claims all hold under independent measurement. What needs fixing is the sourcing prose plus one docs line, and none of it touches pcapkit/const/ipx/socket.py, so none of it costs the byte-identity proof.

Verified. Byte identity: regenerated by constructing Socket() for real — md5 7548ec44c461a5c1d30d57751ae54f42, 3298 bytes, before and after, matching origin/main at 980e52f0c too; tree left clean; all 15 members and Socket(0x0000) -> Unspecified intact. No network: same md5 with requests.get/post/request, Session.request/send, HTTPAdapter.send and socket.connect/connect_ex/getaddrinfo/create_connection all patched to raise. Tests not vacuous: all four claimed mutations reproduce exactly (2 / 3 / 2 / 2), and 11 more of mine — 5 vendor-side, 6 const-side — are all caught, 15/15. Suite 943 passed / 8 skipped / 1968 subtests, matching the PR. Transcription from oldid=1368657333 is row for row with zero discrepancies.

Needs changing:

  1. pcapkit/vendor/ipx/socket.py:116-119 — the name TCP over IPXF contradicts its own citation. RFC 1791 §3 is "Running TCP Over IPX" (line 99) and line 129 says "running TCP over IPXF is not recommended". Value is right; fix the comment, not the member. (UDP over IPXF for 0x9092 is correct.)
  2. pcapkit/vendor/ipx/socket.py:127-130 — "if not their exact bounds" understates a contradiction: the cited Novell page says "between 0x4000 and 0x7FFF", not 0x4FFF.
  3. pcapkit/vendor/ipx/socket.py:81-83 — "the only source found" is refuted, and correcting it strengthens the table. IANA's live xns-protocol-types has an "Assigned well-known socket numbers" registry (1 Routing Information, 2 Echo, 3 Router Error, 40-77 Experimental) citing Xerox XSIS 028112, whose Appendix D gives "range 1 to 3000 decimal", "all other ... ephemeral", column header octal. That confirms this PR's own numbers: 3000 = 0x0BB8, 3001 = 0x0BB9, octal 40-77 = 0x0020-0x003F.
  4. pcapkit/vendor/ipx/socket.py:88-96 — the cited Novell Table 2 has seven rows; 0x9001 (NLSP) and 0x9004 (IPXWAN) are dropped silently. 0x9004 is reserved by RFC 1634 line 552, which is in the "swept, 0 hits" set — so narrow that claim to the five specific codes.
  5. docs/source/pcapkit/vendor/ipx.rst:39 — still :members: FLAG, LINK for a class that no longer defines LINK. Across all 117 vendor autoclass entries this is now the only code/docs mismatch in the repo; 22 of the other 23 LINK-less modules are :members: FLAG. The :47 footnote also still cites the dead #Socket_number anchor.
  6. PR description — "IndexError at vendor/ipx/socket.py:82" is not today's behaviour: measured, the 403 raises RequestException, caught at pcapkit/vendor/__main__.py:59. IndexError appears only once the fetch is fixed (confirmed separately at exactly :82). The code comment at :42-44 is right; the description is not. Conclusion unaffected.

On the HAS_REQUESTS guard — I would add plain requests to the test extra. The guard is well built and honestly documented, but measured in a requests-free venv it is 9 skipped in 0.02s, and cron-vendor.yml has .[all] yet never runs pytest — so the pin protects neither path. It is the pin that matters: test_regeneration_reproduces_the_committed_constant_file caught 14 of my 15 mutations and was the only thing that caught 2 (a description-only edit; deleting a _missing_ branch). Green CI here means "9 tests did not run", not "the pin passed". tests/_tiers.py:16-19 already requires the unit tier to pass on .[test], and one line keeps that true while making the pin execute. Cleaner follow-up: move import requests at pcapkit/vendor/default.py:23 into Vendor._request — it is used nowhere else — which drops the need for the guard entirely.

On LLC_4 — keep it. Byte identity is this PR's whole evidentiary basis, and the member has four occurrences repo-wide, so renaming with a deprecated alias is cheap later. The reading is confirmed more strongly than stated: the cell markup is id="cite_ref-RFC1132_4-1", and RFC 1132 has only refs [1] and [2] and never names the socket "LLC".

Could not verify: the origin/main baseline (934 / 1952) was not re-run — it follows arithmetically from the measured 943 / 1968 minus the measured 9 / 16, with zero failures; the Sphinx docs were not built, so item 5 rests on the directive plus the 117-module scan (the build passes no -W); the socket-number column of the 1981 XSIS scan is OCR-garbled, so the 1/2/3 name mapping comes from IANA rather than the scan; #492/#503 history taken as given beyond confirming Unspecified = 0x0000 resolves. Python 3.14.7, Linux only.

Minimum to merge: items 1-5 and the test-extra line.

JarryShaw added a commit that referenced this pull request Sep 19, 2026
… runnable in CI

Addresses review feedback on #511.

- pyproject.toml: add requests and beautifulsoup4 to the `test` extra. The nine
  tests pinning the byte-for-byte regeneration skipped in CI, so the only guard
  on the hand-maintained registry never ran on any CI path. Adding requests
  alone is not enough: pcapkit/vendor/__init__.py imports every crawler
  unconditionally and eight of them import bs4 at module scope, so the tests
  fail with ModuleNotFoundError rather than run.
- tests: widen the skip guard from requests to requests + bs4 to match, and pin
  member *names* in test_unlisted_sockets_still_resolve. Every branch of the
  generated _missing_ returns `value`, so asserting the value alone passed with
  three of the five range branches deleted.
- vendor/ipx/socket.py: correct the per-entry sourcing. IANA's xns-protocol-types
  and Xerox XSIS 028112 Appendix D source 0x0001-0x0003 and three of the five
  ranges, so only 0x0457 and 0x4003 rest on the archived revision alone. Record
  that Socket.TCP_over_IPXF is misnamed (RFC 1791 s3 assigns 0x9091 to TCP over
  plain IPX and advises against IPXF), that Novell's Table 2 has seven rows and
  two are absent, and that the 0x4000-0x4FFF bound contradicts Novell's 0x7FFF.
  None renamed or added: that would end the byte-identity proof.
- docs: note that the crawler no longer crawls, and point the footnote at the
  archived revision rather than a live anchor.

pcapkit/const/ipx/socket.py is unchanged (md5 7548ec44c461a5c1d30d57751ae54f42).
@JarryShaw

Copy link
Copy Markdown
Owner Author

Review feedback addressed — pushed dd888da06

pcapkit/const/ipx/socket.py is untouched: md5 7548ec44c461a5c1d30d57751ae54f42, 3298 bytes, identical to origin/main. The byte-identity proof this PR rests on survives the prose round.

The review's own prescription does not work, and this is the substantive finding

Adding plain requests to the test extra does not make the nine tests run — it makes all nine fail:

ModuleNotFoundError: No module named 'bs4'   (from pcapkit/vendor/reg/linktype.py:17)

pcapkit/vendor/__init__.py carries 17 unconditional crawler imports, and 8 crawler modules import bs4 at module scope (ftp/return_code.py, ipx/packet.py, ipx/socket.py, pcapng/block_type.py, pcapng/option_type.py, pcapng/record_type.py, reg/linktype.py, vlan/priority_level.py). The package's own try: import bs4 / except ModuleNotFoundError: warn(...) at __init__.py:25-30 suppresses only the warning, not the ModuleNotFoundError those imports raise.

Measured in a clean venv built from a base interpreter, pip install -e '.[test]' each time, with pcapkit.__file__ asserted:

test extra result
as this PR stood 9 skipped — every reason "requests not installed (vendor extra)"
+ requests only 9 failedModuleNotFoundError: No module named 'bs4'
+ requests, beautifulsoup4 9 passed, 16 subtests

Both are added, plain — no [socks], no [html5lib], since no unit-tier test goes through a proxy or parses HTML. The skip guard is widened from requests to requests and bs4 so a deps-less checkout still skips rather than errors.

The suite baseline in this PR's description is not what CI measures

CI installs .[test], and a clean .[test] venv gives 869 passed / 82 skipped / 1910 subtests, going to 878 / 73 / 1926 with this change — a delta of exactly +9 passed, −9 skipped, +16 subtests. The 82 skips are the optional engines (dpkt, scapy, pyshark, pypcapfile, cryptography, emoji), none of which .[test] installs. Higher counts quoted earlier came from an environment carrying all extras; the figures above are what unit-tests.yml actually runs.

The name-pinning hardening, with its proof

test_unlisted_sockets_still_resolve now pins member.name as well as the value, because every branch of the generated _missing_ returns value — so asserting the value alone passes with range branches deleted. Proof: dropping the 0x0BB9 <= value <= 0xFFFF masking branch gives

8 failed, 1 passed, 6 subtests passed
SUBFAILED 0x0BB9, 0x4000, 0x4FFF, 0x7FFF, 0x8000, 0x8061, 0x9094, 0xFFFF

— exactly the eight sockets that branch covered. Restored afterwards, md5 re-verified.

Corrections to the review and to this description

  • 117 vendor autoclass entries is 119. 119 .. autoclass:: under docs/source/pcapkit/vendor/; 117 carry :members: (the crawler-class count meant), and the other two are Vendor/VendorMeta with :no-members: at default.rst:10,35. ipx.rst:39 was the sole :members: mismatch among the 23 LINK-less crawlers and is fixed here.
  • The LLC_4 argument's premise was wrong. RFC 1132 has four references, not two: [1] IEEE 802.2, [2] Novell IPX, [3] RFC 791, [4] RFC 826 ARP. The conclusion holds — its [4] is ARP, and the markup id cite_ref-RFC1132_4-1 proves the marker is Wikipedia's footnote — but the stated reason should not be repeated. Also, the spaces in LLC [ 4 ] are not in the page: bare get_text() yields LLC[4], and the spaces are an artefact of the old scrape joining cell strings and picking up the cite-bracket spans as separate tokens.
  • "Will still lose every socket the scrape used to supply" is not what happens — the next regeneration fails outright and the constant file is simply never rewritten. The 403 fetch failure comes first (Vendor._request retries, then raises, and vendor/__main__.py:59 turns it into a VendorRuntimeWarning); the IndexError at vendor/ipx/socket.py:82 is only reachable once the fetch is fixed. Silent loss would need the page to regain a fourth wikitable.
  • Per-entry sourcing is stronger than this PR claimed. IANA's live xns-protocol-types registry and Xerox XSIS 028112 (December 1981) Appendix D both source 0x00010x0003 and three of the five ranges, so only 0x0457 and 0x4003 rest on the archived revision alone. The octal reading of Appendix D is settled by arithmetic rather than OCR: 3000 → 0x0BB8, 3001 → 0x0BB9, octal 40–77 → 0x00200x003F, all matching the transcribed bounds, where decimal 40–77 would give 0x00280x004D and does not.

Recorded in code, deliberately not fixed

Socket.TCP_over_IPXF (0x9091) is misnamed: RFC 1791 assigns it in §3, "Running TCP Over IPX", which opens "Unlike UDP, TCP runs directly over IPX" and closes "Hence, running TCP over IPXF is not recommended." The value is right; the name came from the archived revision and describes the one arrangement its own RFC advises against. UDP over IPXF for 0x9092 is accurate. Also absent with good sourcing: 0x9001 NLSP and 0x9004 IPXWAN (Novell's Table 2 has seven rows; 0x9004 is reserved by RFC 1634 line 552, RFC 1551 line 532 verbatim, and RFC 1362 line 253), and an entry for all-ones 0xFFFF, which XSIS reserves to mean "all". And (0x4000, 0x4FFF) is contradicted by Novell's "between 0x4000 and 0x7FFF are dynamic sockets", not merely rounded.

None renamed or added: each would end the byte-identity proof, so they belong in their own review.

… runnable in CI

Addresses review feedback on #511.

- pyproject.toml: add requests and beautifulsoup4 to the `test` extra. The nine
  tests pinning the byte-for-byte regeneration skipped in CI, so the only guard
  on the hand-maintained registry never ran on any CI path. Adding requests
  alone is not enough: pcapkit/vendor/__init__.py imports every crawler
  unconditionally and eight of them import bs4 at module scope, so the tests
  fail with ModuleNotFoundError rather than run.
- tests: widen the skip guard from requests to requests + bs4 to match, and pin
  member *names* in test_unlisted_sockets_still_resolve. Every branch of the
  generated _missing_ returns `value`, so asserting the value alone passed with
  three of the five range branches deleted.
- vendor/ipx/socket.py: correct the per-entry sourcing. IANA's xns-protocol-types
  and Xerox XSIS 028112 Appendix D source 0x0001-0x0003 and three of the five
  ranges, so only 0x0457 and 0x4003 rest on the archived revision alone. Record
  that Socket.TCP_over_IPXF is misnamed (RFC 1791 s3 assigns 0x9091 to TCP over
  plain IPX and advises against IPXF), that Novell's Table 2 has seven rows and
  two are absent, and that the 0x4000-0x4FFF bound contradicts Novell's 0x7FFF.
  None renamed or added: that would end the byte-identity proof.
- docs: note that the crawler no longer crawls, and point the footnote at the
  archived revision rather than a live anchor.

pcapkit/const/ipx/socket.py is unchanged (md5 7548ec44c461a5c1d30d57751ae54f42).
@JarryShaw
JarryShaw force-pushed the fix/507-ipx-socket-retire-scrape branch from dd888da to 1da80b1 Compare September 19, 2026 22:14
@JarryShaw

Copy link
Copy Markdown
Owner Author

Re-review of dd888da06: GOOD TO GO — fixes applied in 1da80b122

Second cross-review, different model, scoped to the delta 6007b19d2..dd888da06.

The load-bearing claim reproduced character for character

It built three separate clean venvs from git archive 6007b19d2, pip install -e '.[test]' fresh each time:

test extra reproduced
as it stood 9 skipped, 1 warning in 0.02s
+ plain requests only 9 failed, 2 warnings in 8.23s — traceback bottoms out at exactly pcapkit/vendor/reg/linktype.py:17
+ requests and beautifulsoup4 9 passed, 2 warnings, 16 subtests passed

So overriding the original review's prescription was correct: plain requests genuinely does not work. It also checked something the claim never addressed — pytest categorises all nine as FAILED rather than ERROR even though the exception fires inside setUp(), matching the wording used here.

It found the undetected mutation, as asked

The three masked range branches are each a strict subset of an earlier always-checked-first branch, so they are unreachable at runtime. Widening 0x0020 <= value <= 0x003F to 0x0025 <= value <= 0x0999 — still fully masked — is completely invisible to test_unlisted_sockets_still_resolve alone: 1 passed, 14 subtests passed.

The whole-file run does catch it, but via test_regeneration_reproduces_the_committed_constant_file re-deriving from unmutated source and diffing byte-for-byte — a different mechanism than the name-pinning under review. So the hardening has a real blind spot for masked-branch bounds, while the suite as a whole does not; and a realistic edit (change RANGES in the vendor module, then regenerate) would additionally trip test_range_order_is_preserved's exact tuple compare regardless of masking. Recorded rather than changed.

Two corrections, one of them in committed code — both now fixed in 1da80b122

1. The bs4 importer count was wrong in its own context, and the confusion is worth explaining because three different numbers were all correct.

tree unguarded module-scope bs4 imports
main 8 — both ipx/packet.py and ipx/socket.py still scrape
#518's branch 7#518 retires ipx/packet.py's scrape
this branch 7 — this PR retires ipx/socket.py's scrape

Both branches reach seven by removing a different file. An earlier comment of mine asserted eight as a correction to someone measuring seven — that was measured against main, so it was true there and wrong as a correction. The figure is now seven in pyproject.toml and tests/vendor/test_ipx_socket_unit.py, verified on this branch. The mechanism is unaffected either way: one unguarded import (reg/linktype.py, pulled in via reg/__init__.py) is sufficient to reproduce the failure, and it does.

2. "RFC 1132 has four references" is wrong — it has eight. Fetched from rfc-editor.org and the IETF datatracker independently: [1] IEEE 802.2, [2] Novell IPX, [3] RFC 791, [4] RFC 826/ARP, [5] ISO DIS 8473, [6] Xerox XNSG 068504, [7] RFC 1042, [8] Cohen. The sub-claim that [4] is RFC 826/ARP is correct, and the conclusion holds on ground the reviewer verified directly — the raw wikitext reuses <ref name="RFC1132" /> at the LLC row and the rendered anchor is cite_ref-RFC1132_4-1, where the _4 is Wikipedia's own footnote position and -1 its second use on the page, nothing to do with RFC 1132's bibliography. My "four references" line conflated two unrelated numbering systems and was wrong on the count as well. GitHub-conversation text only, not committed.

Verified more strongly than this PR claimed

The PR worried that Appendix D of Xerox XSIS 028112 was OCR-garbled and settled the octal radix by arithmetic instead. The reviewer installed poppler and read the scanned 1981 document directly — page 58, PDF page 67 — and it is fully legible: "socket numbers zero and all ones are reserved to mean unknown and all respectively", "Well-known socket numbers have the range 1 to 3000 decimal", "All other socket numbers are ephemeral", and a table header reading "Well-Known Socket (octal)" outright, with rows 1/2/3/40-77 exactly as transcribed. So three of the five ranges rest on a directly-legible primary source, and the arithmetic workaround is no longer load-bearing. Novell's live page independently confirms the other two bounds, including that (0x4000, 0x4FFF) really is contradicted by Novell's own 0x4000-0x7FFF.

Also confirmed exactly: 119 autoclass with 117 :members: and 2 :no-members:; bare get_text() yielding LLC[4]; the 0x9004 wording in RFC 1634 and 1551 as "protocol exchange" against RFC 1362's "information exchange"; 17 unconditional crawler imports; byte-identity at md5 7548ec44c461a5c1d30d57751ae54f42, 3298 bytes, with git diff origin/main on that file empty and the whole vendor-module diff being comments only.

On TCP_over_IPXF, agreed — with a better reason than mine

RFC 1791 §3 confirmed verbatim: titled "Running TCP Over IPX", opening "Unlike UDP, TCP runs directly over IPX", closing "Hence, running TCP over IPXF is not recommended." The value 0x9091 is right and only the label is wrong. Defer the rename — and the sharper argument is that even a deprecated alias changes extend_enum's generated output, which would end the byte-identity proof that is this PR's entire evidentiary basis. Its own change, later.

Not independently verified

The full-suite delta (869→878 passed, 82→73 skipped, 1910→1926 subtests) was not re-run, under host-care constraints after an earlier OOM incident; the nine-test delta is confirmed instead. The exhaustive "0 hits" sweep across all ten RFCs for 0x0457/0x4003 was not replicated — the three bearing on 0x9004 were spot-checked and matched exactly.

@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE — cross-reviewed twice, the test-extra fix reproduced in three clean venvs, and pcapkit/const/ipx/socket.py byte-identical to main (md5 7548ec44…).

@JarryShaw
JarryShaw merged commit 2215d0c into main Sep 19, 2026
23 checks passed
@JarryShaw
JarryShaw deleted the fix/507-ipx-socket-retire-scrape branch September 19, 2026 23:18
JarryShaw added a commit that referenced this pull request Sep 20, 2026
…#519)

Closes #519.

#501 fixed one wrong exception name and forty stale `Args:` labels by hand.
#519 showed the class was not exhausted. Re-deriving its census turned up one
finding that is not a docstring defect at all, plus a third class the issue
never mentions.

`TCP._read_mode_sack` documented `ProtocolError: If length is **NOT** multiply
of 8 plus 2` and never checked it. The tempting reading -- and the first one
taken here -- is that the clause was stale, like the other five phantoms. It is
the opposite, and three things settle it: RFC 2018 gives SACK a 2-octet header
followed by 8-octet edge pairs; both neighbouring readers validate their own
lengths, `_read_mode_sackpmt` on `!= 2` and `_read_mode_echo` on `!= 6`, with
the same message; and nothing upstream enforced it either, since the schema's
`ListField` consumes as many whole 8-octet items as it finds and ignores the
tail. So the docstring was right and the code was wrong.

- tcp.py: add the `(length - 2) % 8` check to `_read_mode_sack`, raising
  `ProtocolError` with the message its two siblings already use.
- httpv2.py: drop the `Raises: ProtocolError` clause from the five
  `_read_http_*` readers that cannot raise it. `_read_http_none` shows the
  mechanism -- its `raise` is commented out and replaced by a `ProtocolWarning`
  on the next line. All 11 `_read_http_*` methods carried the clause; the 6
  that genuinely raise keep it.
- tcp.py, twice: un-indent a `Returns:` header that sat inside the `Args:`
  block. This is the class the issue missed, and it loses documentation rather
  than merely misnaming it -- napoleon parses the header as a parameter, so the
  page rendered `:param Returns:` and carried no `:returns:` at all. Measured
  through `GoogleDocstring` directly, before and after.
- frame.py, schema/application/httpv2.py, schema/misc/pcapng.py: correct four
  `Args:` labels naming a parameter that does not exist. None of the four takes
  `**kwargs`, so each is a hard `TypeError`, not a cosmetic slip:
  `Frame.register(code=..., module=IPv4)` really does raise `TypeError: got an
  unexpected keyword argument 'module'`, while `protocol=` is accepted.
- hip.py: document the parameters `_make_param_reg_response` and
  `_make_param_route_dst` omit, in the wording their own siblings
  `_make_param_reg_failed` and `_make_param_route_via` already use.
- tests: `test_tcp_sack_length_unit.py` pins the length rule, accepted and
  rejected cases both. `test_docstring_contract.py` walks every function under
  `pcapkit/` and checks each documented name against the real signature, each
  documented exception against the real `raise` statements, and each section
  header against its indentation. It imports no pcapkit, reading source under
  its own root, so no editable install can shadow what it measures.

Census re-derived at this base rather than taken from the issue, which counted
against an older one:

    phantom Raises:                  6   -> 0 remaining
    Args: naming a missing parameter 13  -> 9 remaining, all allowlisted
    swallowed section headers        3   -> 1 remaining, allowlisted

The `Args:` figure is 13 here and was 14 when #519 was written: #511 retired the
IPX socket scrape and renamed that crawler's `soup` parameter to `data`,
incidentally fixing one. The nine that remain are in files owned by other
in-flight changes and are recorded in `KNOWN_DEFECTS` with a reason each, under
a test asserting every one still reproduces -- so the list cannot rot into a
description of bugs that are gone. That test has already earned its keep: it
failed on exactly the IPX entry during the rebase, which is how the stale entry
was found and removed.

Be clear about what is provable. A docstring label is not executed, so the
corrections cannot fail a test individually -- the contract test is what pins
them, by deriving the answer from the code instead of snapshotting today's
wrongness. Verified by reintroducing each class into a scratch tree: baseline
exit 0, phantom `Raises:` exit 1, bad `Args:` label exit 1, re-indented
`Returns:` exit 1. The two `Raises:` assertions are complementary rather than
redundant, and `_read_http_none` proves it -- `any(header.flags)` in its body
makes the conservative reachability check judge it possibly-raising, so only the
commented-out-raise check catches it. Measured: reachability exit 0, commented
raise exit 1.

The SACK change is the one with a real behavioural fails-before. Lengths 3, 11,
14, 17, 19 and 25 all parsed clean on `c8fd97bcd` with the SACK option present
in `tcp.info['options']`, and all six now raise `ProtocolError: TCP: [OptNo 5]
invalid format`.

Two things deliberately not done. All 12 functions carrying both `**kwargs` and
an `Args:` section without documenting `**kwargs` are in hip.py -- 438 of 450
document it elsewhere -- and that set includes the two siblings this change
copies its wording from, so fixing 4 of the 12 is what would make hip.py
inconsistent. And the SACK check implements exactly the documented rule, not
RFC 2018's one-to-four block bound; `length=2` is degenerate and still parses,
recorded in the test as a decision.

Tests: 5 passed / 10 subtests (contract), 3 passed / 10 subtests (SACK),
25 passed / 19 subtests (tier guard), 81 passed / 73 subtests (transport and
schema units, unchanged by the new check).

An independent second scanner, written from scratch against the same baseline,
reproduced all three counts (6 / 13 / 3) and the residuals (0 / 9 / 1), and
turned up three things now recorded in the test module:

- `_documented_names` justified its relative-indent measurement by saying
  `__doc__` is dedented at compile time. True of `__doc__` and irrelevant here,
  since this module reads `ast.get_docstring(..., clean=False)`, which preserves
  raw source indentation -- measured [0, 8, 12, 12] against [0, 0, 4, 4] for the
  dedented forms. The implementation was right for a different reason (nesting
  depth moves the absolute column); the rationale is corrected rather than left
  wrong in a checker for wrong rationales.
- `_raised_names` resolves only `Name` and `Attribute` raise targets, and
  `ast.walk` attributes a nested `def`'s raise to the enclosing function. Both
  can only miss a phantom, never fail a correct docstring, and both are now
  documented with their measurements: 856 `Name` + 2 `Attribute` + 0 `Subscript`
  across 858 raise targets, and 11 documented functions with a nested `def`, 3
  raising inside it. The second is the correct answer rather than a gap --
  `_read_param_locator_set` documents `ProtocolError` and raises none itself,
  but calls a `_read_locator` helper that does.
- One reported defect was a false positive and is recorded as a trap:
  `Raw.__post_init__` documents `error` and `alias`, has neither in its
  signature, and assigns a local `alias` -- but forwards `**kwargs` to `read`,
  which declares both as keyword-only and uses them. Reporting it would ask for
  correct documentation to be deleted.
JarryShaw added a commit that referenced this pull request Sep 20, 2026
…#519) (#535)

Closes #519.

#501 fixed one wrong exception name and forty stale `Args:` labels by hand.
#519 showed the class was not exhausted. Re-deriving its census turned up one
finding that is not a docstring defect at all, plus a third class the issue
never mentions.

`TCP._read_mode_sack` documented `ProtocolError: If length is **NOT** multiply
of 8 plus 2` and never checked it. The tempting reading -- and the first one
taken here -- is that the clause was stale, like the other five phantoms. It is
the opposite, and three things settle it: RFC 2018 gives SACK a 2-octet header
followed by 8-octet edge pairs; both neighbouring readers validate their own
lengths, `_read_mode_sackpmt` on `!= 2` and `_read_mode_echo` on `!= 6`, with
the same message; and nothing upstream enforced it either, since the schema's
`ListField` consumes as many whole 8-octet items as it finds and ignores the
tail. So the docstring was right and the code was wrong.

- tcp.py: add the `(length - 2) % 8` check to `_read_mode_sack`, raising
  `ProtocolError` with the message its two siblings already use.
- httpv2.py: drop the `Raises: ProtocolError` clause from the five
  `_read_http_*` readers that cannot raise it. `_read_http_none` shows the
  mechanism -- its `raise` is commented out and replaced by a `ProtocolWarning`
  on the next line. All 11 `_read_http_*` methods carried the clause; the 6
  that genuinely raise keep it.
- tcp.py, twice: un-indent a `Returns:` header that sat inside the `Args:`
  block. This is the class the issue missed, and it loses documentation rather
  than merely misnaming it -- napoleon parses the header as a parameter, so the
  page rendered `:param Returns:` and carried no `:returns:` at all. Measured
  through `GoogleDocstring` directly, before and after.
- frame.py, schema/application/httpv2.py, schema/misc/pcapng.py: correct four
  `Args:` labels naming a parameter that does not exist. None of the four takes
  `**kwargs`, so each is a hard `TypeError`, not a cosmetic slip:
  `Frame.register(code=..., module=IPv4)` really does raise `TypeError: got an
  unexpected keyword argument 'module'`, while `protocol=` is accepted.
- hip.py: document the parameters `_make_param_reg_response` and
  `_make_param_route_dst` omit, in the wording their own siblings
  `_make_param_reg_failed` and `_make_param_route_via` already use.
- tests: `test_tcp_sack_length_unit.py` pins the length rule, accepted and
  rejected cases both. `test_docstring_contract.py` walks every function under
  `pcapkit/` and checks each documented name against the real signature, each
  documented exception against the real `raise` statements, and each section
  header against its indentation. It imports no pcapkit, reading source under
  its own root, so no editable install can shadow what it measures.

Census re-derived at this base rather than taken from the issue, which counted
against an older one:

    phantom Raises:                  6   -> 0 remaining
    Args: naming a missing parameter 13  -> 9 remaining, all allowlisted
    swallowed section headers        3   -> 1 remaining, allowlisted

The `Args:` figure is 13 here and was 14 when #519 was written: #511 retired the
IPX socket scrape and renamed that crawler's `soup` parameter to `data`,
incidentally fixing one. The nine that remain are in files owned by other
in-flight changes and are recorded in `KNOWN_DEFECTS` with a reason each, under
a test asserting every one still reproduces -- so the list cannot rot into a
description of bugs that are gone. That test has already earned its keep: it
failed on exactly the IPX entry during the rebase, which is how the stale entry
was found and removed.

Be clear about what is provable. A docstring label is not executed, so the
corrections cannot fail a test individually -- the contract test is what pins
them, by deriving the answer from the code instead of snapshotting today's
wrongness. Verified by reintroducing each class into a scratch tree: baseline
exit 0, phantom `Raises:` exit 1, bad `Args:` label exit 1, re-indented
`Returns:` exit 1. The two `Raises:` assertions are complementary rather than
redundant, and `_read_http_none` proves it -- `any(header.flags)` in its body
makes the conservative reachability check judge it possibly-raising, so only the
commented-out-raise check catches it. Measured: reachability exit 0, commented
raise exit 1.

The SACK change is the one with a real behavioural fails-before. Lengths 3, 11,
14, 17, 19 and 25 all parsed clean on `c8fd97bcd` with the SACK option present
in `tcp.info['options']`, and all six now raise `ProtocolError: TCP: [OptNo 5]
invalid format`.

Two things deliberately not done. All 12 functions carrying both `**kwargs` and
an `Args:` section without documenting `**kwargs` are in hip.py -- 438 of 450
document it elsewhere -- and that set includes the two siblings this change
copies its wording from, so fixing 4 of the 12 is what would make hip.py
inconsistent. And the SACK check implements exactly the documented rule, not
RFC 2018's one-to-four block bound; `length=2` is degenerate and still parses,
recorded in the test as a decision.

Tests: 5 passed / 10 subtests (contract), 3 passed / 10 subtests (SACK),
25 passed / 19 subtests (tier guard), 81 passed / 73 subtests (transport and
schema units, unchanged by the new check).

An independent second scanner, written from scratch against the same baseline,
reproduced all three counts (6 / 13 / 3) and the residuals (0 / 9 / 1), and
turned up three things now recorded in the test module:

- `_documented_names` justified its relative-indent measurement by saying
  `__doc__` is dedented at compile time. True of `__doc__` and irrelevant here,
  since this module reads `ast.get_docstring(..., clean=False)`, which preserves
  raw source indentation -- measured [0, 8, 12, 12] against [0, 0, 4, 4] for the
  dedented forms. The implementation was right for a different reason (nesting
  depth moves the absolute column); the rationale is corrected rather than left
  wrong in a checker for wrong rationales.
- `_raised_names` resolves only `Name` and `Attribute` raise targets, and
  `ast.walk` attributes a nested `def`'s raise to the enclosing function. Both
  can only miss a phantom, never fail a correct docstring, and both are now
  documented with their measurements: 856 `Name` + 2 `Attribute` + 0 `Subscript`
  across 858 raise targets, and 11 documented functions with a nested `def`, 3
  raising inside it. The second is the correct answer rather than a gap --
  `_read_param_locator_set` documents `ProtocolError` and raises none itself,
  but calls a `_read_locator` helper that does.
- One reported defect was a false positive and is recorded as a trap:
  `Raw.__post_init__` documents `error` and `alias`, has neither in its
  signature, and assigns a local `alias` -- but forwards `**kwargs` to `read`,
  which declares both as keyword-only and uses them. Reporting it would ask for
  correct documentation to be deleted.
@JarryShaw JarryShaw added the fix Pull requests that fix a defect (fix: subject prefix) label Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull requests that fix a defect (fix: subject prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IPX vendor crawler is doubly broken: Wikipedia 403s the default User-Agent, and the socket-number table is gone

1 participant