Skip to content

Fix channel-name trim parity between frontend and server - #112

Merged
dborup merged 3 commits into
masterfrom
codex/fix-channel-name-trim-parity
Sep 28, 2026
Merged

dborup merged 3 commits into
masterfrom
codex/fix-channel-name-trim-parity

Conversation

@dborup

@dborup dborup commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Summary

Small follow-up to dborup/CoreScope#99 (shared channels): the frontend's
channel-name trimming in normalizeName() (public/channel-proposals.js)
did not match the server's trimming in NormalizeName()
(internal/channelregistry/name.go).

Low priority, no security impact — the server is authoritative and already
rejects everything it should. This only fixes the frontend's pre-submit
validation/preview so it agrees with what the server will actually decide.

The two mismatches

normalizeName() called String(raw).trim() before the invisible-format
check. JS's native trim() and Go's strings.TrimSpace (unicode.IsSpace)
disagree on two codepoints:

  1. U+FEFF (BOM) — JS's trim() strips it, Go's TrimSpace does not.

    • Before: input "<BOM>test" (a leading U+FEFF) → frontend trims the BOM away, shows/submits
      "#test" → server's NormalizeName sees the BOM still there (not
      whitespace to Go), rejects with ErrNameInvisible (400). The user sees
      a rejection for a name that looked fine and accepted in the UI.
    • After: the frontend also sees the BOM, matches the INVISIBLE_RE check,
      and rejects up front with "The channel name contains invisible
      formatting characters."
  2. U+0085 (NEL) — Go's TrimSpace strips it (unicode.IsSpace(0x85) == true), JS's trim() does not.

    • Before: input "test\u0085" → frontend does not trim the NEL, it falls
      in CONTROL_RE's range and gets rejected as "contains control
      characters" → server's NormalizeName trims it away and would have
      accepted "#test".
    • After: the frontend trims it too and accepts "#test", matching the
      server.

An input right after # in the middle of a name (e.g. #mid<BOM>dle, a
mid-string U+FEFF) was already rejected before this change and still is —
only the leading/trailing codepoints handled by trim() were affected.

Fix

normalizeName() now calls a small local trimGoSpace() helper instead of
.trim(). It trims exactly the codepoint set Go's unicode.IsSpace covers,
enumerated from the Go standard library (not guessed) and pinned as
GO_SPACE_CODEPOINTS:

U+0009–U+000D, U+0020, U+0085, U+00A0, U+1680, U+2000–U+200A,
U+2028, U+2029, U+202F, U+205F, U+3000

Parity proof

  • internal/channelregistry/name_test.go adds TestGoSpaceCodepointsParity,
    which enumerates all runes 0..0x10FFFF with unicode.IsSpace and compares
    the result against the same fixed 25-codepoint list used in the JS test
    (GO_SPACE_CODEPOINTS in test-channel-proposals.js, also exported from
    public/channel-proposals.js as CP.GO_SPACE_CODEPOINTS). A drift on
    either side — a Go stdlib White_Space table update, or an edit to one
    list but not the other — fails a test.
  • Both TestNormalizeNameTrimsU0085LikeGoTrimSpace (Go) and the matching JS
    tests lock in the U+0085 trim-and-accept behavior; TestNormalizeNameRejectsLeadingTrailingFEFF
    (Go) and the matching JS tests lock in the U+FEFF reject-as-invisible
    behavior.

No server/API changes — internal/channelregistry/name.go is untouched.

Tests run (counts from actual output, not estimated)

  • node test-channel-proposals.js → 32 passed, 0 failed (28 pre-existing
    • 4 new; confirmed red against the pre-fix frontend first: 4 failed before
      the fix, 0 after)
  • cd internal/channelregistry && go test -count=1 -v ./... → 16 passed,
    0 failed
    (13 pre-existing + 3 new)
  • cd cmd/server && go test -count=1 -v ./... → 1976 passed, 0 failed
    (confirms the read-only server package is unaffected)
  • bash scripts/check-xss-sinks.sh --diff origin/master → clean, no flagged
    sinks

What changed

  • public/channel-proposals.js — trimGoSpace() helper + GO_SPACE_CODEPOINTS,
    used in normalizeName() in place of .trim()
  • test-channel-proposals.js — new coverage for both mismatches and the
    parity list
  • internal/channelregistry/name_test.go — TestGoSpaceCodepointsParity and
    matching Go-side regression tests

No npm dependencies, no workflow changes, no new map[string]interface{}.


🤖 Generated with Claude Code

https://claude.ai/code/session_01Vxw6Ez4BQJo7w99BQqxnji


Generated by Claude Code

dborup and others added 3 commits September 28, 2026 14:19
…ow-up)

normalizeName() in public/channel-proposals.js trims with JS's native
String.prototype.trim(), which disagrees with Go's strings.TrimSpace
(used by internal/channelregistry.NormalizeName) on two codepoints:
U+FEFF (trimmed by JS, not by Go) and U+0085 NEL (trimmed by Go, not
by JS). Add failing-first coverage for both mismatches, plus a Go test
that pins the full unicode.IsSpace rune enumeration so a drift in
either the JS mirror or the Go stdlib's White_Space table is caught.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vxw6Ez4BQJo7w99BQqxnji
normalizeName() called String(raw).trim() before the invisible-format
check, so JS's native trim rules decided what counted as leading/
trailing whitespace. That set differs from Go's unicode.IsSpace (what
strings.TrimSpace, and therefore internal/channelregistry.NormalizeName,
use):

  - U+FEFF (BOM): JS's trim() strips it, Go's does not. A name typed as
    "mychannel" showed and submitted as "#mychannel" in the UI,
    then the server rejected it with ErrNameInvisible (400) — a
    confusing rejection for a name the user never saw accepted.
  - U+0085 (NEL): Go's TrimSpace strips it, JS's trim() does not. A
    trailing NEL made the frontend reject the name as a control
    character, while the server would have trimmed it and accepted
    "#mychannel".

Replace the .trim() call with trimGoSpace(), which trims exactly the
codepoint set enumerated from Go's unicode.IsSpace (pinned in
GO_SPACE_CODEPOINTS and cross-checked against a matching Go test in
internal/channelregistry/name_test.go). The server's NormalizeName is
unchanged and remains authoritative; this only aligns the frontend's
pre-submit validation/preview with what the server will actually do.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vxw6Ez4BQJo7w99BQqxnji
- test-channel-proposals.js: fix a section heading and a comment that had
  literal ─/— escape text instead of the real ── and — characters
  used elsewhere in the file.
- name_test.go (TestGoSpaceCodepointsParity) and the GO_SPACE_CODEPOINTS
  comment in channel-proposals.js: correct an inaccurate claim that a drift
  in the JS list would fail the Go test. The Go test only catches a change
  in Go's own unicode.IsSpace/White_Space table; the JS test only catches
  drift in its own copy. The two lists are kept in sync by hand.

No logic changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vxw6Ez4BQJo7w99BQqxnji

dborup commented Sep 28, 2026

Copy link
Copy Markdown
Owner Author

Review feedback addressed (commit ab31ed5d)

  1. test-channel-proposals.js: the section heading and one comment had literal ─/— escape text instead of the actual ── and — characters used by the other section headings in the file — fixed to use the real characters.
  2. internal/channelregistry/name_test.go (TestGoSpaceCodepointsParity) and the GO_SPACE_CODEPOINTS comment in public/channel-proposals.js: corrected an inaccurate claim that a drift in the JS list would fail the Go test. The Go test only catches a change in Go's own unicode.IsSpace/White_Space table against its own pinned want slice; the JS test only catches drift in its own copy of the list. The two lists are kept in sync by hand — both comments now say so explicitly.

No logic changes. Re-ran both suites after the fix: node test-channel-proposals.js → 32 passed, 0 failed; cd internal/channelregistry && go test -count=1 ./... → 16 passed, 0 failed.


Generated by Claude Code

@dborup
dborup marked this pull request as ready for review September 28, 2026 15:12
@dborup
dborup merged commit add434b into master Sep 28, 2026
6 checks passed
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.

1 participant