Skip to content

refactor(dpi/ssdp): case-fold header name without allocating#290

Merged
domcyrus merged 1 commit into
domcyrus:mainfrom
0xghost42:refactor/dpi-ssdp-case-fold
May 19, 2026
Merged

refactor(dpi/ssdp): case-fold header name without allocating#290
domcyrus merged 1 commit into
domcyrus:mainfrom
0xghost42:refactor/dpi-ssdp-case-fold

Conversation

@0xghost42
Copy link
Copy Markdown
Contributor

Summary

The ST/NT header lookup in analyze_ssdp converted each header line into a fresh lowercased String just to compare the 3-byte prefix \"st:\" / \"nt:\". For an SSDP packet with N header lines this allocates N times per packet, and only the prefix actually needs case-insensitive comparison.

Replace the per-line to_lowercase() allocation with line.get(..3).is_some_and(|p| p.eq_ignore_ascii_case(\"st:\") || p.eq_ignore_ascii_case(\"nt:\")). The value extraction via line.get(3..) is unchanged, since both \"st:\" and \"nt:\" are exactly 3 ASCII bytes.

Behavior is unchanged: the field-name check is still case-insensitive (HTTP/1.1 §3.2), and the value is still trimmed and required to be non-empty.

Why a regression test

The existing tests only exercise upper-case ST: / NT:, so dropping the case-fold would not have failed any test. The new test_ssdp_mixed_case_st_and_nt_headers exercises all four case mixes per field name (ST, St, sT, st and NT, Nt, nT, nt) so a future refactor that drops the case fold will fail this test instead of silently regressing — same shape as the regression test added in #278 for decode_netbios_name.

Verification

  • cargo test --lib: 360 passed, 0 failed (8 in network::dpi::ssdp, incl. the new test).
  • cargo clippy --all-targets -- -D warnings: clean.
  • cargo fmt --check: clean.

Notes

Real SSDP traffic from UPnP devices commonly uses mixed-case header names — the Microsoft Windows SSDP stack and the Linux miniupnpd implementation both differ from each other in capitalisation, and HTTP §3.2 explicitly makes the field-name case-insensitive.

The ST/NT header lookup converted each header line into a fresh
lowercased `String` (`line.to_lowercase()`) just to compare the
3-byte prefix "st:" / "nt:". For an SSDP message with N header
lines this allocates N times per packet, and only the prefix actually
needs case-insensitive comparison.

Replace the `to_lowercase` allocation with
`line.get(..3).is_some_and(|p| p.eq_ignore_ascii_case("st:") ||
p.eq_ignore_ascii_case("nt:"))`. The value extraction via
`line.get(3..)` is unchanged.

Add `test_ssdp_mixed_case_st_and_nt_headers` exercising all four
case mixes (`ST`/`St`/`sT`/`st` and the same for `NT`) to
lock the case-insensitivity invariant per HTTP/1.1 §3.2 field-name
rules — a future refactor that drops the case fold will fail this
test rather than silently regress.
@domcyrus
Copy link
Copy Markdown
Owner

@0xghost42 thanks a lot, this lgtm!

@domcyrus domcyrus merged commit 8ddb486 into domcyrus:main May 19, 2026
16 checks passed
domcyrus pushed a commit that referenced this pull request May 21, 2026
The header loop in `analyze_http` lowercased every header line via
`to_lowercase()` only to compare the result against the two ASCII
literals "host" and "user-agent". That allocates a fresh `String` per
header line per packet, on the DPI hot path.

HTTP/1.1 §3.2 makes field-names case-insensitive, so compare in place
with `str::eq_ignore_ascii_case` — no allocation, behavior preserved.

Add `test_http_mixed_case_host_and_user_agent_headers` covering four
case mixes per header name so a future refactor that drops the
case-insensitive compare fails this test instead of silently
regressing — same shape as the regression tests added in #290 (SSDP)
and #278 (NetBIOS).

Refs #300
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants