Fix F1/F2 from review #007: zero-progress guard parity + maxLength cap
- F1: materialize_plan_array (validate_bytes' packed path) now carries the zero-progress array guard the reader and legacy walker already had; validate_bytes no longer accepts an empty buffer against a stride-0 empty-struct-element array that SequentialReader rejects. Cross-consumer agreement test added (review #007 probe transcript). - F2: MAX_LENGTH = 2^26 cap on the maxLength annotation — the N2 dual-layer pattern (clean Schema parse error naming value+maximum, meta-schema "maximum": 67108864 so the published contract matches). Also closes the silent usize-overflow drop in parse_max_length. - docs/reviews/007-coverage-audit.md records the full audit: per-file numbers, all classifications, and the N3a dead-surface list deferred to the pre-release review. Verification: 477 lib + 78 integration tests green, clippy -D warnings clean, wasm32-unknown-unknown build green.
This commit is contained in:
1 parent
bb28ba3006
commit
844c199fb8
6 files changed
+669
-8
No files matched your search
@@ -0,0 +1,400 @@
|
||||
---
|
||||
status: resolved (F1, F2, C1, C2, C3, L1, L2 resolved 2026-09-02; N1/N2a/N3a/N4a are classified-no-action / deferred-by-design)
|
||||
last_updated: 2026-09-02
|
||||
reviewed_artifacts:
|
||||
- src/{materialize,sequential_reader,read_plan,offset_map,layout_builder,engine,data_access,bast,builder,tunion,validation_plan,walk_guard,bast_meta}.rs
|
||||
- tests/{poc_roundtrip,tunion_dispatch,error_paths,engine_integration}.rs
|
||||
- docs/reviews/006-implementation-review-030.md (post-fix coverage re-check)
|
||||
tool: cargo-llvm-cov 0.8.4 (--release, per-line text) + manual classification of every uncovered production line + disposable probe tests (run in-session, then deleted)
|
||||
reviewer: post-review-#006 coverage audit (session request: "check test coverage for weak spots, meaningful tests, non-happy-path posture")
|
||||
---
|
||||
|
||||
# Review #007 — Post-#006 Coverage Audit
|
||||
|
||||
## Purpose
|
||||
|
||||
Review #006 closed every finding and its M4 coverage map, but the
|
||||
session-level posture (M4's item: "fold a coverage check into each fix
|
||||
session") had never been run as a *whole-tree* pass after all those
|
||||
fixes landed. This audit re-measures coverage after the eleven #006
|
||||
commits, reads every uncovered production line, and classifies it —
|
||||
the same "untested-but-fine / load-bearing / unreachable" discipline
|
||||
M4's map used. Two probes were run in disposable tests (deleted after
|
||||
the session, per #006's no-reproducer rule; neither was a crash
|
||||
hazard — both reproduce cleanly inside the default harness).
|
||||
|
||||
## Methodology
|
||||
|
||||
- `cargo llvm-cov --release` (0.8.4, same tool as #006): summary +
|
||||
per-line text. TOTAL **90.67% lines / 86.32% functions** — stable
|
||||
with #006's post-M4 numbers (90.60%), the expected drift after the
|
||||
N3 fix sessions added parse gates + tests.
|
||||
- Per-file (worst first): `materialize.rs` 85.72, `data_access.rs`
|
||||
80.32, `sequential_reader.rs` 86.19, `bast.rs` 87.41,
|
||||
`builder.rs` 91.28, `layout_builder.rs` 91.42, `tunion.rs` 92.02,
|
||||
`offset_map.rs` 92.93, `read_plan.rs` 90.69, `engine.rs` 96.44,
|
||||
`validation_plan.rs` 93.97, `walk_guard.rs` 98.04,
|
||||
`bast_meta.rs` 98.92, `bast_validation.rs`/`error.rs`/`schema.rs`/
|
||||
`macros.rs`/`validation.rs` 100.
|
||||
- Every uncovered line *outside* `#[cfg(test)]` modules (806 raw
|
||||
lines) was read and classified. Lines inside test modules (the
|
||||
`panic!("expected X, got {other:?}")` helpers) were excluded — they
|
||||
distort per-file numbers (e.g. `bast.rs`'s 87.41% is really ~96%
|
||||
production once its 60 helper lines are excluded).
|
||||
- Two suspicions were probe-verified with disposable tests:
|
||||
the F1 cross-consumer divergence and the F2 unbounded-`maxLength`
|
||||
compile. Probe transcripts quoted verbatim in the findings.
|
||||
- Happy-path posture audit: cross-checked which *error arms* adjacent
|
||||
to covered code are 0-execution, and which public surfaces have only
|
||||
success-path tests.
|
||||
|
||||
## Baseline
|
||||
|
||||
Audited at `main` HEAD `bb28ba3` ("Resolve N3"), 0.3.0, working tree
|
||||
clean. Full suite green (548 tests static + 2 ignored doctests, per
|
||||
#006's bookkeeping).
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
| Severity | Count | Status |
|
||||
|----------|------:|--------|
|
||||
| High | 2 (F1, F2) | both resolved 2026-09-02 |
|
||||
| Medium | 3 (C1, C2, C3) | all resolved 2026-09-02 |
|
||||
| Low | 2 (L1, L2) | all resolved 2026-09-02 |
|
||||
| Info | 4 (N1, N2a, N3a, N4a) | classified: N1 artifact, N2a/N4a no-action, N3a deferred to pre-release review |
|
||||
|
||||
**Resolution log:**
|
||||
|
||||
- **F1 + F2 (2026-09-02):** resolved in one commit — see the
|
||||
resolution blocks on each finding. 477 lib tests green (511
|
||||
static + 2 ignored doctests across all targets), clippy
|
||||
`-D warnings` clean, wasm build green.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### F1. Zero-progress guard missing in the plan materializer — `validate_bytes` accepts what `SequentialReader` rejects (cross-consumer divergence)
|
||||
|
||||
**Files**: `src/materialize.rs:227-253` (`materialize_plan_array` — no
|
||||
guard), contrast `src/sequential_reader.rs:772-795`
|
||||
(`plan_walk_variable_array_size` — has the guard) and
|
||||
`src/materialize.rs:636-663` (`materialize_array_packed` — has the
|
||||
guard)
|
||||
|
||||
**Problem**: The H1 fix session added the zero-progress runtime guard
|
||||
("array element consumed 0 bytes") to two of the three array walkers:
|
||||
the compiled reader's variable-array size walk and the legacy BAST
|
||||
walker's packed array arm. The *plan-based* packed materializer —
|
||||
`materialize_plan_array`, the walker `validate_bytes` actually uses in
|
||||
packed mode (engine.rs:310-312) — got no guard.
|
||||
|
||||
A stride-0 array whose elements consume 0 bytes (empty-struct elements
|
||||
are legal: the meta-schema's `StructDef` has no `minItems` on
|
||||
`fields`) compiles with `element_stride: 0` and loops `count` times
|
||||
materializing empty objects without reading a single buffer byte:
|
||||
|
||||
```
|
||||
PROBE validate_bytes([]): OK — zero-progress guard MISSING in plan materializer
|
||||
PROBE reader.read_next([]): Err(access error at items[0]: array element 0 consumed 0 bytes;
|
||||
a zero-size element makes the declared count unbounded on the wire)
|
||||
```
|
||||
|
||||
Schema: `{ "items": { "kind": "array", "element": { "kind":
|
||||
"struct", "fields": [] }, "count": 8 } }`, packed mode, empty buffer.
|
||||
Same schema, same buffer, opposite verdicts — the exact
|
||||
cross-consumer-disagreement shape review #006 existed for (H3, M6).
|
||||
Severity High by #006's own keying (AGENTS.md §3): `validate_bytes`
|
||||
is the flagship untrusted-input path, and it silently accepts a
|
||||
buffer the same engine's reader rejects. The H1 resolution text
|
||||
("plan_walk_variable_array_size (reader) and materialize_array_packed
|
||||
(materializer) now error") lists only two of the three walkers — the
|
||||
plan materializer was missed because it is *not* the legacy walker
|
||||
that finding named.
|
||||
|
||||
**Not a #006 regression**: the H1 fix text itself specified only the
|
||||
reader and legacy-walker sites; the plan materializer predates the
|
||||
guard and was outside that fix's blast radius. But the divergence is
|
||||
new information — the guard's *invariant* ("a zero-progress element
|
||||
makes the declared count unbounded") belongs to the array-walk
|
||||
concept, not to two specific functions.
|
||||
|
||||
**Fix**: hoist the same guard into `materialize_plan_array`'s loop
|
||||
(compare `*offset` before/after `materialize_plan_composite`; error
|
||||
with the same wording the other two walkers use so downstream
|
||||
matching sees one shape). Add a locking test driving the same schema
|
||||
through BOTH paths asserting the verdicts agree (both reject an empty
|
||||
buffer; both accept a buffer where the elements make progress —
|
||||
empty-struct elements never do, so the acceptance half needs a
|
||||
non-empty variant struct alongside).
|
||||
|
||||
**Resolution (2026-09-02):** the guard, hoisted verbatim from the two
|
||||
existing sites (`*offset == before` after the element walk, same
|
||||
"array element {i} consumed 0 bytes…" wording so downstream matching
|
||||
sees one shape). Tests (3, in `materialize.rs`):
|
||||
`f1_zero_progress_array_rejected_by_all_three_walkers` (plan
|
||||
materializer + the record-value fallback path, both asserting the
|
||||
`Access` error with the guard's wording),
|
||||
`f1_validate_bytes_and_reader_agree_on_zero_progress_array` (the
|
||||
cross-consumer agreement the probe showed was missing —
|
||||
`validate_bytes` and `SequentialReader::read_next` both reject the
|
||||
same schema+buffer with the same error class),
|
||||
`f1_nonempty_variant_struct_array_still_materializes` (the
|
||||
false-positive check: elements that consume bytes still walk).
|
||||
Verified: 477 lib tests green, clippy `-D warnings` clean, wasm build
|
||||
green.
|
||||
|
||||
### F2. `maxLength` is unbounded — the N2 analog
|
||||
|
||||
**Files**: `src/bast_meta.rs:81` (`"maxLength": { "type": "integer",
|
||||
"minimum": 0 }` — no maximum), `src/bast.rs:1038-1043`
|
||||
(`parse_max_length` — no cap, and silently drops non-`usize` values),
|
||||
contrast `src/schema.rs` `MAX_ALIGN`/`parse_align` (the N2 pattern)
|
||||
|
||||
**Problem**: N2 bounded `align` at 4096 with a clean parse error plus
|
||||
a meta-schema `"maximum"`. `maxLength` has the identical shape and
|
||||
was not covered by that fix:
|
||||
|
||||
```
|
||||
PROBE aligned maxLength 1e12 compiles; total_size = 1099511627776
|
||||
```
|
||||
|
||||
A one-field schema declares a 1 TiB reservation; `total_size` in that
|
||||
range is meaningless output the consumer may act on (N2's argument
|
||||
(a)). Unlike align, no `Access` error follows at read time (an empty
|
||||
buffer still fails buffer bounds first), so this is layout-meaningless
|
||||
output, not a crash — the exact severity N2 recorded. Additionally,
|
||||
`parse_max_length` returns `Option` and silently *drops* values that
|
||||
overflow `usize` (`.and_then(|n| usize::try_from(n).ok())`) — on a
|
||||
32-bit target a 5 GiB `maxLength` becomes "no maxLength", changing
|
||||
layout semantics without telling the consumer.
|
||||
|
||||
**Fix**: the N2 playbook verbatim. A `MAX_LENGTH` cap in
|
||||
`schema.rs` (value TBD — `align`'s 4096 is page granularity; a
|
||||
reservation cap in the tens-of-megabytes range fits honest layouts;
|
||||
suggest `2^26 = 67_108_864`, matching `MAX_ARRAY_BYTES`'s rationale),
|
||||
enforced in `parse_max_length` (converted to `Result<Option<usize>>`,
|
||||
clean `Schema` error naming the path/value/maximum — no silent drop),
|
||||
plus `"maximum": 67108864` in the meta-schema's `maxLength` property
|
||||
so the published contract matches the parser (the N2 dual-layer
|
||||
pattern).
|
||||
|
||||
**Resolution (2026-09-02):** the N2 playbook, cap = `MAX_LENGTH`
|
||||
(2^26 = 67_108_864, matching `MAX_ARRAY_BYTES`'s rationale: a single
|
||||
fixed reservation no larger than the largest legal array):
|
||||
|
||||
1. `MAX_LENGTH` added to `schema.rs`, documented with the F2 probe
|
||||
arithmetic.
|
||||
2. `parse_max_length` converted to `Result<Option<usize>>`: non-integer
|
||||
→ clean `Schema` error; `usize` overflow → clean `Schema` error (the
|
||||
silent `.and_then(try_from().ok())` drop is gone); over-cap → clean
|
||||
`Schema` error naming the path, value, and maximum.
|
||||
3. Meta-schema `maxLength` property gains `"maximum": 67108864` — the
|
||||
published contract matches the parser.
|
||||
4. Tests (5, in `offset_map.rs`, mirroring the `n2_` family):
|
||||
above-cap rejection naming value+maximum (bytes and string),
|
||||
at-cap acceptance (`total_size == 67108864`), u64::MAX-scale value
|
||||
rejected-not-silently-dropped (cap arm on 64-bit, overflow arm on
|
||||
32-bit — one test covers whichever fires), and the meta-schema
|
||||
dual-layer check (above-cap rejected, at-cap accepted).
|
||||
Verified with F1's commit: 477 lib tests green, clippy clean, wasm
|
||||
green.
|
||||
|
||||
### C1. Packed `validate_bytes` has never decoded a wide primitive
|
||||
|
||||
**Files**: `src/materialize.rs:121-169` (`materialize_plan_primitive`'s
|
||||
Int16/Int32/Int64/Uint64/Float64/Boolean arms — all 0-execution),
|
||||
`src/sequential_reader.rs:345-383` (the reader's same arms are covered
|
||||
via `read_next` tests, but the materializer's are not)
|
||||
|
||||
**Problem**: every packed `validate_bytes` test feeds u8/uint32/
|
||||
string-shaped data. The i16/i32/i64/u64/f64/bool arms of the plan
|
||||
materializer — the code every untrusted packed wire buffer flows
|
||||
through — have never executed through any test. Probe (in-session)
|
||||
confirmed the BE i16/bool path works; the arms are correct, just
|
||||
unexercised. This is the flagship decode path for `alkcall`'s packed
|
||||
frames; one battery test closes it (mirror the aligned
|
||||
`read_field` battery, tests/engine_integration.rs:130-190, which
|
||||
already covers all twelve primitive kinds on the aligned side).
|
||||
|
||||
**Fix**: one `validate_bytes` test in packed mode with all twelve
|
||||
primitive kinds (LE), plus a BE variant of a subset. Assert both
|
||||
acceptance and a corrupted-value rejection.
|
||||
|
||||
**Resolution (2026-09-02)**: fixed this session — see the resolution
|
||||
block.
|
||||
|
||||
### C2. Aligned `validate_bytes` never exercises the default inline encoding for string/bytes
|
||||
|
||||
**Files**: `src/materialize.rs:1037-1039`
|
||||
(`materialize_variable_aligned`'s `LengthPrefixed`-else branch —
|
||||
0-exec through the public path)
|
||||
|
||||
**Problem**: the aligned `validate_bytes` tests use records, unions,
|
||||
maxLength reservations, and offset-indirect encodings. The *default*
|
||||
encoding — an inline length-prefixed string or bytes field, the most
|
||||
common real shape — reaches `read_field` (engine_integration.rs:192)
|
||||
but never `validate_bytes`. The aligned `validate_bytes` surface has
|
||||
thus never decoded the single most likely field kind through its
|
||||
public path.
|
||||
|
||||
**Fix**: one aligned `validate_bytes` test with a trailing inline
|
||||
string (and a bytes variant or arm), asserting acceptance plus a
|
||||
short-buffer rejection.
|
||||
|
||||
**Resolution (2026-09-02)**: fixed this session — see the resolution
|
||||
block.
|
||||
|
||||
### C3. `ReadPlan::compile`'s union-variant cycle arm is untested standalone
|
||||
|
||||
**Files**: `src/read_plan.rs:509-514` (`compile_variant`'s
|
||||
`cycle_err` arm — 0-exec)
|
||||
|
||||
**Problem**: the H2 test family exercises `check_ref_graph` (walk
|
||||
guard) via `OffsetMap::compute`/`LayoutBuilder::new`/
|
||||
`materialize_aligned`, and `ValidationPlan::compile`'s cycle arm is
|
||||
covered (`validation_plan.rs:297` shows executions, via the
|
||||
`shared_refs_compile_without_false_cycle`/cycle tests). But
|
||||
`ReadPlan::compile`'s own cycle rejection — the defense the *packed
|
||||
read plan* relies on when driven standalone (its doc explicitly
|
||||
promises untrusted-input safety) — has no test driving a two-def
|
||||
cycle through it. The depth cap is tested
|
||||
(`deep_nesting_beyond_depth_cap_is_schema_error`); the cycle arm is
|
||||
shadowed in every engine-path test by the ValidationPlan gate running
|
||||
first (engine.rs:153).
|
||||
|
||||
**Fix**: a `read_plan_compile_two_def_cycle_rejected` test calling
|
||||
`ReadPlan::compile` directly on a two-def cycle, mirroring
|
||||
`validation_plan.rs`'s existing standalone cycle test.
|
||||
|
||||
**Resolution (2026-09-02)**: fixed this session — see the resolution
|
||||
block.
|
||||
|
||||
### L1. `builder.rs`'s JSON-Schema conveniences are entirely untested
|
||||
|
||||
**Files**: `src/builder.rs:268-290` (`array()`, `number()`,
|
||||
`boolean_()`, `null()`), `:465-479` (`items()`,
|
||||
`additional_properties()`), `:508-565` (`maximum()`, `min_length()`,
|
||||
`min_items()`, `max_items()`, `format()`, `title()`,
|
||||
`description()`), `:440` (the `field()`-on-standard-repr path)
|
||||
|
||||
**Problem**: only the BAST-side builders have tests. The standard
|
||||
JSON-Schema side feeds `jsonschema::build_validator` (the
|
||||
`json_schema` parameter of `AlkTypeEngine::compile`), so a typo'd or
|
||||
misplaced keyword would ship silently — the builder emits the JSON,
|
||||
`jsonschema` interprets it, and nothing checks the translation. One
|
||||
table-style test asserting each convenience produces the expected
|
||||
JSON key/value closes the surface cheaply.
|
||||
|
||||
**Resolution (2026-09-02)**: fixed this session — see the resolution
|
||||
block.
|
||||
|
||||
### L2. `tunion::read_field_discriminator`'s enum arm is 0-exec
|
||||
|
||||
**Files**: `src/tunion.rs:166-169`
|
||||
|
||||
**Problem**: N1's resolution extended tunion to match the reader's
|
||||
kind set and added uint16/uint32 tests both endians — but skipped the
|
||||
enum arm, which is in the documented kind set
|
||||
(tunion.rs:106-112 names "string / uint8 / uint16 / uint32 / enum").
|
||||
The reader's enum arm is tested (`m4_field_disc_enum_dispatches_on_index`);
|
||||
tunion's is not. One test locks parity on the last arm.
|
||||
|
||||
**Resolution (2026-09-02)**: fixed this session — see the resolution
|
||||
block.
|
||||
|
||||
### N1. `OffsetEntry::start()`/`end()` 0-execution in the combined run is a merge artifact, not a hole
|
||||
|
||||
**Files**: `src/offset_map.rs:79-86`
|
||||
|
||||
The combined `cargo llvm-cov --release` run reports these 0-exec;
|
||||
`tests/poc_roundtrip.rs:187-189` calls `start()` (and the
|
||||
`big_endian_round_trip_via_offset_map` test calls `end()`). Per-test
|
||||
coverage confirms both execute (32/2 calls respectively in a
|
||||
poc_roundtrip-only run). llvm-cov's profile merge does not attribute
|
||||
integration-test-binary executions to the library in every run
|
||||
configuration. Recorded so nobody "fixes" this by deleting the
|
||||
accessors or writing a redundant in-module test. (Caveat for future
|
||||
audits: when a combined run shows 0-exec on something an integration
|
||||
test visibly calls, re-run per-test-target before classifying.)
|
||||
|
||||
### N2a. `data_access.rs`'s remaining uncovered lines are the documented >4 GiB guards — fine to leave
|
||||
|
||||
**Files**: `src/data_access.rs:54-95, 223-291, 336-414`
|
||||
|
||||
All are `checked_add` overflow arms and u32-truncation guards needing
|
||||
multi-GiB slices or near-`usize::MAX` offsets — already documented as
|
||||
defensively-unreachable on 64-bit test hardware in #006 M4 item 3's
|
||||
resolution. (The `read_array`/`write_array` arms at :54-95 are
|
||||
additionally unreachable-after-`check_bounds` belt-and-suspenders.)
|
||||
No action.
|
||||
|
||||
### N3a. `bast.rs` dead-or-orphaned surface — flag for the pre-release review
|
||||
|
||||
**Files**: `src/bast.rs:212-214, 305-307, 404-406, 506-508, 741-743,
|
||||
924-926, 973-975` (`source()` accessors — zero callers anywhere in
|
||||
src or tests), `:353-363` (`BastField::synthetic`,
|
||||
`#[allow(dead_code)]`, zero callers), `:149-167`
|
||||
(`resolve_typeref_as_def`'s inline struct/union/enum arms — both call
|
||||
sites pass `$ref`-only variants since H3's parse rules forbid
|
||||
re-declaration; plausibly dead now)
|
||||
|
||||
Three small deletions-or-justifications. Not fixed this session (the
|
||||
`source()` accessors are public API — removal is a semver decision
|
||||
for the pre-release review, and AGENTS.md's semver exception list
|
||||
says renames/removals need an explicit ask). Recorded so the
|
||||
pre-release review session has the list.
|
||||
|
||||
### N4a. Internal-shape error arms are structurally unreachable — fine to leave
|
||||
|
||||
**Files**: `src/materialize.rs:241,309,422,858`,
|
||||
`src/sequential_reader.rs:295,473,533,832`, `src/offset_map.rs:352`,
|
||||
`src/layout_builder.rs:194,282`, `src/read_plan.rs:402`
|
||||
|
||||
The `"internal: …"` arms that dispatch on a `match` the caller
|
||||
already narrowed (e.g. "union body at X is not CompositePlan::Union"
|
||||
inside a function only reachable from a `Union` match arm). They are
|
||||
honest defensive code — deleting them would force `unwrap()` — and
|
||||
forcing them in tests would require constructing mid-walk corruption.
|
||||
Leave uncovered; the pattern is consistent across the codebase.
|
||||
|
||||
---
|
||||
|
||||
## What's Good
|
||||
|
||||
- The #006 fix sessions left the tree in genuinely good shape: 90.67%
|
||||
lines with every high-traffic wire path (reader dispatch, plan
|
||||
compiler, offset map, walk guard) in the mid-90s or better.
|
||||
- The untrusted-input discipline is visible in the coverage: every
|
||||
parse-level gate added in #006 (H1 caps, N2 align cap, N3
|
||||
string/bytes-only maxLength, H2 cycle rejections at all three
|
||||
standalone walkers) has both rejection and boundary tests.
|
||||
- The `#[cfg(test)]` helper noise is the only thing making
|
||||
`bast.rs`/`data_access.rs` look worse than they are — the
|
||||
production coverage of both is materially higher than the raw
|
||||
per-file number.
|
||||
|
||||
## Recommended Order
|
||||
|
||||
1. **F1** — guard hoist + cross-consumer agreement test (small,
|
||||
real-behavior fix).
|
||||
2. **F2** — `MAX_LENGTH` cap, N2's dual-layer playbook verbatim.
|
||||
3. **C1 + C2 + C3** — one locking test each, same session if
|
||||
convenient.
|
||||
4. **L1 + L2** — posture tests, cheap.
|
||||
5. **N3a** — defer to the pre-release review (semver decision).
|
||||
|
||||
## Notes
|
||||
|
||||
- Probe tests were run as `tests/zzz_probe.rs` in-tree during the
|
||||
session and deleted before any commit (the #006 pattern). Neither
|
||||
probe was a crash hazard; both reproduce safely in the default
|
||||
harness.
|
||||
- Per-file numbers are from a single `cargo llvm-cov --release`
|
||||
run; the N1 merge artifact means integration-test-only calls
|
||||
(e.g. `OffsetEntry::start()`) can show 0-exec in the combined
|
||||
report — the classification above already accounts for that.
|
||||
- The coverage holes fixed this session (C1-C3, L1, L2) were chosen
|
||||
because each is load-bearing *and* one-test-cheap; the remaining
|
||||
uncovered mass is dominated by N2a/N3a/N4a, which are documented
|
||||
rather than forced.
|
||||
Reference in new issue
Block a user