Commit graph

5 commits

Author SHA1 Message Date
Stephen Dennis
8808b4c375 feat(#2136): flip fargs to const UTF8 * const — and convert every site the compiler surfaced
The flip: FUNCTION/XFUNCTION/FUN::fun/delim_check and the module
interfaces take `const UTF8 * const fargs[]`.  Double-const is
load-bearing: C++ qualification conversion needs const at both pointer
levels, so builder-side `UTF8 *[]` arrays convert implicitly — the
evaluator, the JIT marshaller, and every owner site need zero casts,
and slot reassignment inside bodies becomes a compile error for free.

The conversions: the flip landed first so the compiler enumerated every
violation; this commit is that inventory worked to zero — ~250 sites
across funceval, funceval2, functions, funmath, help, mail, session,
powers, levels, predicates, conf, walkdb, stringutil, timeutil/
date_scan (regenerated, one-line diff), exp3, and mux_main, each
classified per docs/campaign-2136-const-fargs.md's four recipes.

New idioms (functions.h): trim_space_sep_n() — non-destructive trim for
(pointer, length) consumers, so trim-then-scan sites need no copy at
all; FargVec — the argv counterpart of FargCopy for CS_ARGV handlers.
countwords() and DecodeListOfIntegers() rewritten non-destructive.

The flip deleted more than it added: #2157's fun_munge list1 copy, the
engine_com help-topic copy, fun_index's in-place NUL write, and five
const_casts (process_sex x4, sha1_helper).  const_cast budget: zero
added.

Trap recorded in the brief: an old-signature definition doesn't fail
the build — it becomes a C++ overload, and the new-signature symbol
stays undefined until dlopen(RTLD_NOW).  delim_check, the conn_bridge
bridges, the dbt_spike stub, and exp3::Call were all silently shadowed;
muxscript was the only host that noticed, because netmux's own net.cpp
resolved the flat-namespace lookup.  After any signature flip, grep the
old spelling.

Verified: make test EXPECT_CONFIG="jit=yes" (35 passed / 0 failed) and
make test-scenario, including the new tests/scenario/sidefx_fargs.py
that live-probes the class-3 wrappers smoke never touches (pemit/
trigger/link/tel/wipe/destroy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 14:34:23 -06:00
Stephen Dennis
571eef854b build(ragel): strip #line directives so the outputs are canonical (#1950)
Ragel's #line values depend on the Ragel build rather than on our source,
so regenerating with a different Ragel rewrites dozens of #line-only lines
and leaves the tree dirty after any pull that touches a .rl. That is not
cosmetic: dounix.sh packages from the working tree, not from a git export,
so the churn ships -- into the tarball and into the generated .patch.gz.

Stripping the directives makes each output a function of its .rl alone.
Same input, same file, every box. The cost is that a debugger reports
positions in the generated .c rather than the .rl, which is the accepted
trade for an artifact that can be compared by hash.

Applied to all five Ragel outputs, not the four in docs/generated-files.md
-- date_scan.cpp is also Ragel-generated and had 102 directives.

  color_ops.c   194        art_scan.cpp   24
  date_scan.cpp 102        ast_scan.cpp   43
  muxescape.cpp  16        total         379

sed rather than sed -i: the -i spelling differs between GNU and BSD, and
this has to run on Linux, macOS and FreeBSD. Deleting whole lines is safe
here -- all 379 directives are anchored at column 0 and none carries
trailing code, both checked before the change.

Makefile.in is hand-edited to match rather than regenerated: this box has
automake 1.18.1 and the tree's Makefile.in came from 1.16.5, so
autoreconf would churn whole files for a five-line change (#1477). The
inserted lines are identical to the Makefile.am ones.

The pre-commit hook needed teaching, and this commit is the example: it
rejects a generated file staged without its source, but changing how a
file is generated rewrites the output while the .rl stays untouched. The
build rule now counts as a source. Verified all three ways -- a bare
generated file is still blocked, and staging it with either its .rl or its
Makefile.am is accepted.

Verified on macOS/arm64:
  - regeneration is idempotent: all five byte-identical across two runs
  - the generated diffs are 379 deletions, every one a #line, nothing added
  - outputs return to mode 444, no .tmp files left behind
  - make test 35 passed / 1 skipped / 0 failed
    (jit=yes stubslave=no nls=yes realitylvls=yes wodrealms=yes)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:21:47 -06:00
Stephen Dennis
86c6374673 fix(net,build): close #1141 restart fail-open; make date_scan.cpp reproducible
#1141 — load_restart_db is the @restart twin of the #1135 accept-path
fail-open.  When getpeername() on an adopted fd fails it zeroed d->address
and kept the descriptor.  A zeroed sockaddr has an sa_family that
mux_subnet::compare_to answers kGreaterThan for (netaddr.cpp:498-499), so
it matches no rule and every later g_access_list query fails open for that
descriptor — isForbid, the graduated site thresholds, isRegistered, and all
same_source_key rate/pre-auth accounting.  Unlike the accept path this is a
surviving authenticated session, so the exemption lasts the whole
connection rather than being a transient RST-race artifact.

Fail closed like #1135 did: drop the descriptor instead of carrying an
unpoliceable one.

Two details the issue did not cover:

  - The bail cannot happen at the getpeername call.  restart.db is a
    sequential stream and the record is only partly consumed there, so
    returning early would desynchronize every descriptor after it.  Record
    the failure and drop after the record is fully parsed.

  - The fd must be closed here.  ganl_initialize()'s adopt loop walks
    g_descriptors_list (ganl_adapter.cpp:1717), so a DESC that is never
    inserted is never adopted and its fd would leak.

No stale-CONNECTED hazard: sqlite_load_game clears CONNECTED for every
player at load (db.cpp:3813-3816) and load_restart_db only re-sets it for
descriptors it inserts, so a dropped session simply stays disconnected.

Logged undamped, unlike the accept-path refusal: this population is bounded
by who was connected across the restart and cannot be driven by a remote
flood, so each dropped session is worth a line.

build — date_scan.cpp's Ragel rule was added directly to the *generated*
mux/lib/Makefile.in (4884ba3db) and never existed in Makefile.am, so the
next automake/autoreconf would have silently dropped it; after that,
editing date_scan.rl would no longer regenerate date_scan.cpp and the stale
committed output would be compiled instead.  Add the rule to Makefile.am,
add date_scan.cpp to BUILT_SOURCES, and hand-sync the one BUILT_SOURCES
line in Makefile.in so the next autoreconf is a no-op here.

The committed date_scan.cpp had also been generated by hand from the repo
root (#line "mux/lib/date_scan.rl") rather than by its own rule, which runs
in mux/lib and emits #line "date_scan.rl" — matching art_scan.cpp,
ast_scan.cpp and color_ops.c.  Any build that retriggered the rule dirtied
the tree with a 146-line phantom diff.  Commit the rule-produced output;
the change is #line paths only, no generated code differs.

Verified: build clean, smoke 1404/1404, 0 crashes; date_scan.cpp no longer
re-churns across a full make install + make test cycle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 08:08:19 -06:00
Stephen Dennis
766f66f3cd date_scan: reject out-of-range years instead of wrapping them silently
Two linked defects let an oversized year slip through ParseDate() as a
plausible-but-wrong date:

- #707: the `digit+` tokenizer accumulated into a 32-bit int with no
  bound, so a 10+ digit run (e.g. "9999999999") overflowed -- undefined
  behavior that in practice wrapped to a small/negative value and bypassed
  every downstream range check. Accumulate in int64 and saturate to
  INT_MAX instead (and cap nDigits, which is stored in a byte).

- #708: ParseDate() did `ft.iYear = (short)dr.iYear` with no range check,
  so a year outside INT16 -- or merely outside timeutil's accepted
  [-27256, 30826] -- narrowed silently. "67536-01-01" became year 2000 and
  was accepted as the wrong date. Validate dr.iYear against the timeutil
  range before the cast; this guards every conversion branch, including the
  week-date and ordinal paths that bypass FieldedTimeToLinearTime's own
  year check.

date_scan.cpp is regenerated with ragel 6.10. The previously-committed
output came from a now-retired host's ragel, so the regeneration also
shifts the self-referencing #line directives (cosmetic; the local ragel's
numbering is the accurate one).

parsedate_fn.mux gains TC031 (10-digit overflow rejected), TC032 (year
narrowing rejected), and TC033 (boundary year 30826 still parses).

Smoke: 1064/0.

Closes #707
Closes #708

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:30:07 -05:00
Stephen Dennis
4884ba3dba Replace ParseDate with unified Ragel -G2 scanner + recursive descent parser
Delete the 1650-line multi-pass constraint-satisfaction engine
(timeparser.cpp) and replace it with date_scan.rl: a Ragel -G2
scanner that tokenizes date strings into a flat token array, plus a
small recursive descent parser that dispatches on the leading token.

The scanner handles month names, day-of-week names, timezone
abbreviations, military timezone letters, meridian (AM/PM), ordinal
suffixes, and numbers — all as DFA alternations with no hash tables
or linear searches.

The parser dispatches on the first non-whitespace token:
  4+ digit number → ISO 8601 family (extended, basic, ordinal, week)
  Month name      → US order (Mmm DD [YYYY] HH:MM:SS / legacy)
  1-2 digit + Month → European order (DD Mmm YYYY HH:MM:SS)
  Day-of-week     → strip prefix, re-dispatch

This is a single code path — always exercised, always tested.  The
old architecture had two independent parsers (do_convtime + ParseDate)
where one could be broken without anyone noticing.

Sub-second precision: unified handling across all formats, accumulating
up to 7 fractional digits as hectonanoseconds (100ns), matching the
internal FACTOR_100NS_PER_SECOND resolution.

All 998 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 00:24:10 -06:00