From 4951c966d4eda1b23c0c1e0a1d8abeb74bc14b71 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 27 Jul 2026 01:06:57 +0200 Subject: [PATCH] feat: publish Regex Tools 0.2.0 --- .gitignore | 2 +- .prettierignore | 1 + CHANGELOG.md | 53 + LICENSES/Emscripten-MIT.txt | 19 + LICENSES/PCRE2.txt | 104 ++ README.md | 241 +++- SOURCE.md | 18 +- THIRD_PARTY_NOTICES.md | 32 +- docs/ANALYSIS.md | 121 ++ docs/ARCHITECTURE.md | 103 +- docs/COMPARISON_AND_CODEGEN.md | 118 ++ docs/ENGINE_BUILDS.md | 86 +- docs/EXECUTION_TRACES.md | 44 +- docs/EXPLANATION_MODEL.md | 17 +- docs/FLAVOUR_SUPPORT.md | 67 +- docs/FORMATTING.md | 57 + docs/GENERATED_CASES.md | 135 ++ docs/MINIMIZATION.md | 71 + docs/PARSER_PROFILES.md | 7 +- docs/PCRE2_NEXT_SLICES.md | 66 + docs/PERFORMANCE.md | 68 +- docs/PORTAL_REQUIREMENTS.md | 16 +- docs/REFERENCE_IMPLEMENTATIONS.md | 4 +- docs/RELEASE.md | 7 +- docs/SECURITY.md | 56 +- engines/pcre2/CMakeLists.txt | 71 + engines/pcre2/README.md | 27 + engines/pcre2/pcre2_bridge.c | 910 ++++++++++++ engines/pcre2/pcre2_bridge.h | 236 ++++ eslint.config.mjs | 1 + package-lock.json | 7 +- package.json | 7 +- public/engines/README.md | 10 +- public/engines/pcre2/LICENSE.txt | 104 ++ public/engines/pcre2/SHA256SUMS | 4 + public/engines/pcre2/engine-metadata.json | 90 ++ public/engines/pcre2/pcre2.mjs | 2 + public/engines/pcre2/pcre2.wasm | Bin 0 -> 339494 bytes public/toolbox-app.json | 2 +- scripts/build-engines.mjs | 40 +- scripts/generate-toolbox-manifest.mjs | 13 +- scripts/install-pcre2-engine.mjs | 16 + scripts/package-release.mjs | 25 +- scripts/pcre2-codegen-golden.test.ts | 110 ++ scripts/pcre2-engine-pack.mjs | 1018 ++++++++++++++ scripts/pcre2-engine-pack.test.ts | 89 ++ scripts/serve-test.mjs | 1 + scripts/verify-engine-assets.mjs | 33 +- src/components/AnalysisPanel.css | 521 +++++++ src/components/AnalysisPanel.test.tsx | 248 ++++ src/components/AnalysisPanel.tsx | 1245 +++++++++++++++++ src/components/CapabilityPanel.test.tsx | 58 +- src/components/CapabilityPanel.tsx | 79 +- src/components/ComparisonCodePanel.css | 331 +++++ src/components/ComparisonCodePanel.test.tsx | 82 ++ src/components/ComparisonCodePanel.tsx | 884 ++++++++++++ src/components/CorpusPanel.test.tsx | 253 ++++ src/components/CorpusPanel.tsx | 1024 ++++++++++++++ src/components/GenerationPanel.css | 314 +++++ src/components/GenerationPanel.test.tsx | 326 +++++ src/components/GenerationPanel.tsx | 829 +++++++++++ src/components/HelpDialog.tsx | 138 +- src/components/MinimizePanel.css | 241 ++++ src/components/MinimizePanel.test.tsx | 184 +++ src/components/MinimizePanel.tsx | 848 +++++++++++ src/components/ModalDialog.tsx | 71 + src/components/PatternFormatterPanel.css | 342 +++++ src/components/PatternFormatterPanel.test.tsx | 245 ++++ src/components/PatternFormatterPanel.tsx | 617 ++++++++ src/components/Pcre2TracePanel.test.tsx | 59 + src/components/Pcre2TracePanel.tsx | 407 ++++++ src/components/ProjectPanel.tsx | 4 + src/components/QuickReference.test.tsx | 38 +- src/components/QuickReference.tsx | 162 ++- src/components/ReplacementPanel.tsx | 10 +- src/components/UnitTestPanel.test.tsx | 36 + src/components/UnitTestPanel.tsx | 112 +- src/components/Workbench.test.tsx | 56 + src/components/Workbench.tsx | 602 +++++++- src/project/project.test.ts | 182 ++- src/project/project.types.ts | 9 +- src/project/project.validation.ts | 144 +- src/regex/analysis/AnalysisSupervisor.test.ts | 161 +++ src/regex/analysis/AnalysisSupervisor.ts | 94 ++ src/regex/analysis/analysis-limits.ts | 36 + src/regex/analysis/analysis-runner.test.ts | 336 +++++ src/regex/analysis/analysis-runner.ts | 819 +++++++++++ src/regex/analysis/analysis.types.ts | 266 ++++ .../analysis/ecmascript-analysis.test.ts | 180 +++ src/regex/analysis/ecmascript-analysis.ts | 299 ++++ src/regex/analysis/static-risk.test.ts | 178 +++ src/regex/analysis/static-risk.ts | 573 ++++++++ src/regex/analysis/statistics.test.ts | 75 + src/regex/analysis/statistics.ts | 61 + src/regex/codegen/pcre2-c.test.ts | 116 ++ src/regex/codegen/pcre2-c.ts | 591 ++++++++ .../comparison/ComparisonOrchestrator.test.ts | 335 +++++ .../comparison/ComparisonOrchestrator.ts | 536 +++++++ src/regex/comparison/compare-results.ts | 666 +++++++++ src/regex/comparison/comparison.types.ts | 174 +++ src/regex/corpus/corpus-export.test.ts | 185 +++ src/regex/corpus/corpus-export.ts | 313 +++++ src/regex/corpus/corpus-input.test.ts | 122 ++ src/regex/corpus/corpus-input.ts | 255 ++++ src/regex/corpus/corpus-lines.test.ts | 25 + src/regex/corpus/corpus-lines.ts | 40 + src/regex/corpus/corpus-runner.test.ts | 406 ++++++ src/regex/corpus/corpus-runner.ts | 617 ++++++++ src/regex/corpus/corpus.types.ts | 66 + src/regex/execution/EngineSupervisor.test.ts | 168 +++ src/regex/execution/EngineSupervisor.ts | 67 +- src/regex/execution/Pcre2TraceSupervisor.ts | 51 + src/regex/execution/WorkerSupervisor.ts | 2 +- .../EcmaScriptEngineAdapter.test.ts | 1 + .../ecmascript/EcmaScriptEngineAdapter.ts | 11 +- .../adapters/pcre2/Pcre2EngineAdapter.test.ts | 277 ++++ .../adapters/pcre2/Pcre2EngineAdapter.ts | 823 +++++++++++ .../adapters/pcre2/Pcre2TraceAdapter.ts | 517 +++++++ .../execution/adapters/pcre2/pcre2-module.ts | 80 ++ src/regex/execution/engine-registry.ts | 66 + src/regex/execution/request-limits.ts | 18 + src/regex/execution/worker-protocol.ts | 11 + src/regex/flavours/flavour-registry.test.ts | 158 +++ src/regex/flavours/flavour-registry.ts | 389 +++++ .../formatting/PatternFormatValidator.test.ts | 417 ++++++ .../formatting/PatternFormatValidator.ts | 777 ++++++++++ .../formatting/ecmascript-format.test.ts | 125 ++ src/regex/formatting/ecmascript-format.ts | 190 +++ src/regex/formatting/formatting.types.ts | 98 ++ .../CaseGenerationOrchestrator.test.ts | 455 ++++++ .../generation/CaseGenerationOrchestrator.ts | 429 ++++++ .../generation/GenerationSupervisor.test.ts | 108 ++ src/regex/generation/GenerationSupervisor.ts | 64 + .../generation/generate-candidates.test.ts | 252 ++++ src/regex/generation/generate-candidates.ts | 1222 ++++++++++++++++ src/regex/generation/generation-limits.ts | 30 + .../generation/generation-provenance.test.ts | 29 + src/regex/generation/generation-provenance.ts | 79 ++ src/regex/generation/generation.types.ts | 195 +++ src/regex/generation/seeded-random.test.ts | 34 + src/regex/generation/seeded-random.ts | 51 + .../minimization/SubjectMinimizer.test.ts | 471 +++++++ src/regex/minimization/SubjectMinimizer.ts | 1052 ++++++++++++++ src/regex/minimization/minimization.types.ts | 140 ++ src/regex/minimization/oracles.test.ts | 275 ++++ src/regex/minimization/oracles.ts | 406 ++++++ .../minimization/subject-transforms.test.ts | 242 ++++ src/regex/minimization/subject-transforms.ts | 370 +++++ src/regex/model/flavour.ts | 4 + src/regex/model/match.ts | 12 +- src/regex/model/syntax.ts | 7 +- src/regex/model/trace.ts | 69 + src/regex/reference/token-reference.ts | 102 ++ .../replacement/replacement-preview.test.ts | 18 + src/regex/replacement/replacement-preview.ts | 21 + .../syntax/pattern-syntax-freshness.test.ts | 57 +- src/regex/syntax/pattern-syntax-freshness.ts | 35 +- .../pcre2/Pcre2SyntaxProvider.test.ts | 100 ++ .../providers/pcre2/Pcre2SyntaxProvider.ts | 322 +++++ .../syntax/providers/pcre2/replacement.ts | 236 ++++ src/regex/tests/test-case.types.ts | 7 +- src/regex/tests/test-runner.test.ts | 5 +- src/regex/tests/test-runner.ts | 4 + .../tests/test-suite.serialization.test.ts | 58 + src/styles.css | 605 +++++++- src/toolbox/manifest.source.json | 2 +- src/version.ts | 2 +- src/workers/analysis.worker.ts | 58 + src/workers/generation.worker.ts | 43 + src/workers/pcre2-trace.worker.ts | 76 + src/workers/pcre2.worker.ts | 91 ++ src/workers/syntax.worker.ts | 16 +- tests/browser/analysis.spec.ts | 66 + tests/browser/comparison-codegen.spec.ts | 89 ++ tests/browser/formatting.spec.ts | 98 ++ tests/browser/generation.spec.ts | 125 ++ tests/browser/minimization.spec.ts | 102 ++ tests/browser/workbench.spec.ts | 359 +++++ tests/conformance/pcre2.conformance.test.ts | 348 +++++ tests/fixtures/README.md | 5 +- 180 files changed, 35344 insertions(+), 503 deletions(-) create mode 100644 .prettierignore create mode 100644 LICENSES/Emscripten-MIT.txt create mode 100644 LICENSES/PCRE2.txt create mode 100644 docs/ANALYSIS.md create mode 100644 docs/COMPARISON_AND_CODEGEN.md create mode 100644 docs/FORMATTING.md create mode 100644 docs/GENERATED_CASES.md create mode 100644 docs/MINIMIZATION.md create mode 100644 docs/PCRE2_NEXT_SLICES.md create mode 100644 engines/pcre2/CMakeLists.txt create mode 100644 engines/pcre2/README.md create mode 100644 engines/pcre2/pcre2_bridge.c create mode 100644 engines/pcre2/pcre2_bridge.h create mode 100644 public/engines/pcre2/LICENSE.txt create mode 100644 public/engines/pcre2/SHA256SUMS create mode 100644 public/engines/pcre2/engine-metadata.json create mode 100644 public/engines/pcre2/pcre2.mjs create mode 100644 public/engines/pcre2/pcre2.wasm create mode 100644 scripts/install-pcre2-engine.mjs create mode 100644 scripts/pcre2-codegen-golden.test.ts create mode 100644 scripts/pcre2-engine-pack.mjs create mode 100644 scripts/pcre2-engine-pack.test.ts create mode 100644 src/components/AnalysisPanel.css create mode 100644 src/components/AnalysisPanel.test.tsx create mode 100644 src/components/AnalysisPanel.tsx create mode 100644 src/components/ComparisonCodePanel.css create mode 100644 src/components/ComparisonCodePanel.test.tsx create mode 100644 src/components/ComparisonCodePanel.tsx create mode 100644 src/components/CorpusPanel.test.tsx create mode 100644 src/components/CorpusPanel.tsx create mode 100644 src/components/GenerationPanel.css create mode 100644 src/components/GenerationPanel.test.tsx create mode 100644 src/components/GenerationPanel.tsx create mode 100644 src/components/MinimizePanel.css create mode 100644 src/components/MinimizePanel.test.tsx create mode 100644 src/components/MinimizePanel.tsx create mode 100644 src/components/ModalDialog.tsx create mode 100644 src/components/PatternFormatterPanel.css create mode 100644 src/components/PatternFormatterPanel.test.tsx create mode 100644 src/components/PatternFormatterPanel.tsx create mode 100644 src/components/Pcre2TracePanel.test.tsx create mode 100644 src/components/Pcre2TracePanel.tsx create mode 100644 src/regex/analysis/AnalysisSupervisor.test.ts create mode 100644 src/regex/analysis/AnalysisSupervisor.ts create mode 100644 src/regex/analysis/analysis-limits.ts create mode 100644 src/regex/analysis/analysis-runner.test.ts create mode 100644 src/regex/analysis/analysis-runner.ts create mode 100644 src/regex/analysis/analysis.types.ts create mode 100644 src/regex/analysis/ecmascript-analysis.test.ts create mode 100644 src/regex/analysis/ecmascript-analysis.ts create mode 100644 src/regex/analysis/static-risk.test.ts create mode 100644 src/regex/analysis/static-risk.ts create mode 100644 src/regex/analysis/statistics.test.ts create mode 100644 src/regex/analysis/statistics.ts create mode 100644 src/regex/codegen/pcre2-c.test.ts create mode 100644 src/regex/codegen/pcre2-c.ts create mode 100644 src/regex/comparison/ComparisonOrchestrator.test.ts create mode 100644 src/regex/comparison/ComparisonOrchestrator.ts create mode 100644 src/regex/comparison/compare-results.ts create mode 100644 src/regex/comparison/comparison.types.ts create mode 100644 src/regex/corpus/corpus-export.test.ts create mode 100644 src/regex/corpus/corpus-export.ts create mode 100644 src/regex/corpus/corpus-input.test.ts create mode 100644 src/regex/corpus/corpus-input.ts create mode 100644 src/regex/corpus/corpus-lines.test.ts create mode 100644 src/regex/corpus/corpus-lines.ts create mode 100644 src/regex/corpus/corpus-runner.test.ts create mode 100644 src/regex/corpus/corpus-runner.ts create mode 100644 src/regex/corpus/corpus.types.ts create mode 100644 src/regex/execution/EngineSupervisor.test.ts create mode 100644 src/regex/execution/Pcre2TraceSupervisor.ts create mode 100644 src/regex/execution/adapters/pcre2/Pcre2EngineAdapter.test.ts create mode 100644 src/regex/execution/adapters/pcre2/Pcre2EngineAdapter.ts create mode 100644 src/regex/execution/adapters/pcre2/Pcre2TraceAdapter.ts create mode 100644 src/regex/execution/adapters/pcre2/pcre2-module.ts create mode 100644 src/regex/execution/engine-registry.ts create mode 100644 src/regex/flavours/flavour-registry.test.ts create mode 100644 src/regex/flavours/flavour-registry.ts create mode 100644 src/regex/formatting/PatternFormatValidator.test.ts create mode 100644 src/regex/formatting/PatternFormatValidator.ts create mode 100644 src/regex/formatting/ecmascript-format.test.ts create mode 100644 src/regex/formatting/ecmascript-format.ts create mode 100644 src/regex/formatting/formatting.types.ts create mode 100644 src/regex/generation/CaseGenerationOrchestrator.test.ts create mode 100644 src/regex/generation/CaseGenerationOrchestrator.ts create mode 100644 src/regex/generation/GenerationSupervisor.test.ts create mode 100644 src/regex/generation/GenerationSupervisor.ts create mode 100644 src/regex/generation/generate-candidates.test.ts create mode 100644 src/regex/generation/generate-candidates.ts create mode 100644 src/regex/generation/generation-limits.ts create mode 100644 src/regex/generation/generation-provenance.test.ts create mode 100644 src/regex/generation/generation-provenance.ts create mode 100644 src/regex/generation/generation.types.ts create mode 100644 src/regex/generation/seeded-random.test.ts create mode 100644 src/regex/generation/seeded-random.ts create mode 100644 src/regex/minimization/SubjectMinimizer.test.ts create mode 100644 src/regex/minimization/SubjectMinimizer.ts create mode 100644 src/regex/minimization/minimization.types.ts create mode 100644 src/regex/minimization/oracles.test.ts create mode 100644 src/regex/minimization/oracles.ts create mode 100644 src/regex/minimization/subject-transforms.test.ts create mode 100644 src/regex/minimization/subject-transforms.ts create mode 100644 src/regex/model/trace.ts create mode 100644 src/regex/syntax/providers/pcre2/Pcre2SyntaxProvider.test.ts create mode 100644 src/regex/syntax/providers/pcre2/Pcre2SyntaxProvider.ts create mode 100644 src/regex/syntax/providers/pcre2/replacement.ts create mode 100644 src/workers/analysis.worker.ts create mode 100644 src/workers/generation.worker.ts create mode 100644 src/workers/pcre2-trace.worker.ts create mode 100644 src/workers/pcre2.worker.ts create mode 100644 tests/browser/analysis.spec.ts create mode 100644 tests/browser/comparison-codegen.spec.ts create mode 100644 tests/browser/formatting.spec.ts create mode 100644 tests/browser/generation.spec.ts create mode 100644 tests/browser/minimization.spec.ts create mode 100644 tests/conformance/pcre2.conformance.test.ts diff --git a/.gitignore b/.gitignore index b88b9b1..5893d04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ dist/ release/ +.engine-build/ coverage/ playwright-report/ test-results/ @@ -9,4 +10,3 @@ test-results/ .env .env.* !.env.example - diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c805541 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +public/engines/pcre2/pcre2.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index a468414..1761bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,58 @@ # Changelog +## Unreleased + +## 0.2.0 — 2026-07-27 + +- Open Quick reference and Capabilities as populated, accessible in-viewport + dialogs with keyboard dismissal, focus handling and explicit empty states. +- Add flavour-neutral version, flag, option and execution-worker registries + and expose only the ECMAScript and fully wired PCRE2 runtimes. +- Validate flavour/version/options consistently across workbench requests, + projects and test suites, including legacy schema-v1 identities. +- Add a production local-first Corpus / Apply workspace with bounded UTF-8 + multi-file and pasted-text input, sequential killable worker jobs, progress, + cancellation, explicit whole-document/independent-line semantics, preserved + line separators, bounded capture extraction and replacement previews, + per-document exact/partial summaries, content-free JSON/CSV/NDJSON reports + and privacy-gated exact-output downloads and bounded ZIP export. +- Keep corpus content and results ephemeral: project schema v1 stores only the + selected workspace mode and rejects injected corpus payloads. +- Add a deterministic, offline PCRE2 10.47 WebAssembly runtime with pinned + source/toolchain identities, provenance, licences and checksums. +- Add bounded PCRE2 compile, match and native-substitution ABI calls with + match/depth/heap caps, copied caller-owned records, cleanup on every exit, + UTF-8-to-UTF-16 range normalization and lone-surrogate rejection. +- Run PCRE2 in a dedicated killable/recoverable worker, expose its real + flavour-specific flags and options, and add lexical syntax/replacement + coverage, official quick reference, conformance and browser tests. +- Add an ABI-v3 PCRE2 automatic-callout operation with caller-owned event and + mark buffers, dual event/byte caps, native truncation stop, exact UTF-8 and + UTF-16 positions, a separate killable worker and a bounded viewer that keeps + reported fields distinct from derived movement labels. +- Add ECMAScript-scoped advisory static risk diagnostics plus cancellable + bounded growth probes and cold/warm native-engine benchmarks with median/p95, + separate timing/outcome plots, runtime identity and strict + input/output/wall limits. +- Add an ECMAScript/PCRE2 comparison orchestrator with exact per-side requests, + independent worker timeout outcomes, normalized-range match/capture + alignment, replacement differences and explicit non-comparable states. +- Add a reviewed PCRE2 10.47 8-bit C17 generator with exact UTF-8 byte arrays, + native/host limits, error handling, deterministic source identities and + compile/execute golden gates against the named toolchain. +- Add deterministic, cancellable failing-subject minimization for saved-test + failures, capture ranges, exact engine timeouts and ECMAScript/PCRE2 + mismatches, with real-worker verification, explicit budgets and honest + transform-local minimality. +- Add deterministic, seed-addressed ECMAScript positive/negative case + generation from the normalized AST, with bounded synthesis, actual-engine + verification, explicit unsupported coverage, cancellable workers and + provenance-preserving unit-test integration. +- Add an idempotent grammar-backed ECMAScript literal/control formatter with an + exact preview, separate source/candidate syntax and engine workers, capture + shape checks, applicable unit-test replay, explicit inconclusive states and a + mandatory confirmation before apply. + ## 0.1.0 — 2026-07-24 - Added the open-source ECMAScript 2025 syntax provider using regexpp 4.12.2. diff --git a/LICENSES/Emscripten-MIT.txt b/LICENSES/Emscripten-MIT.txt new file mode 100644 index 0000000..18ad35a --- /dev/null +++ b/LICENSES/Emscripten-MIT.txt @@ -0,0 +1,19 @@ +Copyright (c) 2018 Emscripten authors (see AUTHORS in Emscripten) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSES/PCRE2.txt b/LICENSES/PCRE2.txt new file mode 100644 index 0000000..f6fba35 --- /dev/null +++ b/LICENSES/PCRE2.txt @@ -0,0 +1,104 @@ +PCRE2 Licence +============= + +| SPDX-License-Identifier: | BSD-3-Clause WITH PCRE2-exception | +|---------|-------| + +PCRE2 is a library of functions to support regular expressions whose syntax +and semantics are as close as possible to those of the Perl 5 language. + +Releases 10.00 and above of PCRE2 are distributed under the terms of the "BSD" +licence, as specified below, with one exemption for certain binary +redistributions. The documentation for PCRE2, supplied in the "doc" directory, +is distributed under the same terms as the software itself. The data in the +testdata directory is not copyrighted and is in the public domain. + +The basic library functions are written in C and are freestanding. Also +included in the distribution is a just-in-time compiler that can be used to +optimize pattern matching. This is an optional feature that can be omitted when +the library is built. The just-in-time compiler is separately licensed under the +"2-clause BSD" licence. + + +COPYRIGHT +--------- + +### The basic library functions + + Written by: Philip Hazel + Email local part: Philip.Hazel + Email domain: gmail.com + + Retired from University of Cambridge Computing Service, + Cambridge, England. + + Copyright (c) 1997-2007 University of Cambridge + Copyright (c) 2007-2024 Philip Hazel + All rights reserved. + +### PCRE2 Just-In-Time compilation support + + Written by: Zoltan Herczeg + Email local part: hzmester + Email domain: freemail.hu + + Copyright (c) 2010-2024 Zoltan Herczeg + All rights reserved. + +### Stack-less Just-In-Time compiler + + Written by: Zoltan Herczeg + Email local part: hzmester + Email domain: freemail.hu + + Copyright (c) 2009-2024 Zoltan Herczeg + All rights reserved. + +The code in the `deps/sljit` directory has its own LICENSE file. + +### All other contributions + +Many other contributors have participated in the authorship of PCRE2. As PCRE2 +has never required a Contributor Licensing Agreement, or other copyright +assignment agreement, all contributions have copyright retained by each +original contributor or their employer. + + +THE "BSD" LICENCE +----------------- + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notices, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notices, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of the University of Cambridge nor the names of any + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +EXEMPTION FOR BINARY LIBRARY-LIKE PACKAGES +------------------------------------------ + +The second condition in the BSD licence (covering binary redistributions) does +not apply all the way down a chain of software. If binary package A includes +PCRE2, it must respect the condition, but if package B is software that +includes package A, the condition is not imposed on package B unless it uses +PCRE2 independently. diff --git a/README.md b/README.md index 0274a00..bf20e5b 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,45 @@ Patterns, replacement templates, test text and project files stay in the browser. There is no account, backend, upload, remote execution, telemetry, runtime CDN, remote font, or cloud-save service. -## Current milestone +## Current release — 0.2.0 -Version 0.1.0 is the first production flavour slice: ECMAScript. +The v0.2.0 release contains two real production engine slices: ECMAScript and +PCRE2 10.47. - Syntax acceptance: `@eslint-community/regexpp` 4.12.2, ECMAScript 2025 grammar; application-owned structural explanations are partial and feature-matrix tested. - Execution engine: the current browser's native ECMAScript `RegExp`. - Native and editor offsets: UTF-16 code units. +- PCRE2 execution and substitution: pinned official PCRE2 10.47 compiled to a + self-hosted 8-bit WebAssembly pack with UTF/UCP always enabled. +- PCRE2 native UTF-8 byte ranges are retained and normalized to exact editor + UTF-16 ranges; inputs with lone surrogates are rejected instead of rewritten. +- PCRE2 match-step, depth and heap limits are configurable inside hard caps. +- PCRE2 automatic callouts are copied through a separate ABI operation and + displayed from a dedicated killable trace worker under 50,000-event and + 10 MiB serialized-data hard caps. - Parsing and execution run in separate workers. - Actual execution can be terminated by killing its worker. - The worker is recreated after timeout, crash or cancellation. - Capture ranges use the engine's `d` indices result. - Named, numbered, optional, empty and repeated-final captures are represented. -- Match, replacement, typed list/extraction and unit-test modes are available. +- Match, replacement, typed list/extraction, corpus/apply and unit-test modes + are available. +- ECMAScript-specific advisory static risk findings, bounded dynamic growth + probes and cold/warm benchmarks run locally; timing and growth execution is + isolated in a separately killable worker. +- ECMAScript and PCRE2 can be compared through two exact, independently timed + requests without translating patterns, flags or replacements. Incomplete or + rejected sides are explicitly not comparable. +- The reviewed PCRE2 C17 generator emits exact UTF-8 byte arrays, configured + limits and error handling for the PCRE2 10.47 8-bit API. +- A cancellable, deterministic subject minimizer verifies saved-test failures, + capture-range failures, exact timeouts and ECMAScript/PCRE2 mismatches against + the selected real workers under explicit evaluation and wall budgets. +- Deterministic ECMAScript test-case generation samples the normalized AST from + an explicit seed, reports structural coverage and unsupported constructs, and + labels cases only after verification by the selected real engine worker. - Project JSON import/export and optional IndexedDB save are available. - Interactive test text is not persisted by default. @@ -40,39 +64,67 @@ hierarchy uses syntactic capture nesting while preserving actual engine ranges. ECMAScript exposes only the final retained capture of a repeated group; Regex Tools says so and never invents capture history. -Browser ECMAScript engines do not expose their internal backtracking trace. -Version 0.1.0 therefore presents no ECMAScript execution trace and no -educational trace disguised as one. +Browser ECMAScript exposes no actual engine trace. PCRE2 has a separate bounded +automatic-callout viewer. Its event fields are reported by PCRE2, while +“forward”, “same position” and “apparent backtrack” are visibly derived only +from adjacent reported subject positions. The callout stream is not described +as a complete record of every internal engine action. ## Workbench modes - **Match** — live highlighting, extraction tree and bounded capture table. -- **Replace** — native ECMAScript `RegExp` matches with bounded, - specification-compatible string substitution; output completeness is - reported separately from bounded per-match original/result, changed-range and - token-contribution previews that synchronize the replacement, pattern, - subject and output editors. +- **Replace** — engine-native bounded substitution for ECMAScript and PCRE2; + output completeness is reported separately from bounded per-match + original/result, changed-range and token-contribution previews that + synchronize the replacement, pattern, subject and output editors. - **List** — non-executable typed templates with text, CSV, JSON and NDJSON export; export is disabled when engine collection or bounded rendering is incomplete. +- **Corpus / Apply** — bounded multi-file UTF-8 and pasted-text input processed + sequentially in killable workers. Whole-document and independent-line + semantics are explicit; line apply preserves original separators. Progress, + cancellation, bounded capture samples, replacement previews, per-document + exact/partial summaries, content-free JSON/CSV/NDJSON reports and + privacy-gated exact-output ZIP/downloads are available. Original files are + never modified. - **Unit tests** — editable and cloneable versioned presence, count, full-match, capture, replacement, timing and expected-timeout assertions. Capture a complete current result, select only cases to rerun, filter failures, and import/export a standalone validated JSON suite; execution stays in isolated workers. -Corpus, comparison, debugging, analysis, generation, minimization, formatting -and code generation remain roadmap items. Controls for them are not exposed in -this release. +The auxiliary **PCRE trace**, ECMAScript **Analysis**, **Compare & code**, +**Generate cases**, **Minimize** and **Format** workspaces are separate from +normal matching, replacement, tests and corpus execution. Trace collection is +never included in benchmark measurements. +Comparison retains native ranges while aligning actual results by normalized +editor UTF-16 ranges. Agreement is described only as evidence for the current +subject, never as proof of pattern equivalence. +Case generation is bounded sampling, not a proof of language coverage. Version +1 supports only a complete accepted ECMAScript normalized AST; the partial +PCRE2 provider is reported as unavailable rather than passed through or +relabeled. See +[`docs/GENERATED_CASES.md`](docs/GENERATED_CASES.md). +Minimization verifies every accepted candidate against the exact selected +worker request and claims only local minimality under its documented +Unicode-scalar transforms; see +[`docs/MINIMIZATION.md`](docs/MINIMIZATION.md). +Formatting is currently ECMAScript-only and changes only parser-reported +literal slash/control representations. Apply requires independent reparse, +actual-engine current-subject/replacement comparison, every applicable exact +unit test and explicit confirmation; see +[`docs/FORMATTING.md`](docs/FORMATTING.md). ## Flags and iteration Pattern and flags are stored separately; delimiters are presentation only. -Native `g` and `y` behavior is preserved. The optional **Scan all** action is -explicit. When neither `g` nor `y` is present it adds `g` only to that execution -request, visibly records the internal flag, and does not change the saved user -flags. The `d` flag may likewise be added internally to obtain exact indices -without changing matching semantics. +ECMAScript preserves native `g` and `y`; PCRE2 offers the application-level +`g` iteration flag plus `i`, `m`, `s`, `x`, `U` and `J`. PCRE2 UTF/UCP mode is +mandatory because browser strings are Unicode and partial-byte ranges cannot be +represented safely in the editor. The optional **Scan all** action adds only an +internal iteration flag to that request and does not change saved flags. +ECMAScript may add internal `d` to obtain exact indices without changing match +semantics. Zero-length global iteration advances using the selected Unicode semantics and cannot loop forever. @@ -81,29 +133,63 @@ cannot loop forever. Important defaults: -| Limit | Default | -| ------------------------------ | -----------------: | -| Live parse debounce | 120 ms | -| Live execution debounce | 220 ms | -| Live execution timeout | 250 ms | -| Manual execution timeout | 2 s | -| Advanced maximum timeout | 10 s | -| Pattern hard limit | 64 Ki UTF-16 units | -| Interactive subject hard limit | 16 MiB | -| Replacement template | 64 Ki UTF-16 units | -| List template | 16 Ki UTF-16 units | -| Per-match replacement preview | 200 matches | -| Replacement contributions | 2,000 | -| Maximum matches | 10,000 | -| Maximum capture groups | 1,000 | -| Maximum capture rows | 100,000 | -| Replacement preview | 64 MiB | -| Project JSON document | 32 MiB UTF-8 | -| Standalone test-suite JSON | 32 MiB UTF-8 | -| In-memory unit-test suite | 32 MiB UTF-8 | -| Tests per suite | 1,000 | -| Retained message per test | 2 KiB UTF-8 | -| Unit-test suite wall time | 60 s | +| Limit | Default | +| ------------------------------- | -----------------: | +| Live parse debounce | 120 ms | +| Live execution debounce | 220 ms | +| Live execution timeout | 250 ms | +| Manual execution timeout | 2 s | +| Advanced maximum timeout | 10 s | +| Pattern hard limit | 64 Ki UTF-16 units | +| Interactive subject hard limit | 16 MiB | +| Corpus documents | 256 | +| Per corpus document | 16 MiB | +| Aggregate corpus input | 256 MiB | +| Independent corpus lines | 100,000 | +| Aggregate corpus matches | 100,000 | +| Capture summaries per document | 32 | +| Samples per capture | 3 | +| Retained applied output | 64 MiB | +| Applied ZIP | 68 MiB | +| Corpus batch wall time | 5 min | +| Replacement template | 64 Ki UTF-16 units | +| List template | 16 Ki UTF-16 units | +| Per-match replacement preview | 200 matches | +| Replacement contributions | 2,000 | +| Maximum matches | 10,000 | +| Maximum capture groups | 1,000 | +| Maximum capture rows | 100,000 | +| Replacement preview | 64 MiB | +| Project JSON document | 32 MiB UTF-8 | +| Standalone test-suite JSON | 32 MiB UTF-8 | +| In-memory unit-test suite | 32 MiB UTF-8 | +| Tests per suite | 1,000 | +| Retained message per test | 2 KiB UTF-8 | +| Unit-test suite wall time | 60 s | +| PCRE2 trace events | 50,000 | +| PCRE2 serialized trace | 10 MiB | +| Comparison retained differences | 5,000 | +| Comparison retained alignments | 2,000 | +| Static analysis traversal | 20,000 nodes | +| Static analysis findings | 100 | +| Growth probe steps | 24 | +| Analysis aggregate wall time | 30 s | +| Generated verified cases | 48 | +| Generated candidate attempts | 192 | +| Generation aggregate wall time | 15 s | +| Generation aggregate candidates | 1 MiB | +| Minimizer subject | 1 MiB UTF-8 | +| Minimizer candidate evaluations | 2,000 | +| Minimizer aggregate wall time | 60 s | +| Formatter validation tests | 1,000 | +| Formatter test wall time | 60 s | +| Retained minimizer history | 200 | + +Analysis defaults to three warm-up plus 15 measured samples (caps: 100 and +1,000), with a 2-second per-sample worker limit that can be raised to 10 +seconds. Growth generation defaults to 1 MiB, preflights the central 16 MiB +subject cap and rejects more than 10,000,000 repetitions. Replacement timing +is skipped above an 8 Mi UTF-16-unit output estimate. A timeout terminates actual execution. It is reported as **timed out**, never as “no match”. The next request creates a fresh worker. @@ -128,8 +214,12 @@ require separate explicit opt-ins for JSON export and IndexedDB save. Standalone test-suite JSON likewise omits subjects by default, is schema- and field-validated on import, and appends at most 1,000 total tests. Add, update, clone and append-import also preflight the complete in-memory suite against a -32 MiB aggregate bound. Corpus content is not supported or persisted in this -release. Import, export and IndexedDB save enforce one 32 MiB aggregate UTF-8 +32 MiB aggregate bound. Added generated tests retain their validated generator +version, seed, candidate ID and intended outcome; editing one removes that +provenance. Schema v1 can remember that the corpus workspace was selected, but +corpus documents, contents, outputs and results are always ephemeral and +excluded from JSON and IndexedDB. Imports that try to inject those fields are +rejected. Import, export and IndexedDB save enforce one 32 MiB aggregate UTF-8 JSON-document limit in addition to per-field limits. The latest local save is read through an `updatedAt` index cursor; loading it does not materialize the complete project store. @@ -154,6 +244,9 @@ npm run test:conformance npm run test:browser npm run engines:build npm run engines:verify +npm run engines:pcre2:build -- --source-dir /path/to/pcre2 --emcc /path/to/emcc +npm run engines:pcre2:verify +npm run engines:pcre2:install npm run build npm run toolbox:check npm run check @@ -173,17 +266,34 @@ The deterministic release ZIP can be consumed by Toolbox Portal using an exact artifact URL, manifest ID, version and SHA-256. Portal builds consume the artifact; they do not build Regex Tools source. -Version 0.1.0 has no WebAssembly. Its minimum CSP needs self-hosted scripts and -workers, not `wasm-unsafe-eval`. See -[`docs/PORTAL_REQUIREMENTS.md`](docs/PORTAL_REQUIREMENTS.md). +The PCRE2 worker loads self-hosted JavaScript and WebAssembly only. Deployment +must serve `.wasm` as `application/wasm` and allow WebAssembly compilation in +`script-src`; see [`docs/PORTAL_REQUIREMENTS.md`](docs/PORTAL_REQUIREMENTS.md). ## Flavour roadmap -The next flavour is official PCRE2 10.47 WebAssembly. A technical spike from the -signed official tag has already demonstrated Unicode matching, named captures, -substitution, automatic and explicit callouts, and match/depth/heap limits. -PCRE2 is not shipped until the application-owned bridge, byte/UTF-16 offsets, -trace caps, browser tests and source/licence material meet the acceptance gate. +PCRE2 10.47 is the second implemented flavour. Its application-owned ABI +provides bounded compilation, matching, native substitution and separate +automatic-callout tracing, copied caller-owned records, deterministic +allocation cleanup and explicit result completeness. The exact +source/toolchain build is reproducible offline and the verified pack is bundled +with its metadata, checksums and licence. + +Generated-case version 1 is ECMAScript-only. It traverses the complete +application-owned ECMAScript AST under node, depth, attempt, byte, repetition +and wall-time caps, then verifies every retained label through the actual +browser `RegExp` adapter. PCRE2 generation remains unavailable until its syntax +provider can expose a complete normalized tree. + +Pattern formatting is likewise ECMAScript-only. It consumes the complete +regexpp token model and does not reinterpret the partial PCRE2 structure. +PCRE2 formatting remains unavailable until a complete grammar-backed +implementation passes equivalent idempotence, reparse, engine and test gates. + +The first advertised generated-code target is the PCRE2 10.47 8-bit C API. +Its deterministic match and replacement fixtures compile and execute against +the exact official release. See +[`docs/COMPARISON_AND_CODEGEN.md`](docs/COMPARISON_AND_CODEGEN.md). Python, Go, Rust, .NET, Java and legacy PCRE follow only through their actual named runtimes. No flavour is emulated through JavaScript and relabelled. @@ -198,7 +308,7 @@ notices; see [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) and No regex101 application source, branding, generated explanation prose, reference text or visual design is copied. RegexLib and RGXP.RU fixtures are not redistributed because a suitable fixture licence was not established. No -RegexHub fixture is shipped in v0.1.0. +RegexHub fixture is shipped in v0.2.0. ## Known limitations @@ -211,7 +321,17 @@ RegexHub fixture is shipped in v0.1.0. error node, not a fabricated multi-character span or partial provider AST. - Native JavaScript compile errors do not consistently expose a machine-readable source offset; the provider diagnostic supplies the range. -- Capture history and actual ECMAScript execution tracing are unavailable. +- Capture history is unavailable. Browser ECMAScript exposes no engine trace; + PCRE2 exposes bounded automatic callouts, not a complete internal trace. + Movement classifications are derived and labelled as such. +- Static risk heuristics and generated-input observations apply only to + ECMAScript 2025 and are advisory. They are never proof of safety or of a + pattern’s general time complexity; analysis settings and results are + ephemeral. +- PCRE2 structural explanations are deliberately partial. The application + recognizes common capture forms and PCRE2-only constructs for reference, but + the actual PCRE2 compiler is authoritative. Branch-reset capture numbering is + omitted from the explanation rather than guessed. - Capture-table rendering is paged at 200 rows while aggregate collection is bounded separately. - Explanation/extraction trees and editor decorations show bounded 2,000-node @@ -235,8 +355,17 @@ RegexHub fixture is shipped in v0.1.0. - Unit-test failure messages retain at most 2 KiB UTF-8 per case, so 1,000 results stay below the 2 MiB log budget. Oversized exact replacement results offer a lightweight `should-match` draft when one is valid and fits the suite. -- Benchmark, risk analysis, generated cases, minimization and corpus processing - are not in this release. +- Corpus input accepts recognized UTF-8 text only. It does not auto-detect + legacy encodings or recursively traverse directories. +- Corpus capture extraction is a bounded per-document summary: full match plus + at most 31 capture groups, three 160-unit samples each. Content-free reports + include counts and clipping metadata, never the samples themselves. +- Generated cases deliberately sample only ECMAScript and report + backreferences, lookaround, boundaries, complex Unicode sets and other + unsupported constraints instead of claiming exhaustive coverage. +- Pattern formatting is intentionally narrow and ECMAScript-only. Its bounded + source/candidate and unit-test validation is evidence for the checked + snapshots, not a proof of equivalence over all subjects. Architecture, security, performance, provenance and flavour details live in [`docs/`](docs/). diff --git a/SOURCE.md b/SOURCE.md index 4be6470..b55c84b 100644 --- a/SOURCE.md +++ b/SOURCE.md @@ -1,9 +1,10 @@ # Source identity - Project: Regex Tools -- Version: 0.1.0 +- Release version: `0.2.0` +- Release tag: `v0.2.0` - Repository: -- Release tag: `v0.1.0` +- Previous release tag: `v0.1.0` - Licence: GPL-3.0-or-later - Copyright: © 2026 Albrecht Degering @@ -19,5 +20,14 @@ runtime or build-time CDN fetch. Complete preferred-form source is the tagged repository. The release archive contains compiled static assets plus legal, source-identity and attribution documents. -No PCRE2, Python, Go, Rust, .NET, Java or legacy-PCRE runtime is included in -version 0.1.0. +The explicit offline PCRE2 build is documented in `docs/ENGINE_BUILDS.md`. +PCRE2 source is not vendored: the gate accepts only the pinned, clean official +10.47 checkout and Emscripten 6.0.4 compiler. Generated staging is ignored; the +verified runtime files, deterministic metadata, checksums and exact licence are +installed under `public/engines/pcre2/` and included in the application source +and build. + +The v0.2.0 release includes PCRE2. Python, Go, Rust, .NET, Java and legacy PCRE +remain unimplemented. The v0.1.0 release remains available under its historical +tag and artifact coordinates. A future public artifact must bump package, +manifest, tag and source identity together; it must not reuse `v0.2.0`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index c8a8f39..ad8cee6 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,27 +1,29 @@ # Third-party notices Regex Tools original source is GPL-3.0-or-later. The production browser bundle -also contains the compatible components below. Exact dependency resolution is -recorded in `package-lock.json`. +contains the compatible runtime components below. +Exact dependency resolution is recorded in `package-lock.json`. -| Component | Version | Licence | Shipped | Role and source | -| -------------------------------- | ------------------------------------------------------------------------ | ---------- | ----------------- | ------------------------------------------------------------------------ | -| `@add-ideas/toolbox-contract` | 0.2.2 | Apache-2.0 | Runtime | Manifest contract; | -| `@add-ideas/toolbox-shell-react` | 0.2.2 | Apache-2.0 | Runtime | Shared shell; same source | -| `@eslint-community/regexpp` | 4.12.2 | MIT | Syntax worker | ECMAScript parser; | -| CodeMirror packages | state 6.7.1; view 6.43.6; language 6.12.4; commands 6.10.4; search 6.7.1 | MIT | Runtime | Editors; | -| `@lezer/highlight` | 1.2.3 | MIT | Runtime | Editor highlighting support; | -| React / React DOM | 19.2.7 | MIT | Runtime | User interface; | -| `fflate` | 0.8.3 | MIT | Packaging utility | Deterministic release ZIP; | -| Vite | 8.1.5 | MIT | Generated helpers | Production build; | -| Rolldown | 1.1.5 | MIT | Generated helpers | Production bundling; | +| Component | Version | Licence | Shipped | Role and source | +| -------------------------------- | ------------------------------------------------------------------------ | --------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------- | +| `@add-ideas/toolbox-contract` | 0.2.2 | Apache-2.0 | Runtime | Manifest contract; | +| `@add-ideas/toolbox-shell-react` | 0.2.2 | Apache-2.0 | Runtime | Shared shell; same source | +| `@eslint-community/regexpp` | 4.12.2 | MIT | Syntax worker | ECMAScript parser; | +| CodeMirror packages | state 6.7.1; view 6.43.6; language 6.12.4; commands 6.10.4; search 6.7.1 | MIT | Runtime | Editors; | +| `@lezer/highlight` | 1.2.3 | MIT | Runtime | Editor highlighting support; | +| React / React DOM | 19.2.7 | MIT | Runtime | User interface; | +| `fflate` | 0.8.3 | MIT | Runtime and build dependency | Local corpus-output ZIP and deterministic release ZIP; | +| Emscripten generated runtime | 6.0.4 | MIT | Generated WebAssembly glue | PCRE2 module loader/runtime; | +| PCRE2 | 10.47 | BSD-3-Clause WITH PCRE2-exception | WebAssembly runtime | Pinned official 8-bit engine; | +| Vite | 8.1.5 | MIT | Generated helpers | Production build; | +| Rolldown | 1.1.5 | MIT | Generated helpers | Production bundling; | `@add-ideas/toolbox-testkit` 0.2.2 and the other test/build dependencies are development-only and are not part of the static runtime bundle. `recheck` 4.5.0 and `regexp-ast-analysis` 0.7.1 were inspected but deliberately -not installed or shipped. PCRE2 10.47 was used only in an external feasibility -spike and is not present in the v0.1.0 artifact. +not installed or shipped. PCRE2 source is not vendored; its generated +WebAssembly pack is shipped with exact metadata, checksums and licence. Corresponding licence texts and copyright notices are in `LICENSES/`. The release package additionally carries the exact Vite and Rolldown legal files diff --git a/docs/ANALYSIS.md b/docs/ANALYSIS.md new file mode 100644 index 0000000..88bd95f --- /dev/null +++ b/docs/ANALYSIS.md @@ -0,0 +1,121 @@ +# Performance and risk analysis + +Regex Tools provides one deliberately scoped analysis implementation: +ECMAScript 2025 patterns parsed into the application-owned normalized AST and +executed by the browser's native `RegExp` runtime. The analyser does not apply +ECMAScript findings to PCRE2 or any other flavour. + +Analysis is advisory. The UI uses “potential risk”, “possibly” and “observed +under selected limits”. It never turns an empty finding list into “safe”, +“guaranteed linear” or a vulnerability proof. + +## Static analysis + +The application-owned `regex-tools-ecmascript-heuristics` analyser walks at +most 20,000 normalized nodes and returns at most 100 findings. Each finding +contains: + +- flavour and UTF-16 source range; +- stable rule identifier; +- static or dynamically observed provenance; +- explanation and example risk; +- low, medium or high confidence; +- limitations; +- a suggested investigation. + +The current rules review nested and nullable repetition, ambiguous repeated +alternatives, overlapping alternative prefixes, wildcards and backreferences +inside repetition, repeated lookaround bodies, unanchored amplification, +capture count, nesting depth and replacement-output expansion. These are +structural heuristics. They do not model the native engine's complete compiled +program, optimizations or calling context. + +## Dynamic growth probes + +The user defines a bounded subject family: + +```text +prefix + repeatedFragment.repeat(repetitions) + suffix +``` + +The byte and UTF-16 size are calculated before the generated string is +allocated. The repeated fragment is limited to 4,096 UTF-16 units, each affix +to 16,384 units, the run to 24 samples, and repetitions to 10,000,000. The UI +defaults to a 1 MiB generated-subject cap even though the central interactive +hard cap remains 16 MiB. + +Every probe executes in a dedicated worker. The supervisor terminates and +discards that worker on per-sample timeout, cancellation or crash. Runs also +stop at: + +- the selected repetition bound; +- the selected generated-byte bound; +- the selected step bound; +- the 30-second aggregate wall bound; +- a selected normalized-growth threshold. + +The growth threshold compares the time ratio with the input-size ratio for two +adjacent samples. Sub-millisecond measurements below a noise floor do not +trigger it. A threshold observation characterizes only those generated inputs +and that browser. Timeout, crash, compile rejection and non-match remain +distinct states. + +The UI renders time and outcome in separate plots and keeps the exact sample +table alongside them. + +## Benchmark method + +A benchmark begins with a fresh analysis worker. Worker creation and its +identity response form the separate cold-start measurement. The first native +engine use is retained as a cold sample. Configured warm-up samples are +discarded; configured measured samples produce the warm statistics. + +Each sample measures these phases separately: + +- `RegExp` construction; +- first `exec`; +- bounded all-match iteration; +- native string replacement when its conservative output bound is acceptable; +- all-match throughput and match count. + +All-match iteration retains only counts, not values or captures. It follows the +same explicit scan-all, global, sticky and Unicode zero-length advancement +semantics as the workbench. The application-added indices flag remains in the +effective flags so the measured operation reflects interactive execution. + +Replacement timing is skipped when match collection reaches its bound or a +conservative estimate can exceed 8 Mi UTF-16 units. This prevents a benchmark +from using an unbounded native replacement merely to obtain a timing. +It is also skipped for explicit scan-all with sticky `y`: native +`String.replace` processes only one sticky match, while the workbench's +explicit scan-all operation can process a contiguous sequence. Reporting that +different operation as the same benchmark would be misleading. + +Warm metrics include sample count, minimum, conventional median, nearest-rank +p95 and maximum. Milliseconds are shown to two decimal places, with values +below 0.01 ms labelled accordingly; this does not imply nanosecond accuracy. +The result retains subject size, match count, effective flags, browser runtime +identity and engine identity. + +Defaults: + +| Setting | Default | Hard bound | +| ----------------------- | ------: | ---------: | +| Warm-up samples | 3 | 100 | +| Measured samples | 15 | 1,000 | +| Per-sample timeout | 2 s | 10 s | +| Aggregate wall time | 30 s | 30 s | +| Matches per sample | 10,000 | 10,000 | +| Replacement output gate | 8 MiU | 8 MiU | + +One subject is not a complete performance characterization. Results from +different algorithms, pattern ports, browser engines, WebAssembly runtimes or +machines are not directly comparable. Trace-enabled execution is never used +for benchmark results. + +## Privacy and persistence + +Patterns, subjects, generated inputs, findings and benchmark samples remain in +the browser. No analysis request is uploaded. Analysis settings and results are +currently ephemeral and are not added to project JSON or IndexedDB; this also +keeps large benchmark subjects out of persisted projects by default. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3d49116..d198537 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -5,9 +5,32 @@ boundaries. ```text React workbench - ├─ Pattern SyntaxSupervisor → syntax.worker → Regexpp provider → normalized AST + ├─ Pattern SyntaxSupervisor → syntax.worker + │ ├─ Regexpp ECMAScript provider → normalized AST + │ └─ PCRE2 lexical provider → partial normalized structure ├─ Replacement SyntaxSupervisor → syntax.worker → typed replacement tokens - ├─ EngineSupervisor → ecmascript.worker → native RegExp → match DTOs + ├─ EngineSupervisor + │ ├─ ecmascript.worker → native RegExp → match DTOs + │ └─ pcre2.worker → PCRE2 10.47 WASM ABI → match DTOs + ├─ Pcre2TraceSupervisor → pcre2-trace.worker + │ └─ separate ABI-v3 PCRE2_AUTO_CALLOUT operation → bounded trace DTOs + ├─ AnalysisPanel + │ ├─ normalized-AST ECMAScript static heuristics + │ └─ AnalysisSupervisor → analysis.worker → native RegExp benchmark/growth probes + ├─ ComparisonOrchestrator + │ ├─ two exact syntax snapshots + independently killable engine workers + │ └─ normalized UTF-16 match/capture alignment + explicit incomplete states + ├─ PCRE2 C17 generator → validated snapshot → exact UTF-8 byte-array source + ├─ SubjectMinimizer + │ ├─ fixed syntax snapshots + exact real-engine failure or comparison oracle + │ └─ deterministic Unicode-scalar reducer → bounded local-minimal result + ├─ CaseGenerationOrchestrator + │ ├─ generation.worker → deterministic bounded ECMAScript AST candidates + │ └─ EngineSupervisor → selected actual-engine label verification + ├─ ECMAScript literal/control formatter + │ ├─ exact regexpp literal-token ranges → idempotent candidate preview + │ └─ PatternFormatValidator → paired syntax + actual-engine workers and exact tests + ├─ Corpus EngineSupervisor → sequential bounded document jobs → summaries/outputs ├─ Test SyntaxSupervisor + EngineSupervisor → isolated cancellable test runs └─ project validator / serializer / IndexedDB persistence ``` @@ -21,6 +44,59 @@ request ID and worker generation. A supervisor allows one active request, rejects stale responses, terminates on timeout/crash/cancel, and lazily creates a new generation. Timeout is a distinct error state. +PCRE2 tracing is not an `EngineSupervisor` match or replacement operation. Its +own worker compiles with `PCRE2_AUTO_CALLOUT`, copies only complete fixed +records and bounded mark bytes, and stops natively at either trace cap. +Reported callout fields retain byte positions; movement classifications are a +separate derived UI layer. Normal execution, corpus, tests and benchmarks never +run through the trace operation. + +Static analysis traverses the normalized ECMAScript tree on the UI side under +node/finding caps. Timing and generated-input growth probes use the independent +analysis worker. Timeout, cancellation or crash discards that worker; no trace +mode is benchmarked. + +Comparison is a separate orchestration layer, not an `EngineSupervisor` +operation. It runs explicit ECMAScript and PCRE2 syntax/execution snapshots, +retains each engine identity and native range model, and aligns only normalized +editor ranges. Timeout, cancellation, compile rejection and truncation remain +distinct non-comparable outcomes. Shared patterns are sent unchanged; +per-flavour ports are user-owned variants. + +The PCRE2 C generator is application-owned and never implements browser +execution. It validates one exact PCRE2 snapshot, emits user strings as +independent UTF-8 byte arrays, and states the host-level mappings that the +PCRE2 API cannot express directly. Its advertised C17 target is protected by +deterministic golden identities and an exact PCRE2 10.47 compile/execute gate. + +Subject minimization is a main-thread bounded reducer whose expensive predicate +checks stay in independently terminable engine workers. Fixed pattern syntax +is parsed once for capture metadata. The baseline and every accepted candidate +retain the exact target and engine identity. Timeout, crash, cancellation, +worker failure and incomplete output are separate outcomes. Deterministic +ddmin deletion is followed by a fixed-point local sweep over Unicode-scalar +deletion and lower-rank canonical replacement; only completion of that sweep +permits a transform-local minimality claim. + +Generated-case synthesis and verification are separate trust boundaries. The +dedicated generation worker receives only a validated, complete ECMAScript +normalized tree plus explicit settings and seed. The main-thread orchestrator +then sends each bounded candidate to `EngineSupervisor`; only the selected real +adapter can establish a `should-match` or `should-not-match` label. Worker +timeout, crash, cancellation, compile rejection, wrong outcome and ordinary +failure stay distinct. Settings/results are ephemeral, while explicitly added +unit tests retain field-validated generator provenance. + +Formatting is not an engine operation and never rewrites from heuristic text +alone. The ECMAScript formatter consumes an exact accepted regexpp snapshot and +changes only parser-reported literal/control token ranges. Its validator +reparses source and candidate separately, preserves capture shape, runs both in +separate actual-engine workers against the current replacement snapshot, then +replays every enabled test tied to the exact active pattern/version/flags/options. +Any difference, incomplete output, one-sided timeout, identity change or +inconclusive worker outcome blocks apply. The preview and observations are +ephemeral, and user confirmation is required even after all bounded gates pass. + Pattern and replacement syntax use independent supervisors. A pattern snapshot is bound to its exact pattern, ordered flags and revision; execution cannot use acceptance or capture metadata from an earlier input. Replacement output is @@ -32,10 +108,27 @@ over those actual matches, the complete replacement-token model and the bounded output. They execute no replacement callback or user code and have separate presentation and token-evaluation limits. -Native ranges are retained in the engine's declared unit. Browser editor ranges +Corpus documents are ephemeral React session state, not project state. A +dedicated engine supervisor processes one document at a time and is terminated +on cancellation, timeout or workspace exit. Batch orchestration retains only +per-document summaries and, for apply runs, bounded outputs. It enforces +document-count, per-document byte, aggregate input byte, aggregate match, +aggregate output and batch wall-time limits. Exact-output export is unavailable +if any document is partial, failed, cancelled or not run. + +Whole-document mode sends each document to the engine once. Independent-line +mode segments CRLF, CR, LF, U+2028 and U+2029 without discarding them, executes +each logical line as its own subject, and reattaches the exact original +separator during apply. This makes anchor and cross-line behavior deliberately +different rather than silently rewriting the pattern. Match DTOs are reduced to +bounded per-group participation counts and samples before the next document. + +Native ranges are retained in the engine's declared unit. PCRE2 always uses +UTF/UCP and exposes UTF-8 byte ranges; its ABI copies scalar records and names +out of WebAssembly before returning and retains no code pointer. Browser editor ranges are half-open UTF-16 code-unit ranges. Reusable converters cover UTF-8 byte and -Unicode code-point offsets for later flavours, including invalid-boundary -rejection and lone-surrogate detection. +Unicode code-point offsets, including invalid-boundary rejection and +lone-surrogate detection. The release boundary is `dist/` plus checked-in legal/source documents. The Portal consumes the resulting immutable ZIP and never imports React source. diff --git a/docs/COMPARISON_AND_CODEGEN.md b/docs/COMPARISON_AND_CODEGEN.md new file mode 100644 index 0000000..137851c --- /dev/null +++ b/docs/COMPARISON_AND_CODEGEN.md @@ -0,0 +1,118 @@ +# ECMAScript/PCRE2 comparison and PCRE2 C generation + +## Comparison contract + +The comparison panel sends two explicit requests: one to the native browser +ECMAScript worker and one to the bundled PCRE2 10.47 worker. It does not +translate patterns, flags, options or replacement templates. + +Two pattern models are available: + +- **Shared pattern** sends the same string unchanged to both engines. +- **Per-flavour variants** retains two explicit ports supplied by the user. + +Flags are selected independently from each flavour's registered allowlist. +PCRE2 match, depth and heap bounds are part of its exact request. Both sides +share the selected subject, scan-all intent, host result bounds and wall-clock +worker timeout. + +Each side retains: + +- its syntax-provider request, acceptance, diagnostics and coverage; +- the exact execution or replacement request sent to its worker; +- a deterministic display key plus the complete authoritative request; +- completion, timeout, cancellation or worker-failure status; +- engine, runtime and adapter identity when the engine returns one; +- user and effective flags; +- native ranges and normalized editor UTF-16 ranges; +- compile acceptance, matches, captures and replacement output. + +The orchestrator owns two syntax supervisors and the multi-engine execution +supervisor. The two engine calls run concurrently through independently +killable flavour workers. A side timeout kills only that worker. The shared +Cancel action terminates every active comparison worker. Comparison remains +outside `EngineSupervisor`, whose responsibility is one engine operation and +lifecycle boundary. + +Match pairs are aligned by equal normalized UTF-16 ranges first. Unpaired +matches use an explicitly labelled ordinal fallback. Captures are aligned by +normalized range, then capture identity, then an explicitly labelled ordinal +fallback. Original native offsets remain attached to both results. + +No equivalence claim is made when syntax or compilation is rejected, a worker +times out/fails/is cancelled, results or values are truncated, or replacement +output is incomplete. A completed matching result is labelled only as +agreement for the current subject; it is evidence, not a proof that two +patterns are equivalent. Timeout is never interpreted as “no match.” + +The retained DTO is bounded to 5,000 displayed difference records and 2,000 +match alignments while still counting all differences in the bounded engine +result. The panel renders at most 250 differences and 100 alignments at once. + +## PCRE2 C17 generator + +The first reviewed code-generation target is the PCRE2 10.47 8-bit C API. +Other languages remain unavailable until their generators have equivalent +escaping, semantic and toolchain gates. + +The generator consumes a validated PCRE2 snapshot and: + +- requires the registered PCRE2 10.47 version and flag/option allowlists; +- refuses unpaired UTF-16 surrogates rather than emitting lossy UTF-8; +- emits pattern, subject and replacement as independent explicit UTF-8 byte + arrays, so quotes, backslashes, newlines, NUL, Unicode, dollar signs and + braces cannot escape into C source syntax; +- enables mandatory `PCRE2_UTF` and `PCRE2_UCP`; +- maps `i`, `m`, `s`, `x`, `U` and `J` to named PCRE2 compile constants; +- implements application-level `g`/scan-all behavior explicitly; +- handles the PCRE2 empty-match anchored retry before advancing one UTF-8 + character; +- configures native match, depth and heap limits; +- enforces match/capture row caps in the generated match loop; +- bounds replacement output with a host buffer and withholds output when its + post-run result-row bound is exceeded; +- reports compile offsets and native API errors and cleans up all PCRE2 state; +- rejects compilation against a PCRE2 header other than 10.47 and verifies the + runtime version. + +The generated program states limitations that cannot be mapped honestly: + +- PCRE2 has no `g` compile flag; +- match/capture rows and replacement-output size are host-application bounds, + not PCRE2 matching options; +- replacement result-row limits can be checked only after + `pcre2_substitute()` has completed; +- PCRE2 exposes no native wall-clock timeout, so hard elapsed-time + cancellation belongs to the containing process; +- C API offsets are UTF-8 bytes, not browser editor UTF-16 units. + +Generated snippets are examples only and are never used to implement the +browser runtime. + +## Golden and toolchain gate + +`scripts/pcre2-codegen-golden.test.ts` pins deterministic SHA-256 identities +for match and replacement fixtures: + +```text +match 22b91d578a449b0ed5c30e6eadad30f19a2bcb26ac9db355c8ec7a69328675da +replacement ab90a0d9831ea7b7145aa0072ac352b42baf9a795319d81f531b0f341b04a636 +``` + +The compile/execute gate was run against a host static build from official +signed tag `pcre2-10.47`, commit +`f454e231fe5006dd7ff8f4693fd2b8eb94333429`. Both generated fixtures compile +under C17 with `-Wall -Wextra -Werror`; the match fixture verifies native UTF-8 +ranges and named captures, and the replacement fixture verifies exact Unicode +output and substitution count. + +To repeat the gate with an exact local PCRE2 installation: + +```sh +PCRE2_CODEGEN_PREFIX=/path/to/pcre2-10.47-prefix \ + npm test -- scripts/pcre2-codegen-golden.test.ts +``` + +The prefix must provide `include/pcre2.h` and +`lib64/libpcre2-8.a`. A different static-library location can be supplied +through `PCRE2_CODEGEN_LIBRARY`. diff --git a/docs/ENGINE_BUILDS.md b/docs/ENGINE_BUILDS.md index ec068a7..072ce6f 100644 --- a/docs/ENGINE_BUILDS.md +++ b/docs/ENGINE_BUILDS.md @@ -1,28 +1,74 @@ # Engine builds -Version 0.1.0 ships no WebAssembly engine. +The production application uses the browser's native `RegExp` for ECMAScript +and bundles a verified PCRE2 10.47 WebAssembly pack. Normal application builds +never download or compile an engine: the reviewed files under +`public/engines/pcre2/` are copied into `dist/` and verified there. -`npm run engines:build` verifies that this release has no external pack to -build. After the production build, `npm run engines:verify` rejects any -undeclared engine asset and confirms the packaged ECMAScript-only notice. -Neither command accesses the network. +## Rebuilding PCRE2 -## PCRE2 feasibility record +The reproducible build accepts only: -The next flavour was spiked outside the application repository against official -PCRE2 10.47: +- PCRE2 tag `pcre2-10.47`, signed tag object + `cd007b4466798f66d479d1442a407099e7c40050`, peeled commit + `f454e231fe5006dd7ff8f4693fd2b8eb94333429` and tree + `81a83a3552bd68d0ea7b7004f8bb6e7892f583ba`; +- Emscripten 6.0.4, compiler revision + `fe5be6afdff43ad58860d821fcc8572a23f92d19`, from emsdk commit + `224ec5f9f2f72f09f9ce0e26d66bae7dbd8b692f`; +- CMake 4.3.4 and Ninja 1.13.2; +- 8-bit PCRE2 with Unicode enabled and JIT, threads and filesystem disabled. -- signed tag object `cd007b4466798f66d479d1442a407099e7c40050`; -- peeled commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429`; -- licence `BSD-3-Clause WITH PCRE2-exception`; -- Emscripten 6.0.4; -- 8-bit library, Unicode enabled, JIT disabled. +The source and compiler checkouts must already exist locally and be completely +clean. No command clones, downloads or updates them: -Node and Chromium tests demonstrated version/config reporting, Unicode, -named groups, global substitution, automatic and explicit callouts, and -match/depth/heap errors. This was a technical spike only. No spike source, -binary or PCRE2 source is included in v0.1.0. +```sh +npm run engines:pcre2:build -- \ + --source-dir /absolute/path/to/pcre2 \ + --emcc /absolute/path/to/emscripten/emcc +npm run engines:pcre2:verify +npm run engines:pcre2:install +``` -Production PCRE2 support remains gated on a reviewed bridge, copied DTOs, -allocation cleanup, exact source build, UTF-8/UTF-16 mapping, trace/output caps, -worker recovery, deterministic assets, licences and browser conformance. +Use `--force` only to replace the exact ignored `.engine-build/pcre2` output. +The explicit install command first re-verifies that pack and then atomically +replaces only `public/engines/pcre2`. + +The builder verifies Git objects and critical source/compiler hashes, +sanitizes build flags, sets `SOURCE_DATE_EPOCH=1760997684`, builds serially and +emits exactly: + +- `pcre2.mjs` and `pcre2.wasm`; +- deterministic `engine-metadata.json`; +- the exact upstream `LICENSE.txt`; +- `SHA256SUMS`. + +The verifier checks the closed file set, metadata, source bridge hashes and all +checksums. It validates and instantiates WebAssembly, checks ABI/engine/config +identity, then executes native Unicode compile, bounded match and bounded +substitution smoke cases plus normal and cap-stopped automatic-callout traces. + +The immutable signed-tag object is pinned. OpenPGP trust validation remains a +release-operator step because the offline builder does not install or trust a +key. + +## Runtime boundary + +ABI version 3 has no retained native handles. Each call compiles, obtains +capture names, matches/substitutes or traces and releases its code, match data +and match context before returning. The caller supplies fixed-capacity +result/name/output/event/mark buffers. Hard maxima are 1 MiB pattern, 16 MiB +subject, 1,000 captures, 10,000 matches, 100,000 capture rows, 50,000 trace +events, 10 MiB serialized trace data and 128 MiB PCRE2 heap limit; the +workbench uses stricter pattern and replacement input limits where applicable. + +PCRE2 always runs with UTF and UCP. Native UTF-8 byte offsets are retained in +DTOs and converted only at valid code-point boundaries to half-open editor +UTF-16 ranges. Lone UTF-16 surrogates are rejected before encoding. + +Normal execution lives only in `pcre2.worker`. Instrumented +`PCRE2_AUTO_CALLOUT` collection is a separate ABI operation loaded only by +`pcre2-trace.worker`; normal match, replacement, tests, corpus and benchmarks +never call it. Each supervisor terminates its worker on timeout, crash, +cancellation or supersession and lazily creates a clean generation for the +next request. diff --git a/docs/EXECUTION_TRACES.md b/docs/EXECUTION_TRACES.md index 236ac8f..e337ad7 100644 --- a/docs/EXECUTION_TRACES.md +++ b/docs/EXECUTION_TRACES.md @@ -1,13 +1,39 @@ # Execution traces -Version 0.1.0 exposes no execution trace. +Browser ECMAScript APIs expose compilation, match, capture and replacement +results, but no V8, SpiderMonkey or JavaScriptCore execution trace. Regex Tools +does not relabel structural explanations, static findings or timing +observations as an ECMAScript engine trace. -Browser ECMAScript APIs return compilation, match, capture and replacement -results but not the runtime's internal backtracking events. Regex Tools does -not label structural explanations, static hints or adjacent-result inference -as an actual engine trace. +PCRE2 10.47 has a separate actual automatic-callout vertical. ABI version 3 +compiles the requested pattern with `PCRE2_AUTO_CALLOUT`, registers an +application callback, and copies only: -The next PCRE2 slice will use actual automatic/explicit callout events from an -application-owned bridge. Event count, serialized bytes, match/depth/heap -limits and wall-clock termination must all be enforced. Classifications such -as “apparent backtrack” will remain visibly derived from adjacent events. +- callout number; +- pattern byte position and next-item byte length; +- subject byte position; +- capture-top and capture-last numbers; +- bounded current mark text. + +These fields have `reported` provenance. Pattern and subject byte boundaries +are validated and normalized to exact editor UTF-16 positions while the native +values remain visible. + +Collection runs in `pcre2-trace.worker`, not the normal execution worker. It +uses the selected PCRE2 match, depth and heap limits plus the supervisor wall +clock. The callback retains only complete 32-byte records and mark slices under +both a 50,000-event and 10 MiB serialized-data hard cap. At a cap it returns +PCRE2’s reserved `PCRE2_ERROR_CALLOUT`, abandoning the native match; the result +retains that native status, the last complete event and an explicit truncation +flag. Timeout, cancellation or crash discards the complete worker. + +The viewer derives “forward”, “same position” and “apparent backtrack” solely +from the difference between adjacent reported subject positions. Each label +has `derived` provenance. This is useful navigation, not a claim that the +stream reports every internal action. PCRE2 start optimizations can also +conclude some requests without any callout. + +Tracing represents one exact `pcre2_match()` invocation. The application-level +`g` iteration flag is not applied, and that fact is reported. Normal match, +replacement, tests, corpus, comparisons and benchmarks never use the +instrumented trace path. diff --git a/docs/EXPLANATION_MODEL.md b/docs/EXPLANATION_MODEL.md index 37f2e16..f5ed969 100644 --- a/docs/EXPLANATION_MODEL.md +++ b/docs/EXPLANATION_MODEL.md @@ -1,9 +1,9 @@ # Explanation model -The community provider converts regexpp nodes into a stable application AST -inside the syntax worker. Nodes carry deterministic IDs, UTF-16 ranges, raw -source, capture metadata, quantifier bounds, assertion kind, support status, -provider identity and provenance. +The ECMAScript community provider converts regexpp nodes into a stable +application AST inside the syntax worker. Nodes carry deterministic IDs, +UTF-16 ranges, raw source, capture metadata, quantifier bounds, assertion kind, +support status, provider identity and provenance. Explanations are original deterministic text selected by node type. Nullable and minimum/maximum consumed-length properties are derived recursively where @@ -15,6 +15,13 @@ regexpp 4.12.2 does not expose tolerant recovery. A malformed pattern therefore gets the provider's exact diagnostic plus an application-owned error node. No partial regexpp AST is claimed. +The PCRE2 provider currently emits a deliberately partial lexical tree for +common capture forms. It does not infer branch-reset numbering or claim full +nesting; authoritative compilation, capture counts, names and result ranges +come from PCRE2 itself. + The explanation tree is structural. It is never called an actual execution trace. Selecting a node selects and scrolls its exact editor range; selecting -pattern text chooses the smallest enclosing node. +pattern text chooses the smallest enclosing node. PCRE2’s separate trace viewer +contains actual bounded automatic callouts; its movement labels remain a +distinct derived layer and are never copied into the structural explanation. diff --git a/docs/FLAVOUR_SUPPORT.md b/docs/FLAVOUR_SUPPORT.md index fe88c9e..6590c4c 100644 --- a/docs/FLAVOUR_SUPPORT.md +++ b/docs/FLAVOUR_SUPPORT.md @@ -1,20 +1,55 @@ # Flavour support -| Flavour | Syntax | Execution | Replacement | Captures | Trace | Native offsets | -| ------------ | ------------------------------------------------------------------ | ----------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------- | ----------- | ------------------- | -| ECMAScript | regexpp 4.12.2 grammar; explanations partial/feature-matrix tested | Current browser `RegExp`; feature-matrix tested | Native matches; bounded ECMAScript `GetSubstitution` | Named/numbered; final repeated capture; no history | Unavailable | UTF-16 | -| PCRE2 | Unavailable in release | Technical spike only; not shipped | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes | -| PCRE | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | -| Python `re` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned code points | -| Go `regexp` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes | -| Rust `regex` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes | -| .NET | Unavailable | Unavailable | Unavailable | Planned capture history | Unavailable | Planned UTF-16 | -| Java | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-16 | +| Flavour | Syntax | Execution | Replacement | Captures | Trace | Analysis | Generated cases | Native offsets | +| ------------ | ------------------------------------------------------------ | ------------------------------------------------------ | ---------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------- | ------------------- | +| ECMAScript | regexpp 4.12.2; explanations partial/feature-matrix tested | Current browser `RegExp`; conformance tested | Bounded ECMAScript `GetSubstitution` | Named/numbered; final repeated capture; no history | Unavailable | Experimental static heuristics + bounded native probes | Bounded normalized-AST sampling; actual-engine verified | UTF-16 | +| PCRE2 10.47 | Application lexical provider partial; compiler authoritative | Pinned 8-bit WASM; bounded/killable/conformance tested | Native bounded `pcre2_substitute()` loop | Native named/numbered records; no capture history | Bounded reported automatic callouts; derived movement labels | Unavailable; ECMAScript heuristics are never applied | Unavailable; structural syntax provider is partial | UTF-8 bytes | +| PCRE | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | +| Python `re` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned code points | +| Go `regexp` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes | +| Rust `regex` | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-8 bytes | +| .NET | Unavailable | Unavailable | Unavailable | Planned capture history | Unavailable | Unavailable | Unavailable | Planned UTF-16 | +| Java | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Unavailable | Planned UTF-16 | -ECMAScript tests cover `g`, `y`, `u`, internal `d`, zero-length iteration, -named groups, unmatched/empty/repeated groups, lookbehind, backreferences, -Unicode properties, astral input, replacement and result truncation. +ECMAScript tests cover its complete exposed flag matrix, internal indices, +zero-length iteration, captures, lookbehind, backreferences, Unicode properties +and bounded replacement. -Actual browser identity is displayed because ECMAScript behavior can evolve -with the runtime. Syntax acceptance and engine compilation are separate; -neither silently rewrites the pattern. +PCRE2 tests cover `g`, `i`, `m`, `s`, `x`, `U`, `J`, mandatory UTF/UCP, +branch-reset groups, recursion, `\K`, duplicate names, native substitution, +astral byte/UTF-16 ranges, zero-length iteration, compile errors, match limits +and result/output truncation. Trace tests cover reported automatic callouts, +bounded mark copying, native stop at the complete-event cap, compile ranges, +worker loading and browser rendering. + +ECMAScript static findings and generated-input timing observations are +advisory. They describe only the selected ECMAScript 2025 pattern, browser +runtime and bounded inputs; they are not proofs of safety or general +complexity. + +ECMAScript generated-case version 1 uses only a complete accepted normalized +AST, an explicit deterministic seed and bounded sampling strategies. Every +retained positive or negative label is confirmed by the actual selected +browser engine. PCRE2 generation is unavailable because its current lexical +provider is intentionally partial; ECMAScript behavior is never relabelled as +PCRE2. + +The PCRE2 explanation provider deliberately does not invent full structural +coverage. In particular, branch-reset numbering is omitted from the syntax tree +while actual capture numbers and names still come from PCRE2 results. + +ECMAScript and PCRE2 support exact-request comparison for syntax acceptance, +engine compilation, matches, captures and engine-native replacement. Results +align by normalized UTF-16 ranges while retaining each native offset model. +Timeout, cancellation, rejection and truncation are never presented as +equivalent results. + +Pattern formatting is available only for an exact accepted ECMAScript regexpp +snapshot. It canonicalizes literal slashes and raw control/line-separator +representations, then requires paired actual-engine and applicable exact-test +validation before apply. PCRE2 formatting is unavailable because the current +provider is deliberately partial; its syntax is never treated as ECMAScript. + +PCRE2 is the only current code-generation target. The reviewed C17 generator is +pinned to the PCRE2 10.47 8-bit API; other language generators remain +unavailable until they pass equivalent escaping and named-toolchain gates. diff --git a/docs/FORMATTING.md b/docs/FORMATTING.md new file mode 100644 index 0000000..9ed2a83 --- /dev/null +++ b/docs/FORMATTING.md @@ -0,0 +1,57 @@ +# Pattern formatting + +Regex Tools includes a deliberately narrow, local ECMAScript formatter. It +canonicalizes representations that can be changed without inventing a visual +layout: + +- an unescaped literal `/` becomes `\/`, making the pattern safe to copy into + an ECMAScript regex literal; +- raw NUL and control code units become fixed-width `\xNN` or their canonical + short escape (`\t`, `\n`, `\v`, `\f`, `\r`); +- raw U+2028 and U+2029 line separators become `\u2028` and `\u2029`; +- legacy escaped raw controls are replaced as one parser-reported literal + token, so the formatter does not introduce an extra backslash. + +Existing explicit escape spellings, groups, alternatives, quantifiers, inline +flags and literal whitespace are otherwise preserved byte for byte. ECMAScript +regex whitespace is normally significant, so the tool does not insert +indentation, line breaks or comments and does not claim to be a structural +pretty-printer. The transform is idempotent. + +## Eligibility + +Formatting requires the exact current, accepted, non-recovered syntax snapshot +from the bundled `@eslint-community/regexpp` provider. Transformations are +derived from the provider's literal-token ranges rather than from a heuristic +text scan. A stale parse, a malformed pattern, a partial provider, PCRE2 or any +other flavour is reported as unavailable and is never silently rewritten. + +## Mandatory validation before apply + +The preview cannot be applied directly. Regex Tools first: + +1. reparses source and candidate independently and checks capture numbering, + names, repetition and parent-capture shape; +2. compiles and runs the source and candidate in separate actual ECMAScript + engine workers against the current subject and replacement; +3. compares exact normalized match, capture and replacement output, including + the engine identity; +4. reruns every enabled unit test whose flavour, version, pattern, flags and + engine options exactly match the active source snapshot, and compares both + semantic output and assertion outcome. + +A compile rejection, one-sided timeout, crash, worker error, cancellation, +truncated result, engine-identity change, incomplete test suite or semantic +difference blocks application. Matching fixed timeouts are comparable only for +an exact unit-test pair; two timeouts on the current interactive snapshot are +inconclusive. If no unit test is applicable, the UI says so and still requires +the current subject/replacement gate. + +Validation is bounded to 1,000 tests, the normal worker/input limits and a +60-second aggregate test wall budget. A test is not started unless its complete +configured timeout still fits that budget. The user must explicitly confirm +the exact validated candidate before the Apply button is enabled. + +Passing these gates is strong, reproducible evidence for the current snapshot +and exact tests. It is not a mathematical proof of equivalence for every +possible subject. diff --git a/docs/GENERATED_CASES.md b/docs/GENERATED_CASES.md new file mode 100644 index 0000000..0935037 --- /dev/null +++ b/docs/GENERATED_CASES.md @@ -0,0 +1,135 @@ +# Generated test cases + +Regex Tools generator version 1 creates deterministic candidate subjects from +the application-owned normalized AST and then verifies every candidate through +the selected actual engine adapter. Generation is local-only and bounded. It is +sampling, not proof of language coverage, equivalence or safety. + +## Supported scope + +Version 1 is scoped to an accepted ECMAScript 2025 normalized AST from the +bundled regexpp-based provider. It requests: + +- a shortest structural candidate using the shortest visible alternative and + each quantifier minimum; +- each structurally visible alternative; +- quantifier minimum, adjacent and bounded upper values; +- optional-absent and optional-present values; +- bounded character-class, dot and Unicode-property representatives; +- newline and non-boundary context around anchors; +- deletion, replacement and boundary mutations as likely near misses; +- deterministic random alternative, quantifier and class choices. + +The coverage report says `covered`, `partial`, `unsupported` or +`not applicable` for each category. “Covered” means that the documented bounded +sampling strategy was requested. It does not mean every string in the regular +language was enumerated. + +PCRE2 generation is unavailable in version 1. Its actual engine is complete for +the advertised execution operations, but its current structural syntax +provider is intentionally partial. Regex Tools does not feed PCRE2 syntax to +the ECMAScript generator or relabel ECMAScript behavior. + +## Unsupported and partial constructs + +The report includes exact normalized source ranges and a reason for every +recognized gap, capped at 250 rendered entries. Version 1 does not solve: + +- capture-dependent backreferences; +- lookaround or word-boundary constraints symbolically; +- recursion, subroutine calls or conditional execution; +- branch-reset and atomic grouping; +- engine control verbs or callouts; +- complex Unicode-set algebra and properties of strings; +- recovered or provider-unsupported syntax. + +Surrounding candidates may still happen to satisfy a lookaround or boundary. +They are retained only when the actual selected engine confirms the requested +match outcome. That confirmation does not upgrade the generator’s structural +coverage claim. + +## Determinism and seed + +The seed is explicit text of 1–128 UTF-16 units. Generator version 1 hashes its +UTF-16 code units with a documented stable 32-bit FNV-1a step and drives an +application-owned 32-bit deterministic sequence. It never calls +`Math.random()`. + +The same generator version, normalized AST, flags, settings and seed produce +the same candidate order and candidate identifiers. Verification timing is +environment-dependent, and strict wall-time or engine-timeout bounds can +therefore stop two runs at different points. The UI records both the original +seed and its eight-digit hash. Generated unit tests retain: + +- generator id and version; +- the original seed; +- candidate id; +- intended positive or negative outcome. + +That provenance survives project and test-suite JSON export when subjects are +included. Editing a generated test turns it into a user-authored test and +removes the generation provenance. + +## Actual-engine verification + +Candidate synthesis runs in a dedicated killable worker. The verifier then +sends each exact candidate, pattern, flavour version, flags, engine options and +scan mode through `EngineSupervisor`, which selects the registered real engine +worker. + +- An intended positive is labelled `should-match` only after the engine returns + at least one match. +- An intended negative is labelled `should-not-match` only after the engine + returns no match. +- A candidate with the opposite result is discarded and retained only as a + bounded advanced diagnostic. +- Engine compile rejection, timeout, worker crash, cancellation and ordinary + execution error remain distinct outcomes. +- Cancellation terminates both candidate-synthesis and active engine workers. +- Configuration changes invalidate results immediately; old cases are never + displayed against a new pattern or engine configuration. + +The engine name, version, adapter, runtime and native offset unit shown with the +result come from the actual execution result, not static marketing metadata. + +## Bounds + +Defaults: + +| Bound | Default | +| -------------------------------------- | ------------------------: | +| Retained verified cases | 48 | +| Candidate attempts | 192 | +| Seeded random variants | 16 | +| Per-case engine timeout | 500 ms | +| Aggregate wall time | 15 s | +| Bytes per candidate subject | 64 KiB | +| Aggregate candidate subject bytes | 1 MiB | +| Synthesized repetitions per quantifier | 32 | +| Normalized AST traversal | 20,000 nodes / 512 levels | + +Hard caps: + +| Bound | Hard cap | +| -------------------------------------- | -------: | +| Retained verified cases | 1,000 | +| Candidate attempts | 4,000 | +| Per-case engine timeout | 10 s | +| Aggregate wall time | 30 s | +| Bytes per candidate subject | 16 MiB | +| Aggregate candidate subject bytes | 16 MiB | +| Synthesized repetitions per quantifier | 1,024 | +| Advanced discard diagnostics | 250 | + +UTF-8 sizes are checked before a candidate is retained for verification. +Quantifier expansion is preflighted before `String.prototype.repeat` allocates +the output. Reaching a case, attempt, byte, AST, repetition or wall-time bound +is visible in the result; it is never reported as complete coverage. + +## Privacy and persistence + +No pattern, candidate, verified subject, diagnostic or seed is uploaded. +Results and settings are ephemeral until the user explicitly adds cases to the +unit-test suite or downloads the verified JSON artifact. Test-suite export +omits subjects by default because generated subjects may still encode literal +pattern content. Enabling subject export is an explicit user choice. diff --git a/docs/MINIMIZATION.md b/docs/MINIMIZATION.md new file mode 100644 index 0000000..656dc5d --- /dev/null +++ b/docs/MINIMIZATION.md @@ -0,0 +1,71 @@ +# Subject minimization + +The **Minimize** workspace reduces one failing subject locally against the +selected real ECMAScript or PCRE2 worker. It can preserve: + +- the exact failure class of any supported saved unit-test assertion; +- an identified capture's wrong participation, value or UTF-16 range; +- an engine worker timeout at one fixed deadline; +- an ECMAScript/PCRE2 semantic mismatch with the same mismatch-kind set; or +- the same one-sided comparison timeout while the other engine completes with + authoritative, untruncated output. + +The fixed pattern, ordered flags, options, replacement and engine versions form +the target identity. Syntax is parsed once to obtain capture metadata. Every +accepted subject candidate is then verified through the selected execution +worker; the reducer does not contain a substitute regex implementation. + +## Deterministic transforms + +Subjects must be well-formed Unicode. The reducer never splits a surrogate +pair and rejects input containing a lone surrogate. + +Reduction order is stable: + +1. contiguous ddmin chunk deletion, left to right; +2. single Unicode-scalar deletion, left to right; then +3. lower-rank replacement by `a`, `0`, space, newline and `!`, in that order. + +The last two passes restart after every accepted change and continue to a fixed +point. A complete pass with no accepted candidate establishes only **local +minimality under those transforms**. It is not a proof of the shortest, +globally minimal or semantically simplest reproducer. Evaluation or wall-budget +exhaustion, an inconclusive candidate, cancellation and worker failure always +return a result without a minimality claim. + +## Exact predicates + +A baseline run must reproduce the requested failure before reduction starts. +Unit-test failures retain their assertion class; a wrong capture value cannot +turn into a missing capture, and a semantic comparison retains the exact sorted +set of baseline mismatch kinds. Rejected patterns and truncated match, +capture or replacement data are not accepted as semantic evidence. + +Timeout is a worker deadline outcome, never a no-match. An explicit timeout +predicate accepts only `WorkerRequestError("timeout")` at the configured fixed +deadline. Comparison timeout reduction additionally requires the same flavour +to time out and the peer flavour to complete with the same engine identity and +complete data. Crash, unreadable worker response, ordinary worker error and +cancellation have separate statuses. Crash minimization is unsupported and a +crash never satisfies a timeout predicate. + +For a `must-time-out` unit-test assertion, an ordinary completion is the test +failure being minimized; a timeout makes that assertion pass. The separate +**Exact engine timeout** target is used to minimize an observed timeout. + +## Bounds and progress + +- Subject: 1 MiB UTF-8, separate from the larger interactive editor cap. +- Candidate evaluations: at most 2,000, including the baseline. +- Aggregate syntax-setup and reduction wall time: at most 60 seconds. +- Candidate worker deadline: fixed for the complete run, at most 10 seconds. +- Retained accepted-transform history: 200 entries; totals remain exact. + +A candidate starts only when its full fixed deadline fits inside the remaining +aggregate wall budget. This prevents the aggregate deadline from being +misreported as an engine timeout. Progress exposes phase, evaluation and wall +budgets, accepted reductions, current scalar/byte sizes and the latest +observation. Cancellation terminates syntax and execution workers. + +The minimized subject can be copied or deliberately applied to the main editor. +Inputs and results stay in memory and are neither uploaded nor persisted. diff --git a/docs/PARSER_PROFILES.md b/docs/PARSER_PROFILES.md index b92f89c..a00cfde 100644 --- a/docs/PARSER_PROFILES.md +++ b/docs/PARSER_PROFILES.md @@ -7,10 +7,13 @@ community ``` It uses only reviewed open-source dependencies and builds without private -registry credentials. Version 0.1.0 parses ECMAScript with regexpp 4.12.2. +registry credentials. ECMAScript uses regexpp 4.12.2. PCRE2 uses an +application-owned lexical provider for partial explanations and the actual +bundled PCRE2 compiler for authoritative acceptance. The product deliberately does not implement or reference an `@r101/parser` profile. No commercial package alias, import, tarball, credential, licence key or private CI path exists. Additional flavour syntax will be implemented incrementally as open-source providers behind the same application-owned -interface. +interface. Partial coverage is exposed as metadata and never presented as a +complete parser. diff --git a/docs/PCRE2_NEXT_SLICES.md b/docs/PCRE2_NEXT_SLICES.md new file mode 100644 index 0000000..3258385 --- /dev/null +++ b/docs/PCRE2_NEXT_SLICES.md @@ -0,0 +1,66 @@ +# PCRE2 advanced slices + +This document records the reviewed boundaries for advanced PCRE2 work. A +feature is exposed only after its complete runtime and UI path passes the +stated gates. + +## Automatic-callout trace — implemented + +ABI version 3 and the dedicated trace worker now provide: + +- a new ABI operation separate from normal matching, compiled with + `PCRE2_AUTO_CALLOUT`; +- a fixed copied event record containing only reported callout number, pattern + byte position, next-item length, subject byte position, capture top/last and + bounded mark text; +- caller-supplied event and text buffers capped by both + `maximumTraceEvents` and `maximumTraceBytes`; +- explicit `eventsTruncated`, native status and last-complete-event fields; +- the existing match/depth/heap limits and supervisor timeout; +- cleanup identical to normal ABI calls. + +Tracing runs in its own worker and never replaces the normal result used by +Match, Replace, Tests, Corpus or Analysis. “Forward”, “same position” and +“apparent backtrack” are derived from adjacent reported positions only and +carry derived provenance. The viewer explicitly states that a callout stream +is not a complete account of every internal engine action. + +## ECMAScript versus PCRE2 comparison — implemented + +The comparison orchestrator issues two explicit requests with +`Promise.allSettled` through independently killable flavour workers. It does +not translate flags, patterns or replacement syntax. + +The comparison DTO retains: + +- each exact request identity, engine identity and independently complete or + timed-out result; +- match/capture differences aligned by editor UTF-16 ranges, while retaining + each native offset model; +- explicit “not comparable” states for syntax/compile failure, truncation, + timeout, cancellation, worker failure or flavour-only constructs; +- a shared wall-clock cancellation action that terminates both workers. + +The orchestrator remains outside `EngineSupervisor`, preserving the +single-engine lifecycle boundary. Shared patterns are sent unchanged and +explicit per-flavour variants remain user-owned. Agreement is labelled only as +evidence for the current subject. + +## PCRE2 C code generation — implemented + +The first reviewed target consumes a validated request snapshot and uses an +application-owned C17 template. It: + +- identifies exact PCRE2 10.47, 8-bit, UTF/UCP semantics and selected flags; +- emits pattern, subject and replacement independently as exact UTF-8 byte + arrays; +- includes match/depth/heap limits and error handling in generated C examples; +- states when a target binding cannot express an application-level `g` flag or + a Regex Tools output/result cap directly; +- ships deterministic golden match/replacement fixtures that compile and + execute against official PCRE2 10.47 before the target is advertised. + +No generated snippet may be used as the implementation of the browser runtime. +Other target languages remain unavailable until their own escaping and +toolchain gates pass. See +[`COMPARISON_AND_CODEGEN.md`](COMPARISON_AND_CODEGEN.md). diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index cbf1776..a8a56e6 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -10,6 +10,18 @@ browser, engine version, pattern and subject. The UI therefore reports observed elapsed time and exact runtime identity but makes no universal throughput or safety claim. +PCRE2 cold execution includes loading and instantiating the self-hosted +WebAssembly module inside its worker; warm requests reuse that instance. +Every request still compiles and frees its pattern so no unbounded compiled-code +cache can accumulate. Native match-step, depth and heap limits complement the +supervisor wall clock, and cancellation discards the complete worker heap. + +Automatic-callout tracing is deliberately excluded from every benchmark. It +compiles an independently instrumented PCRE2 pattern in a separate worker and +can materially change runtime work. The trace path retains at most 50,000 +complete 32-byte event records and 10 MiB total serialized event/mark bytes; +the viewer renders at most 2,000 retained events. + The production bundle separates syntax and execution workers. Interactive syntax/extraction trees and editor decoration sets render at most 2,000 items each and report both the rendered and actual totals. These are presentation @@ -62,5 +74,57 @@ representation against a byte budget before materializing or writing it. IndexedDB schema version 2 indexes `updatedAt`; latest-project loading opens one descending cursor and validates only that record instead of calling `getAll()`. -Formal cold/warm benchmarks, p95 statistics and growth charts are deferred to -the analysis milestone and will run in killable workers. +Corpus runs accept at most 256 documents, 16 MiB per document and 256 MiB of +aggregate UTF-8 input. Jobs run sequentially so only one engine request is +active at a time. A batch retains at most 100,000 matches and 64 MiB of applied +output, and stops after five minutes of aggregate wall time. Each document also +uses the selected manual timeout. Progress is reported after every document; +cancellation kills the current worker and labels all remaining documents. +Result tables contain summaries, not match/capture objects. ZIP export is +available only for an all-exact apply batch and is created asynchronously. + +Independent-line mode preflights at most 100,000 logical lines and processes +each as an isolated engine subject while preserving line separators for exact +apply output. Per document, corpus extraction retains at most 32 group summaries +(including the full match), three 160-UTF-16-unit samples per group and 100 +unique diagnostics. Output previews show 2,000 units. Content-free summary +exports are capped at 4 MiB; the applied archive is capped at 68 MiB in addition +to the 64 MiB uncompressed-output budget. + +ECMAScript analysis reports cold compilation separately from warm native +execution. The default benchmark uses three warm-up and 15 measured samples; +user caps are 100 warm-ups and 1,000 samples. It reports median and +nearest-rank p95 values plus separate timing and outcome/growth plots with the +exact runtime identity. Each sample defaults to a 2-second killable-worker +limit (10-second cap), and the aggregate analysis wall time is 30 seconds. + +Growth probes retain at most 24 steps, generate at most 1 MiB by default under +the central 16 MiB subject cap, and reject more than 10,000,000 repetitions +before string allocation. Replacement timing is skipped when match collection +is truncated or the estimated output exceeds 8 Mi UTF-16 units. Results are +observations for the chosen runtime and generated inputs, never a general +complexity proof. See [`ANALYSIS.md`](ANALYSIS.md). + +Generated-case synthesis defaults to 48 retained cases from at most 192 +candidates, 16 seeded-random variants, 64 KiB per candidate, 1 MiB aggregate +candidate bytes and 15 seconds of aggregate wall time. Every candidate then +uses a separate selected-engine request with a 500 ms default timeout. +Generation preflights UTF-8 size and repetition bounds, caps normalized-AST +traversal at 20,000 nodes / 512 levels and retains only bounded diagnostics. +Hard caps are documented in [`GENERATED_CASES.md`](GENERATED_CASES.md). + +Subject minimization accepts at most 1 MiB UTF-8, performs at most 2,000 real +engine candidate evaluations and stops after at most 60 seconds including +syntax setup. One fixed worker timeout of at most 10 seconds applies to every +candidate. A candidate starts only when that complete deadline fits in the +remaining aggregate budget. Accepted-transform history retains 200 entries; +evaluation and accepted totals remain exact. See +[`MINIMIZATION.md`](MINIMIZATION.md). + +Formatter validation uses two syntax supervisors and two actual-engine +supervisors so source/candidate outcomes cannot share compiled state. The +current subject/replacement snapshot always runs; at most 1,000 enabled tests +with the exact active identity are replayed. Test validation has a 60-second +aggregate wall budget, and a test starts only when its complete configured +worker timeout still fits. Preview details retain at most 10,000 transforms and +the UI renders the first 200. See [`FORMATTING.md`](FORMATTING.md). diff --git a/docs/PORTAL_REQUIREMENTS.md b/docs/PORTAL_REQUIREMENTS.md index c6f2530..c519ad1 100644 --- a/docs/PORTAL_REQUIREMENTS.md +++ b/docs/PORTAL_REQUIREMENTS.md @@ -1,6 +1,6 @@ # Portal requirements -Regex Tools 0.1.0 is a static nested-path-safe application. +Regex Tools is a static nested-path-safe application. Required delivery behavior: @@ -9,11 +9,17 @@ Required delivery behavior: revalidation/no-cache resources. - Hashed Vite assets may use immutable long-term caching. - CSP must allow `worker-src 'self' blob:`. -- No WebAssembly MIME rule or `wasm-unsafe-eval` is required by v0.1.0. +- `engines/pcre2/pcre2.wasm` must use `application/wasm`. +- `script-src` must permit self-hosted WebAssembly compilation, normally with + `'wasm-unsafe-eval'`; broad `'unsafe-eval'` is neither needed nor recommended. +- `engines/pcre2/pcre2.mjs`, metadata, checksums, licence and WASM should use + revalidation or a release-scoped immutable cache. They are not + content-hashed filenames and must never drift under one release identity. The Portal must consume the immutable release ZIP, verify its SHA-256, verify -manifest ID `de.add-ideas.regex-tools` and version `0.1.0`, and mount it at a +manifest ID `de.add-ideas.regex-tools` and version `0.2.0`, and mount it at a relative target such as `apps/regex/`. -When PCRE2 ships later, the smallest additional change will be -`application/wasm` delivery and `script-src 'self' 'wasm-unsafe-eval'`. +The package, manifest, tag, artifact URL and checksum must all use the v0.2.0 +identity. The historical v0.1.0 artifact coordinates remain immutable and must +not be overwritten or reused. diff --git a/docs/REFERENCE_IMPLEMENTATIONS.md b/docs/REFERENCE_IMPLEMENTATIONS.md index 8883c03..ab5fba8 100644 --- a/docs/REFERENCE_IMPLEMENTATIONS.md +++ b/docs/REFERENCE_IMPLEMENTATIONS.md @@ -8,7 +8,7 @@ Inspection date: 2026-07-24. | Toolbox SDK | `53c40a61ba1581246f65773fcbb1c1cfd31ac98e` / 0.2.2 | Apache-2.0 | Contract, shell and release conventions; package APIs used, no source adapted. | | Toolbox Portal | `a9c31c8986c40a0097966318e925083302e91e13` / 0.5.0 | AGPL-3.0-only | Assembly/lock conventions inspected; no source copied into the app. | | regexpp | 4.12.2; npm integrity `sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==` | MIT | Pinned ECMAScript syntax provider. Provider AST is normalized in a worker. | -| PCRE2 | signed tag 10.47; commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429` | BSD-3-Clause WITH PCRE2-exception | External technical spike only; not shipped or copied. | +| PCRE2 | signed tag 10.47; commit `f454e231fe5006dd7ff8f4693fd2b8eb94333429` | BSD-3-Clause WITH PCRE2-exception | Pinned offline build; generated verified WebAssembly pack is shipped, source is not vendored. | | ECMAScript specification | ; living specification inspected 2026-07-24 | ECMA terms | Semantics reference; no prose copied. | | regex101 | and public parser documentation | Product/commercial terms | Product reference only. No source, branding, explanations, reference prose or visual design copied. | | `recheck` | 4.5.0 | MIT | Metadata inspected; not installed or shipped. | @@ -17,5 +17,5 @@ Inspection date: 2026-07-24. | RegexLib | Website inspected | Redistribution licence not established | No fixture copied. | | RGXP.RU | Website inspected | Redistribution licence not established | No fixture copied. | -All v0.1.0 conformance cases are project-authored. See +All v0.1.0 and v0.2.0 conformance cases are project-authored. See `tests/fixtures/README.md`. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 7fa1c86..194ecba 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -18,6 +18,9 @@ on publication failure, replaces an existing exact version only with `--force`, and produces: ```text -release/regex-tools-0.1.0.zip -release/regex-tools-0.1.0.zip.sha256 +release/regex-tools-0.2.0.zip +release/regex-tools-0.2.0.zip.sha256 ``` + +The existing `release/regex-tools-0.1.0.zip` and matching sidecar are historical +immutable artifacts. Creating v0.2.0 must not replace, rename or remove them. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 9b2efd0..77ec32b 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -9,6 +9,16 @@ untrusted. - A terminated worker is never reused. - Pattern, subject, replacement/list template, match, capture and output sizes are bounded. +- PCRE2 adds native match-step, depth and heap caps. Its C ABI returns only + caller-owned bounded records and releases code, match data and match context + on every exit path. +- PCRE2 automatic-callout tracing is a separate ABI and worker path. It copies + only complete fixed records and bounded mark text, stops matching at the + 50,000-event or 10 MiB serialized trace cap, and is killed on timeout, + cancellation or crash. +- PCRE2 runs only in mandatory UTF/UCP mode. Exact UTF-8 byte boundaries are + normalized to editor UTF-16 offsets; lone surrogates are rejected instead of + being silently replaced by browser encoding. - Replacement templates are strings; executable callbacks are unavailable. - Replacement/list token decorations, rows, chips, diagnostics and list evaluation work have separate presentation/operation caps; incomplete list @@ -25,14 +35,51 @@ untrusted. - Unit-test assertion diagnostics never stringify complete large values and retain at most 2 KiB UTF-8 per case. Oversized replacement output is not copied into an exact current-result draft. +- Corpus file input is local, recognized UTF-8 text only. It is bounded before + decoding, rejects NUL/binary input, runs sequentially in a dedicated + cancellable supervisor and never modifies source files. +- Corpus JSON/CSV/NDJSON summaries never contain source, capture-sample or + applied-output content. CSV cells are formula-injection protected. Local UI + previews are explicitly bounded. Applied files require an explicit + content-export acknowledgement; names are normalized against archive path + traversal and collisions. Incomplete applied output cannot be downloaded or + included in the all-document ZIP, whose bytes have a separate hard limit. +- Corpus documents, contents, outputs and results are never placed in project + JSON or IndexedDB. Schema-v1 imports that attempt to inject corpus payloads + are rejected. +- ECMAScript growth analysis preflights repetition count and UTF-8 bytes before + allocating a generated string. The analysis worker is terminated on timeout, + cancellation or crash; replacement timing also preflights its output bound. + Analysis settings and results are neither persisted nor uploaded. +- Generated-case synthesis runs in its own killable worker under AST, depth, + attempt, repetition and UTF-8 byte caps. Labels come only from bounded + requests to the selected real engine worker; a wrong outcome, timeout, crash, + cancellation or compile rejection is never converted into a verified case. + Settings, discarded candidates and unselected results are ephemeral and + never uploaded. +- Imported generated-test provenance is accepted only for the exact supported + generator id/version and must agree with the test assertion outcome. Unknown + provenance fields and future versions are rejected; editing a case removes + its provenance. +- ECMAScript formatting consumes only exact accepted regexpp literal-token + ranges and never treats PCRE2 as ECMAScript. Source/candidate syntax and + execution use separate killable workers; compile failure, truncation, + timeout asymmetry, engine-identity drift, incomplete tests or any semantic + difference fails closed. Preview/results are ephemeral and apply requires an + explicit confirmation. +- Subject minimization rejects lone surrogates, caps input at 1 MiB UTF-8, + evaluation count at 2,000 and aggregate wall time at 60 seconds. Each + predicate check runs in the selected killable engine worker. Timeout, crash, + cancellation and worker error remain distinct; truncated results fail + closed. Subjects and results are ephemeral and never uploaded or persisted. - No backend, uploads, telemetry, remote corpus URL, CDN, remote font, `eval`, `Function`, arbitrary command line or unsafe HTML rendering exists. -Version 0.1.0 needs a CSP equivalent to: +The PCRE2-enabled build needs a CSP equivalent to: ```text default-src 'self'; -script-src 'self'; +script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; connect-src 'self'; img-src 'self' data:; @@ -44,8 +91,9 @@ base-uri 'none'; form-action 'none'; ``` -The Toolbox shell currently requires inline styles. No `unsafe-eval` or -`wasm-unsafe-eval` is required until an actual WebAssembly flavour ships. +The Toolbox shell currently requires inline styles. Corpus ZIP generation and +PCRE2 use self-hosted modules and local workers under the existing +`worker-src` policy. Broad `unsafe-eval` is not required. Report vulnerabilities privately to the repository owner before public issue details are posted. diff --git a/engines/pcre2/CMakeLists.txt b/engines/pcre2/CMakeLists.txt new file mode 100644 index 0000000..03d7312 --- /dev/null +++ b/engines/pcre2/CMakeLists.txt @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +cmake_minimum_required(VERSION 3.25) +project(regex_pcre2_bridge LANGUAGES C) + +if(NOT EMSCRIPTEN) + message(FATAL_ERROR "The staged PCRE2 pack must be built with Emscripten.") +endif() + +if(NOT DEFINED PCRE2_SOURCE_DIR OR NOT IS_DIRECTORY "${PCRE2_SOURCE_DIR}") + message(FATAL_ERROR "PCRE2_SOURCE_DIR must identify the verified PCRE2 source checkout.") +endif() + +if(NOT DEFINED REGEX_PCRE2_OUTPUT_DIR) + message(FATAL_ERROR "REGEX_PCRE2_OUTPUT_DIR must identify the private staging directory.") +endif() + +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE) +set(PCRE2_BUILD_PCRE2_8 ON CACHE BOOL "" FORCE) +set(PCRE2_BUILD_PCRE2_16 OFF CACHE BOOL "" FORCE) +set(PCRE2_BUILD_PCRE2_32 OFF CACHE BOOL "" FORCE) +set(PCRE2_BUILD_PCRE2GREP OFF CACHE BOOL "" FORCE) +set(PCRE2_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(PCRE2_DEBUG OFF CACHE STRING "" FORCE) +set(PCRE2_REBUILD_CHARTABLES OFF CACHE BOOL "" FORCE) +set(PCRE2_SHOW_REPORT OFF CACHE BOOL "" FORCE) +set(PCRE2_STATIC_PIC OFF CACHE BOOL "" FORCE) +set(PCRE2_SUPPORT_JIT OFF CACHE BOOL "" FORCE) +set(PCRE2_SUPPORT_UNICODE ON CACHE BOOL "" FORCE) + +add_subdirectory("${PCRE2_SOURCE_DIR}" pcre2-source EXCLUDE_FROM_ALL) + +add_executable(regex-pcre2-pack pcre2_bridge.c) +target_compile_features(regex-pcre2-pack PRIVATE c_std_11) +target_compile_definitions(regex-pcre2-pack PRIVATE PCRE2_CODE_UNIT_WIDTH=8) +target_compile_options( + regex-pcre2-pack + PRIVATE + -O3 + -fno-ident + -fvisibility=hidden +) +target_link_libraries(regex-pcre2-pack PRIVATE pcre2-8-static) +target_link_options( + regex-pcre2-pack + PRIVATE + -O3 + --no-entry + "SHELL:-s ALLOW_MEMORY_GROWTH=1" + "SHELL:-s ASSERTIONS=0" + "SHELL:-s ENVIRONMENT=web,worker,node" + "SHELL:-s EXPORT_ES6=1" + "SHELL:-s EXPORT_NAME=createRegexPcre2" + "SHELL:-s EXPORTED_FUNCTIONS=['_regex_pcre2_bridge_abi_version','_regex_pcre2_compile_probe','_regex_pcre2_config_flags','_regex_pcre2_error_message','_regex_pcre2_execute','_regex_pcre2_self_test','_regex_pcre2_substitute','_regex_pcre2_trace','_regex_pcre2_version','_malloc','_free']" + "SHELL:-s EXPORTED_RUNTIME_METHODS=['HEAPU8','UTF8ToString']" + "SHELL:-s FILESYSTEM=0" + "SHELL:-s INITIAL_MEMORY=16777216" + "SHELL:-s MALLOC=emmalloc" + "SHELL:-s MAXIMUM_MEMORY=268435456" + "SHELL:-s MODULARIZE=1" + "SHELL:-s NO_EXIT_RUNTIME=1" + "SHELL:-s STRICT=1" +) +set_target_properties( + regex-pcre2-pack + PROPERTIES + OUTPUT_NAME pcre2 + RUNTIME_OUTPUT_DIRECTORY "${REGEX_PCRE2_OUTPUT_DIR}" + SUFFIX ".mjs" +) diff --git a/engines/pcre2/README.md b/engines/pcre2/README.md new file mode 100644 index 0000000..78e0022 --- /dev/null +++ b/engines/pcre2/README.md @@ -0,0 +1,27 @@ +# PCRE2 engine bridge + +This directory contains the GPL-3.0-or-later application bridge and +deterministic build description for the bundled PCRE2 flavour. It deliberately +does not vendor the upstream PCRE2 source tree or generated binaries. + +ABI version 3 exposes: + +- exact engine identity and configuration; +- compile diagnostics with UTF-8 byte offsets; +- bounded non-overlapping matching with copied group-zero/capture records and + copied capture-name metadata; +- bounded native `pcre2_substitute()` semantics over the same retained match + sequence; +- bounded automatic-callout tracing from one native `pcre2_match()` invocation, + with copied event and mark data and an immediate native stop at either trace + cap; +- match-step, depth, heap, match-count, capture-row and output limits; +- no native pointer or compiled-code handle across a call boundary. + +All PCRE2 allocations are released before an ABI call returns. JavaScript owns +and frees every input/result buffer in `finally`, and the containing worker is +terminated on timeout, crash, cancellation or supersession. + +The explicit offline source/toolchain identity, build, verification and local +asset-install workflow is documented in +[`docs/ENGINE_BUILDS.md`](../../docs/ENGINE_BUILDS.md). diff --git a/engines/pcre2/pcre2_bridge.c b/engines/pcre2/pcre2_bridge.c new file mode 100644 index 0000000..f5595da --- /dev/null +++ b/engines/pcre2/pcre2_bridge.c @@ -0,0 +1,910 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#define PCRE2_CODE_UNIT_WIDTH 8 + +#include "pcre2_bridge.h" + +#include +#include +#include +#include + +#define REGEX_PCRE2_COMPILE_FLAGS \ + (REGEX_PCRE2_CASELESS | REGEX_PCRE2_MULTILINE | REGEX_PCRE2_DOTALL | \ + REGEX_PCRE2_EXTENDED | REGEX_PCRE2_UNGREEDY | REGEX_PCRE2_UTF | \ + REGEX_PCRE2_UCP | REGEX_PCRE2_DUPNAMES) +#define REGEX_PCRE2_ALL_FLAGS \ + (REGEX_PCRE2_COMPILE_FLAGS | REGEX_PCRE2_GLOBAL) + +_Static_assert(sizeof(regex_pcre2_compile_result) == 20, + "Compile result is part of ABI version 3."); +_Static_assert(sizeof(regex_pcre2_limits) == 20, + "Limits record is part of ABI version 3."); +_Static_assert(sizeof(regex_pcre2_match_record) == 16, + "Match record is part of ABI version 3."); +_Static_assert(sizeof(regex_pcre2_name_record) == 12, + "Name record is part of ABI version 3."); +_Static_assert(sizeof(regex_pcre2_run_result) == 60, + "Run result is part of ABI version 3."); +_Static_assert(sizeof(regex_pcre2_trace_limits) == 20, + "Trace limits are part of ABI version 3."); +_Static_assert(sizeof(regex_pcre2_trace_event) == 32, + "Trace event is part of ABI version 3."); +_Static_assert(sizeof(regex_pcre2_trace_result) == 52, + "Trace result is part of ABI version 3."); + +typedef struct regex_pcre2_run_buffers { + regex_pcre2_match_record *records; + uint32_t record_capacity; + regex_pcre2_name_record *name_records; + uint32_t name_record_capacity; + uint8_t *name_bytes; + uint32_t name_bytes_capacity; + uint8_t *output; + uint32_t output_capacity; +} regex_pcre2_run_buffers; + +typedef struct regex_pcre2_trace_state { + regex_pcre2_trace_event *events; + uint32_t event_capacity; + uint8_t *mark_bytes; + uint32_t mark_bytes_capacity; + uint32_t maximum_events; + uint32_t maximum_trace_bytes; + uint32_t event_count; + uint32_t total_event_count; + uint32_t mark_bytes_length; + uint32_t events_truncated; + uint32_t marks_truncated; +} regex_pcre2_trace_state; + +static const uint8_t empty_bytes[1] = {0}; + +static uint32_t to_pcre2_options(uint32_t application_flags) { + uint32_t options = 0; + if ((application_flags & REGEX_PCRE2_CASELESS) != 0) { + options |= PCRE2_CASELESS; + } + if ((application_flags & REGEX_PCRE2_MULTILINE) != 0) { + options |= PCRE2_MULTILINE; + } + if ((application_flags & REGEX_PCRE2_DOTALL) != 0) { + options |= PCRE2_DOTALL; + } + if ((application_flags & REGEX_PCRE2_EXTENDED) != 0) { + options |= PCRE2_EXTENDED; + } + if ((application_flags & REGEX_PCRE2_UNGREEDY) != 0) { + options |= PCRE2_UNGREEDY; + } + if ((application_flags & REGEX_PCRE2_UTF) != 0) { + options |= PCRE2_UTF; + } + if ((application_flags & REGEX_PCRE2_UCP) != 0) { + options |= PCRE2_UCP; + } + if ((application_flags & REGEX_PCRE2_DUPNAMES) != 0) { + options |= PCRE2_DUPNAMES; + } + return options; +} + +static int32_t fail_compile(regex_pcre2_compile_result *result, + int32_t status) { + if (result != NULL) { + memset(result, 0, sizeof(*result)); + result->status = status; + } + return status; +} + +static int32_t fail_run(regex_pcre2_run_result *result, + int32_t status, + uint32_t phase, + uint32_t offset) { + if (result != NULL) { + memset(result, 0, sizeof(*result)); + result->status = status; + result->error_phase = phase; + result->error_offset = offset; + } + return status; +} + +static int32_t fail_trace(regex_pcre2_trace_result *result, + int32_t status, + uint32_t phase, + uint32_t offset) { + if (result != NULL) { + memset(result, 0, sizeof(*result)); + result->status = status; + result->error_phase = phase; + result->error_offset = offset; + result->last_complete_event = UINT32_MAX; + } + return status; +} + +static int input_pointer_valid(const uint8_t *value, uint32_t length) { + return value != NULL || length == 0; +} + +static const uint8_t *nonnull_input(const uint8_t *value) { + return value == NULL ? empty_bytes : value; +} + +static int limits_valid(const regex_pcre2_limits *limits) { + return limits != NULL && limits->maximum_matches > 0 && + limits->maximum_matches <= REGEX_PCRE2_MAX_MATCHES && + limits->maximum_capture_rows > 0 && + limits->maximum_capture_rows <= REGEX_PCRE2_MAX_CAPTURE_ROWS && + limits->match_limit > 0 && + limits->match_limit <= REGEX_PCRE2_MAX_MATCH_LIMIT && + limits->depth_limit > 0 && + limits->depth_limit <= REGEX_PCRE2_MAX_DEPTH_LIMIT && + limits->heap_limit_kib > 0 && + limits->heap_limit_kib <= REGEX_PCRE2_MAX_HEAP_LIMIT_KIB; +} + +static int buffers_valid(const regex_pcre2_run_buffers *buffers, + int substitution) { + if (buffers == NULL || + (buffers->records == NULL && buffers->record_capacity != 0) || + (buffers->name_records == NULL && + buffers->name_record_capacity != 0) || + (buffers->name_bytes == NULL && buffers->name_bytes_capacity != 0)) { + return 0; + } + /* + * PCRE2 writes a trailing NUL even when the semantic output capacity is + * zero, so the bridge contract always requires the advertised +1 byte. + */ + if (substitution && buffers->output == NULL) { + return 0; + } + return 1; +} + +static uint32_t bounded_offset(PCRE2_SIZE value) { + return value > UINT32_MAX ? UINT32_MAX : (uint32_t)value; +} + +static int32_t copy_names(const pcre2_code *code, + regex_pcre2_run_buffers *buffers, + regex_pcre2_run_result *result) { + uint32_t entry_size = 0; + PCRE2_SPTR table = NULL; + uint32_t index = 0; + uint32_t name_offset = 0; + + if (result->name_count == 0) { + return 0; + } + if (pcre2_pattern_info(code, PCRE2_INFO_NAMEENTRYSIZE, &entry_size) != 0 || + pcre2_pattern_info(code, PCRE2_INFO_NAMETABLE, &table) != 0 || + entry_size < 3 || table == NULL) { + return REGEX_PCRE2_ERROR_CONFIG; + } + for (index = 0; index < result->name_count; index += 1) { + const uint8_t *entry = table + (index * entry_size); + uint32_t group_number = + ((uint32_t)entry[0] << 8) | (uint32_t)entry[1]; + uint32_t maximum_name = entry_size - 2; + uint32_t name_length = 0; + while (name_length < maximum_name && entry[2 + name_length] != 0) { + name_length += 1; + } + if (result->name_record_count >= buffers->name_record_capacity || + name_length > buffers->name_bytes_capacity - name_offset) { + result->names_truncated = 1; + break; + } + buffers->name_records[result->name_record_count].group_number = + group_number; + buffers->name_records[result->name_record_count].name_offset = + name_offset; + buffers->name_records[result->name_record_count].name_length = + name_length; + if (name_length > 0) { + memcpy(buffers->name_bytes + name_offset, entry + 2, name_length); + } + result->name_record_count += 1; + name_offset += name_length; + } + result->name_bytes_length = name_offset; + return 0; +} + +static int append_utf8_prefix(regex_pcre2_run_buffers *buffers, + regex_pcre2_run_result *result, + const uint8_t *source, + uint32_t length) { + uint32_t remaining = buffers->output_capacity - result->output_length; + uint32_t copied = length < remaining ? length : remaining; + + if (copied < length) { + while (copied > 0 && (source[copied] & 0xc0u) == 0x80u) { + copied -= 1; + } + result->output_truncated = 1; + } + if (copied > 0) { + memcpy(buffers->output + result->output_length, source, copied); + result->output_length += copied; + } + return copied == length; +} + +static int32_t configure_match_context_values(pcre2_match_context *context, + uint32_t match_limit, + uint32_t depth_limit, + uint32_t heap_limit_kib) { + if (pcre2_set_match_limit(context, match_limit) != 0 || + pcre2_set_depth_limit(context, depth_limit) != 0 || + pcre2_set_heap_limit(context, heap_limit_kib) != 0) { + return REGEX_PCRE2_ERROR_CONFIG; + } + return 0; +} + +static int32_t configure_match_context(pcre2_match_context *context, + const regex_pcre2_limits *limits) { + return configure_match_context_values( + context, limits->match_limit, limits->depth_limit, + limits->heap_limit_kib); +} + +static int trace_limits_valid(const regex_pcre2_trace_limits *limits) { + return limits != NULL && limits->maximum_events > 0 && + limits->maximum_events <= REGEX_PCRE2_MAX_TRACE_EVENTS && + limits->maximum_trace_bytes >= sizeof(regex_pcre2_trace_event) && + limits->maximum_trace_bytes <= REGEX_PCRE2_MAX_TRACE_BYTES && + limits->match_limit > 0 && + limits->match_limit <= REGEX_PCRE2_MAX_MATCH_LIMIT && + limits->depth_limit > 0 && + limits->depth_limit <= REGEX_PCRE2_MAX_DEPTH_LIMIT && + limits->heap_limit_kib > 0 && + limits->heap_limit_kib <= REGEX_PCRE2_MAX_HEAP_LIMIT_KIB; +} + +static uint32_t bounded_mark_length(PCRE2_SPTR mark, uint32_t *truncated) { + uint32_t length = 0; + if (mark == NULL) { + return 0; + } + while (length < REGEX_PCRE2_MAX_TRACE_MARK_BYTES && mark[length] != 0) { + length += 1; + } + if (length == REGEX_PCRE2_MAX_TRACE_MARK_BYTES && mark[length] != 0) { + *truncated = 1; + while (length > 0 && (mark[length] & 0xc0u) == 0x80u) { + length -= 1; + } + } + return length; +} + +static int trace_callout(pcre2_callout_block *block, void *data) { + regex_pcre2_trace_state *state = (regex_pcre2_trace_state *)data; + regex_pcre2_trace_event *event = NULL; + uint32_t mark_length = 0; + uint32_t mark_was_truncated = 0; + uint64_t required_trace_bytes = 0; + + if (block == NULL || state == NULL) { + return PCRE2_ERROR_CALLOUT; + } + if (state->total_event_count < UINT32_MAX) { + state->total_event_count += 1; + } + mark_length = bounded_mark_length(block->mark, &mark_was_truncated); + required_trace_bytes = + ((uint64_t)state->event_count + 1u) * + sizeof(regex_pcre2_trace_event) + + state->mark_bytes_length + mark_length; + if (state->event_count >= state->maximum_events || + state->event_count >= state->event_capacity || + required_trace_bytes > state->maximum_trace_bytes || + mark_length > state->mark_bytes_capacity - state->mark_bytes_length) { + state->events_truncated = 1; + return PCRE2_ERROR_CALLOUT; + } + + event = &state->events[state->event_count]; + event->callout_number = block->callout_number; + event->pattern_position_byte = bounded_offset(block->pattern_position); + event->next_item_length_byte = bounded_offset(block->next_item_length); + event->subject_position_byte = bounded_offset(block->current_position); + event->capture_top = block->capture_top; + event->capture_last = block->capture_last; + event->mark_offset = state->mark_bytes_length; + event->mark_length = mark_length; + if (mark_length > 0) { + memcpy(state->mark_bytes + state->mark_bytes_length, block->mark, + mark_length); + state->mark_bytes_length += mark_length; + } + state->marks_truncated |= mark_was_truncated; + state->event_count += 1; + return 0; +} + +static int32_t run_bounded(const uint8_t *pattern, + uint32_t pattern_length, + const uint8_t *subject, + uint32_t subject_length, + const uint8_t *replacement, + uint32_t replacement_length, + uint32_t application_flags, + const regex_pcre2_limits *limits, + regex_pcre2_run_buffers *buffers, + int substitution, + regex_pcre2_run_result *result) { + pcre2_code *code = NULL; + pcre2_match_data *match_data = NULL; + pcre2_match_context *match_context = NULL; + int compile_error = 0; + PCRE2_SIZE compile_offset = 0; + uint32_t capture_count = 0; + uint32_t name_count = 0; + uint32_t options = 0; + uint32_t search_offset = 0; + uint32_t match_options = 0; + uint32_t copied_until = 0; + uint32_t capture_rows = 0; + int32_t status = 0; + const uint8_t *safe_pattern = nonnull_input(pattern); + const uint8_t *safe_subject = nonnull_input(subject); + const uint8_t *safe_replacement = nonnull_input(replacement); + + if (result == NULL || !input_pointer_valid(pattern, pattern_length) || + !input_pointer_valid(subject, subject_length) || + (substitution && + !input_pointer_valid(replacement, replacement_length)) || + !buffers_valid(buffers, substitution)) { + return fail_run(result, REGEX_PCRE2_ERROR_INVALID_ARGUMENT, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + memset(result, 0, sizeof(*result)); + + if ((application_flags & ~REGEX_PCRE2_ALL_FLAGS) != 0) { + return fail_run(result, REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + if (pattern_length > REGEX_PCRE2_MAX_PATTERN_BYTES) { + return fail_run(result, REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + if (subject_length > REGEX_PCRE2_MAX_SUBJECT_BYTES) { + return fail_run(result, REGEX_PCRE2_ERROR_SUBJECT_TOO_LARGE, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + if (substitution && + replacement_length > REGEX_PCRE2_MAX_REPLACEMENT_BYTES) { + return fail_run(result, REGEX_PCRE2_ERROR_REPLACEMENT_TOO_LARGE, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + if (!limits_valid(limits)) { + return fail_run(result, REGEX_PCRE2_ERROR_LIMIT_OUT_OF_RANGE, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + + options = to_pcre2_options(application_flags); + result->effective_options = options; + code = pcre2_compile(safe_pattern, pattern_length, options, &compile_error, + &compile_offset, NULL); + if (code == NULL) { + status = compile_error; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_COMPILE; + result->error_offset = bounded_offset(compile_offset); + goto cleanup; + } + if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0 || + pcre2_pattern_info(code, PCRE2_INFO_NAMECOUNT, &name_count) != 0) { + status = REGEX_PCRE2_ERROR_CONFIG; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + result->capture_count = capture_count; + result->name_count = name_count; + if (capture_count > REGEX_PCRE2_MAX_CAPTURE_GROUPS) { + status = REGEX_PCRE2_ERROR_CAPTURE_LIMIT; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + status = copy_names(code, buffers, result); + if (status != 0) { + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + + match_data = pcre2_match_data_create_from_pattern(code, NULL); + match_context = pcre2_match_context_create(NULL); + if (match_data == NULL || match_context == NULL) { + status = PCRE2_ERROR_NOMEMORY; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + status = configure_match_context(match_context, limits); + if (status != 0) { + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + + while (search_offset <= subject_length) { + int match_status = + pcre2_match(code, safe_subject, subject_length, search_offset, + match_options, match_data, match_context); + PCRE2_SIZE *ovector = NULL; + uint32_t required_records = capture_count + 1; + uint32_t required_capture_rows = capture_count == 0 ? 1 : capture_count; + uint32_t group = 0; + uint32_t start = 0; + uint32_t end = 0; + + if (match_status == PCRE2_ERROR_NOMATCH) { + break; + } + if (match_status < 0) { + status = match_status; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_MATCH; + result->error_offset = search_offset; + goto cleanup; + } + if (match_status == 0) { + status = REGEX_PCRE2_ERROR_CONFIG; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + + ovector = pcre2_get_ovector_pointer(match_data); + start = bounded_offset(ovector[0]); + end = bounded_offset(ovector[1]); + if (start > end || end > subject_length) { + status = REGEX_PCRE2_ERROR_CONFIG; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + if (result->match_count >= limits->maximum_matches || + capture_rows + required_capture_rows > + limits->maximum_capture_rows || + required_records > buffers->record_capacity - result->record_count) { + result->results_truncated = 1; + break; + } + + for (group = 0; group <= capture_count; group += 1) { + regex_pcre2_match_record *record = + &buffers->records[result->record_count]; + PCRE2_SIZE native_start = ovector[group * 2]; + PCRE2_SIZE native_end = ovector[group * 2 + 1]; + record->match_number = result->match_count + 1; + record->group_number = group; + if (native_start == PCRE2_UNSET || native_end == PCRE2_UNSET) { + record->start_byte = REGEX_PCRE2_UNSET_OFFSET; + record->end_byte = REGEX_PCRE2_UNSET_OFFSET; + } else { + record->start_byte = bounded_offset(native_start); + record->end_byte = bounded_offset(native_end); + } + result->record_count += 1; + } + result->match_count += 1; + capture_rows += required_capture_rows; + + if (substitution && result->output_truncated == 0) { + PCRE2_SIZE replacement_output_length = + buffers->output_capacity - result->output_length + 1; + int substitute_status = 0; + if (!append_utf8_prefix(buffers, result, safe_subject + copied_until, + start - copied_until)) { + break; + } + replacement_output_length = + buffers->output_capacity - result->output_length + 1; + substitute_status = pcre2_substitute( + code, safe_subject, subject_length, search_offset, + match_options | PCRE2_SUBSTITUTE_MATCHED | + PCRE2_SUBSTITUTE_REPLACEMENT_ONLY | + PCRE2_SUBSTITUTE_UNSET_EMPTY, + match_data, match_context, safe_replacement, replacement_length, + buffers->output + result->output_length, &replacement_output_length); + if (substitute_status == PCRE2_ERROR_NOMEMORY) { + result->output_truncated = 1; + break; + } + if (substitute_status < 0) { + status = substitute_status; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_SUBSTITUTE; + result->error_offset = bounded_offset(replacement_output_length); + goto cleanup; + } + result->output_length += bounded_offset(replacement_output_length); + result->substitution_count += (uint32_t)substitute_status; + copied_until = end; + } + + if ((application_flags & REGEX_PCRE2_GLOBAL) == 0) { + break; + } + { + PCRE2_SIZE next_offset = search_offset; + uint32_t next_options = 0; + if (!pcre2_next_match(match_data, &next_offset, &next_options)) { + break; + } + search_offset = bounded_offset(next_offset); + match_options = next_options; + } + } + + if (substitution && result->output_truncated == 0) { + (void)append_utf8_prefix(buffers, result, safe_subject + copied_until, + subject_length - copied_until); + } + +cleanup: + if (match_context != NULL) { + pcre2_match_context_free(match_context); + } + if (match_data != NULL) { + pcre2_match_data_free(match_data); + } + if (code != NULL) { + pcre2_code_free(code); + } + return status; +} + +uint32_t regex_pcre2_bridge_abi_version(void) { + return REGEX_PCRE2_BRIDGE_ABI_VERSION; +} + +uint32_t regex_pcre2_config_flags(void) { + uint32_t unicode = 0; + uint32_t jit = 0; + uint32_t flags = 0; + + if (pcre2_config(PCRE2_CONFIG_UNICODE, &unicode) != 0 || + pcre2_config(PCRE2_CONFIG_JIT, &jit) != 0) { + return 0; + } + if (unicode != 0) { + flags |= REGEX_PCRE2_CONFIG_UNICODE; + } + if (jit != 0) { + flags |= REGEX_PCRE2_CONFIG_JIT; + } + return flags; +} + +const uint8_t *regex_pcre2_version(void) { + static uint8_t version[64]; + if (pcre2_config(PCRE2_CONFIG_VERSION, version) <= 0) { + version[0] = '\0'; + } + return version; +} + +int32_t regex_pcre2_error_message(int32_t error_code, + uint8_t *output, + uint32_t output_capacity) { + int status = 0; + if (output == NULL || output_capacity == 0) { + return REGEX_PCRE2_ERROR_INVALID_ARGUMENT; + } + status = pcre2_get_error_message(error_code, output, output_capacity); + if (status < 0) { + output[0] = '\0'; + } + return status; +} + +int32_t regex_pcre2_compile_probe(const uint8_t *pattern, + uint32_t pattern_length, + uint32_t application_flags, + regex_pcre2_compile_result *result) { + pcre2_code *code = NULL; + int error_code = 0; + PCRE2_SIZE error_offset = 0; + uint32_t capture_count = 0; + uint32_t name_count = 0; + uint32_t options = 0; + + if (result == NULL || !input_pointer_valid(pattern, pattern_length)) { + return fail_compile(result, REGEX_PCRE2_ERROR_INVALID_ARGUMENT); + } + memset(result, 0, sizeof(*result)); + + if ((application_flags & ~REGEX_PCRE2_COMPILE_FLAGS) != 0) { + return fail_compile(result, REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS); + } + if (pattern_length > REGEX_PCRE2_MAX_PATTERN_BYTES) { + return fail_compile(result, REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE); + } + + options = to_pcre2_options(application_flags); + result->effective_options = options; + code = pcre2_compile(nonnull_input(pattern), pattern_length, options, + &error_code, &error_offset, NULL); + if (code == NULL) { + result->status = error_code; + result->error_offset = bounded_offset(error_offset); + return error_code; + } + + if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0 || + pcre2_pattern_info(code, PCRE2_INFO_NAMECOUNT, &name_count) != 0) { + pcre2_code_free(code); + return fail_compile(result, REGEX_PCRE2_ERROR_CONFIG); + } + + result->capture_count = capture_count; + result->name_count = name_count; + pcre2_code_free(code); + return 0; +} + +int32_t regex_pcre2_execute( + const uint8_t *pattern, + uint32_t pattern_length, + const uint8_t *subject, + uint32_t subject_length, + uint32_t application_flags, + const regex_pcre2_limits *limits, + regex_pcre2_match_record *records, + uint32_t record_capacity, + regex_pcre2_name_record *name_records, + uint32_t name_record_capacity, + uint8_t *name_bytes, + uint32_t name_bytes_capacity, + regex_pcre2_run_result *result) { + regex_pcre2_run_buffers buffers = { + records, record_capacity, name_records, name_record_capacity, + name_bytes, name_bytes_capacity, NULL, 0}; + return run_bounded(pattern, pattern_length, subject, subject_length, NULL, 0, + application_flags, limits, &buffers, 0, result); +} + +int32_t regex_pcre2_substitute( + const uint8_t *pattern, + uint32_t pattern_length, + const uint8_t *subject, + uint32_t subject_length, + const uint8_t *replacement, + uint32_t replacement_length, + uint32_t application_flags, + const regex_pcre2_limits *limits, + regex_pcre2_match_record *records, + uint32_t record_capacity, + regex_pcre2_name_record *name_records, + uint32_t name_record_capacity, + uint8_t *name_bytes, + uint32_t name_bytes_capacity, + uint8_t *output, + uint32_t output_capacity, + regex_pcre2_run_result *result) { + regex_pcre2_run_buffers buffers = { + records, record_capacity, name_records, name_record_capacity, + name_bytes, name_bytes_capacity, output, output_capacity}; + return run_bounded(pattern, pattern_length, subject, subject_length, + replacement, replacement_length, application_flags, + limits, &buffers, 1, result); +} + +int32_t regex_pcre2_trace( + const uint8_t *pattern, + uint32_t pattern_length, + const uint8_t *subject, + uint32_t subject_length, + uint32_t application_flags, + const regex_pcre2_trace_limits *limits, + regex_pcre2_trace_event *events, + uint32_t event_capacity, + uint8_t *mark_bytes, + uint32_t mark_bytes_capacity, + regex_pcre2_trace_result *result) { + pcre2_code *code = NULL; + pcre2_match_data *match_data = NULL; + pcre2_match_context *match_context = NULL; + regex_pcre2_trace_state state; + int compile_error = 0; + PCRE2_SIZE compile_offset = 0; + uint32_t capture_count = 0; + uint32_t options = 0; + int32_t match_status = 0; + int32_t status = 0; + + if (result == NULL || !input_pointer_valid(pattern, pattern_length) || + !input_pointer_valid(subject, subject_length) || events == NULL || + event_capacity == 0 || + (mark_bytes == NULL && mark_bytes_capacity != 0)) { + return fail_trace(result, REGEX_PCRE2_ERROR_INVALID_ARGUMENT, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + memset(result, 0, sizeof(*result)); + result->last_complete_event = UINT32_MAX; + if ((application_flags & ~REGEX_PCRE2_ALL_FLAGS) != 0) { + return fail_trace(result, REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + if (pattern_length > REGEX_PCRE2_MAX_PATTERN_BYTES) { + return fail_trace(result, REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + if (subject_length > REGEX_PCRE2_MAX_SUBJECT_BYTES) { + return fail_trace(result, REGEX_PCRE2_ERROR_SUBJECT_TOO_LARGE, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + if (!trace_limits_valid(limits) || + event_capacity < limits->maximum_events || + event_capacity > REGEX_PCRE2_MAX_TRACE_EVENTS || + mark_bytes_capacity > REGEX_PCRE2_MAX_TRACE_BYTES) { + return fail_trace(result, REGEX_PCRE2_ERROR_LIMIT_OUT_OF_RANGE, + REGEX_PCRE2_PHASE_BRIDGE, 0); + } + + options = to_pcre2_options(application_flags) | PCRE2_AUTO_CALLOUT; + result->effective_options = options; + code = pcre2_compile(nonnull_input(pattern), pattern_length, options, + &compile_error, &compile_offset, NULL); + if (code == NULL) { + status = compile_error; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_COMPILE; + result->error_offset = bounded_offset(compile_offset); + goto cleanup; + } + if (pcre2_pattern_info(code, PCRE2_INFO_CAPTURECOUNT, &capture_count) != 0) { + status = REGEX_PCRE2_ERROR_CONFIG; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + if (capture_count > REGEX_PCRE2_MAX_CAPTURE_GROUPS) { + status = REGEX_PCRE2_ERROR_CAPTURE_LIMIT; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + + match_data = pcre2_match_data_create_from_pattern(code, NULL); + match_context = pcre2_match_context_create(NULL); + if (match_data == NULL || match_context == NULL) { + status = PCRE2_ERROR_NOMEMORY; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + memset(&state, 0, sizeof(state)); + state.events = events; + state.event_capacity = event_capacity; + state.mark_bytes = mark_bytes; + state.mark_bytes_capacity = mark_bytes_capacity; + state.maximum_events = limits->maximum_events; + state.maximum_trace_bytes = limits->maximum_trace_bytes; + status = configure_match_context_values( + match_context, limits->match_limit, limits->depth_limit, + limits->heap_limit_kib); + if (status != 0 || + pcre2_set_callout(match_context, trace_callout, &state) != 0) { + status = REGEX_PCRE2_ERROR_CONFIG; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_BRIDGE; + goto cleanup; + } + /* + * The global flag is intentionally application-level for normal execution. + * A trace represents one exact pcre2_match() invocation. + */ + match_status = pcre2_match(code, nonnull_input(subject), subject_length, 0, 0, + match_data, match_context); + result->native_match_status = match_status; + result->event_count = state.event_count; + result->total_event_count = state.total_event_count; + result->mark_bytes_length = state.mark_bytes_length; + result->trace_bytes_length = + state.event_count * (uint32_t)sizeof(regex_pcre2_trace_event) + + state.mark_bytes_length; + result->events_truncated = state.events_truncated; + result->marks_truncated = state.marks_truncated; + result->last_complete_event = + state.event_count == 0 ? UINT32_MAX : state.event_count - 1; + if (match_status >= 0) { + result->matched = 1; + } else if (match_status == PCRE2_ERROR_NOMATCH || + (match_status == PCRE2_ERROR_CALLOUT && + state.events_truncated != 0)) { + status = 0; + } else { + status = match_status; + result->status = status; + result->error_phase = REGEX_PCRE2_PHASE_MATCH; + if (state.event_count > 0) { + result->error_offset = + events[state.event_count - 1].subject_position_byte; + } + } + +cleanup: + if (match_context != NULL) { + pcre2_match_context_free(match_context); + } + if (match_data != NULL) { + pcre2_match_data_free(match_data); + } + if (code != NULL) { + pcre2_code_free(code); + } + return status; +} + +int32_t regex_pcre2_self_test(void) { + static const uint8_t pattern[] = "(?\\p{L}+)-(?\\d+)"; + static const uint8_t subject[] = "x Grüße-42 y"; + static const uint8_t replacement[] = "$:$"; + static const uint8_t trace_pattern[] = "a+b"; + static const uint8_t trace_subject[] = "aaab"; + regex_pcre2_compile_result compile_result; + regex_pcre2_run_result run_result; + regex_pcre2_trace_result trace_result; + regex_pcre2_limits limits = {10, 100, 1000000, 1000, 32768}; + regex_pcre2_trace_limits trace_limits = {100, 4096, 1000000, 1000, + 32768}; + regex_pcre2_match_record records[3]; + regex_pcre2_name_record names[2]; + regex_pcre2_trace_event trace_events[100]; + uint8_t name_bytes[64]; + uint8_t trace_marks[64]; + uint8_t output[65]; + int32_t status = regex_pcre2_compile_probe( + pattern, (uint32_t)(sizeof(pattern) - 1), + REGEX_PCRE2_UTF | REGEX_PCRE2_UCP, &compile_result); + + if (status != 0 || compile_result.capture_count != 2 || + compile_result.name_count != 2) { + return 1; + } + status = regex_pcre2_substitute( + pattern, (uint32_t)(sizeof(pattern) - 1), subject, + (uint32_t)(sizeof(subject) - 1), replacement, + (uint32_t)(sizeof(replacement) - 1), + REGEX_PCRE2_UTF | REGEX_PCRE2_UCP, &limits, records, 3, names, 2, + name_bytes, sizeof(name_bytes), output, sizeof(output) - 1, &run_result); + if (status != 0 || run_result.match_count != 1 || + run_result.record_count != 3 || run_result.substitution_count != 1 || + run_result.output_truncated != 0 || + run_result.output_length != sizeof("x 42:Grüße y") - 1 || + memcmp(output, "x 42:Grüße y", sizeof("x 42:Grüße y") - 1) != 0) { + return 2; + } + status = regex_pcre2_trace( + trace_pattern, (uint32_t)(sizeof(trace_pattern) - 1), trace_subject, + (uint32_t)(sizeof(trace_subject) - 1), + REGEX_PCRE2_UTF | REGEX_PCRE2_UCP, &trace_limits, trace_events, 100, + trace_marks, sizeof(trace_marks), &trace_result); + if (status != 0 || trace_result.status != 0 || + trace_result.native_match_status <= 0 || trace_result.matched != 1 || + trace_result.event_count == 0 || trace_result.events_truncated != 0 || + trace_result.last_complete_event != trace_result.event_count - 1) { + return 3; + } + if (regex_pcre2_bridge_abi_version() != REGEX_PCRE2_BRIDGE_ABI_VERSION || + regex_pcre2_config_flags() != REGEX_PCRE2_CONFIG_UNICODE) { + return 4; + } + if (strcmp((const char *)regex_pcre2_version(), "10.47 2025-10-21") != 0) { + return 5; + } + return 0; +} diff --git a/engines/pcre2/pcre2_bridge.h b/engines/pcre2/pcre2_bridge.h new file mode 100644 index 0000000..8f6ed64 --- /dev/null +++ b/engines/pcre2/pcre2_bridge.h @@ -0,0 +1,236 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#ifndef REGEX_TOOLS_PCRE2_BRIDGE_H +#define REGEX_TOOLS_PCRE2_BRIDGE_H + +#include + +#define REGEX_PCRE2_BRIDGE_ABI_VERSION 3u +#define REGEX_PCRE2_MAX_PATTERN_BYTES 1048576u +#define REGEX_PCRE2_MAX_SUBJECT_BYTES 16777216u +#define REGEX_PCRE2_MAX_REPLACEMENT_BYTES 262144u +#define REGEX_PCRE2_MAX_MATCHES 10000u +#define REGEX_PCRE2_MAX_CAPTURE_ROWS 100000u +#define REGEX_PCRE2_MAX_CAPTURE_GROUPS 1000u +#define REGEX_PCRE2_MAX_MATCH_LIMIT 100000000u +#define REGEX_PCRE2_MAX_DEPTH_LIMIT 100000u +#define REGEX_PCRE2_MAX_HEAP_LIMIT_KIB 131072u +#define REGEX_PCRE2_MAX_TRACE_EVENTS 50000u +#define REGEX_PCRE2_MAX_TRACE_BYTES 10485760u +#define REGEX_PCRE2_MAX_TRACE_MARK_BYTES 1024u +#define REGEX_PCRE2_UNSET_OFFSET UINT32_MAX + +enum regex_pcre2_flag { + REGEX_PCRE2_CASELESS = 1u << 0, + REGEX_PCRE2_MULTILINE = 1u << 1, + REGEX_PCRE2_DOTALL = 1u << 2, + REGEX_PCRE2_EXTENDED = 1u << 3, + REGEX_PCRE2_UNGREEDY = 1u << 4, + REGEX_PCRE2_UTF = 1u << 5, + REGEX_PCRE2_UCP = 1u << 6, + REGEX_PCRE2_DUPNAMES = 1u << 7, + REGEX_PCRE2_GLOBAL = 1u << 8 +}; + +enum regex_pcre2_config_flag { + REGEX_PCRE2_CONFIG_UNICODE = 1u << 0, + REGEX_PCRE2_CONFIG_JIT = 1u << 1 +}; + +enum regex_pcre2_bridge_error { + REGEX_PCRE2_ERROR_INVALID_ARGUMENT = -10001, + REGEX_PCRE2_ERROR_UNSUPPORTED_FLAGS = -10002, + REGEX_PCRE2_ERROR_PATTERN_TOO_LARGE = -10003, + REGEX_PCRE2_ERROR_CONFIG = -10004, + REGEX_PCRE2_ERROR_SUBJECT_TOO_LARGE = -10005, + REGEX_PCRE2_ERROR_REPLACEMENT_TOO_LARGE = -10006, + REGEX_PCRE2_ERROR_LIMIT_OUT_OF_RANGE = -10007, + REGEX_PCRE2_ERROR_CAPTURE_LIMIT = -10008, + REGEX_PCRE2_ERROR_OUTPUT_BUFFER = -10009 +}; + +enum regex_pcre2_error_phase { + REGEX_PCRE2_PHASE_NONE = 0, + REGEX_PCRE2_PHASE_BRIDGE = 1, + REGEX_PCRE2_PHASE_COMPILE = 2, + REGEX_PCRE2_PHASE_MATCH = 3, + REGEX_PCRE2_PHASE_SUBSTITUTE = 4 +}; + +/* + * This 20-byte, five-u32 record is retained for deterministic compile-only + * build verification. Runtime callers should use the bounded APIs below. + */ +typedef struct regex_pcre2_compile_result { + int32_t status; + uint32_t error_offset; + uint32_t capture_count; + uint32_t name_count; + uint32_t effective_options; +} regex_pcre2_compile_result; + +/* All values are required and validated against the bridge hard maxima. */ +typedef struct regex_pcre2_limits { + uint32_t maximum_matches; + uint32_t maximum_capture_rows; + uint32_t match_limit; + uint32_t depth_limit; + uint32_t heap_limit_kib; +} regex_pcre2_limits; + +/* + * One record is emitted for group zero and every capture group of each + * retained match. Unset captures use REGEX_PCRE2_UNSET_OFFSET for both bounds. + */ +typedef struct regex_pcre2_match_record { + uint32_t match_number; + uint32_t group_number; + uint32_t start_byte; + uint32_t end_byte; +} regex_pcre2_match_record; + +/* Names are UTF-8 slices into the caller-owned name byte buffer. */ +typedef struct regex_pcre2_name_record { + uint32_t group_number; + uint32_t name_offset; + uint32_t name_length; +} regex_pcre2_name_record; + +/* + * Trace collection has an independent bound from match materialization. The + * byte cap covers the fixed event records plus copied mark bytes. + */ +typedef struct regex_pcre2_trace_limits { + uint32_t maximum_events; + uint32_t maximum_trace_bytes; + uint32_t match_limit; + uint32_t depth_limit; + uint32_t heap_limit_kib; +} regex_pcre2_trace_limits; + +/* + * Every field except the mark slice is copied directly from a PCRE2 callout + * block. Positions and lengths are offsets in the 8-bit UTF-8 pattern or + * subject supplied to the engine. + */ +typedef struct regex_pcre2_trace_event { + uint32_t callout_number; + uint32_t pattern_position_byte; + uint32_t next_item_length_byte; + uint32_t subject_position_byte; + uint32_t capture_top; + uint32_t capture_last; + uint32_t mark_offset; + uint32_t mark_length; +} regex_pcre2_trace_event; + +/* + * A zero status means that a bounded trace was returned. native_match_status + * retains pcre2_match()'s exact result, including PCRE2_ERROR_CALLOUT when the + * bridge deliberately stopped at a trace cap. last_complete_event is + * UINT32_MAX when no complete event was copied. + */ +typedef struct regex_pcre2_trace_result { + int32_t status; + uint32_t error_phase; + uint32_t error_offset; + int32_t native_match_status; + uint32_t effective_options; + uint32_t event_count; + uint32_t total_event_count; + uint32_t mark_bytes_length; + uint32_t trace_bytes_length; + uint32_t events_truncated; + uint32_t marks_truncated; + uint32_t last_complete_event; + uint32_t matched; +} regex_pcre2_trace_result; + +/* + * This fixed 60-byte record contains no native pointers. `status` is zero on + * success. Otherwise `error_phase` distinguishes bridge validation, compile, + * match and replacement errors; `error_offset` is a UTF-8 byte offset when the + * phase provides one. + */ +typedef struct regex_pcre2_run_result { + int32_t status; + uint32_t error_phase; + uint32_t error_offset; + uint32_t capture_count; + uint32_t name_count; + uint32_t match_count; + uint32_t record_count; + uint32_t name_record_count; + uint32_t name_bytes_length; + uint32_t effective_options; + uint32_t results_truncated; + uint32_t names_truncated; + uint32_t output_length; + uint32_t substitution_count; + uint32_t output_truncated; +} regex_pcre2_run_result; + +uint32_t regex_pcre2_bridge_abi_version(void); +uint32_t regex_pcre2_config_flags(void); +const uint8_t *regex_pcre2_version(void); + +int32_t regex_pcre2_error_message(int32_t error_code, + uint8_t *output, + uint32_t output_capacity); + +int32_t regex_pcre2_compile_probe(const uint8_t *pattern, + uint32_t pattern_length, + uint32_t application_flags, + regex_pcre2_compile_result *result); + +int32_t regex_pcre2_execute( + const uint8_t *pattern, + uint32_t pattern_length, + const uint8_t *subject, + uint32_t subject_length, + uint32_t application_flags, + const regex_pcre2_limits *limits, + regex_pcre2_match_record *records, + uint32_t record_capacity, + regex_pcre2_name_record *name_records, + uint32_t name_record_capacity, + uint8_t *name_bytes, + uint32_t name_bytes_capacity, + regex_pcre2_run_result *result); + +int32_t regex_pcre2_substitute( + const uint8_t *pattern, + uint32_t pattern_length, + const uint8_t *subject, + uint32_t subject_length, + const uint8_t *replacement, + uint32_t replacement_length, + uint32_t application_flags, + const regex_pcre2_limits *limits, + regex_pcre2_match_record *records, + uint32_t record_capacity, + regex_pcre2_name_record *name_records, + uint32_t name_record_capacity, + uint8_t *name_bytes, + uint32_t name_bytes_capacity, + /* `output` must have output_capacity + 1 writable bytes for PCRE2's NUL. */ + uint8_t *output, + uint32_t output_capacity, + regex_pcre2_run_result *result); + +int32_t regex_pcre2_trace( + const uint8_t *pattern, + uint32_t pattern_length, + const uint8_t *subject, + uint32_t subject_length, + uint32_t application_flags, + const regex_pcre2_trace_limits *limits, + regex_pcre2_trace_event *events, + uint32_t event_capacity, + uint8_t *mark_bytes, + uint32_t mark_bytes_capacity, + regex_pcre2_trace_result *result); + +int32_t regex_pcre2_self_test(void); + +#endif diff --git a/eslint.config.mjs b/eslint.config.mjs index f930094..e08bf99 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,6 +12,7 @@ export default tseslint.config( "coverage", "test-results", "playwright-report", + ".engine-build", ], }, { diff --git a/package-lock.json b/package-lock.json index cf94e2b..22e0e3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "regex-tools", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "regex-tools", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-or-later", "dependencies": { "@add-ideas/toolbox-contract": "0.2.2", @@ -18,6 +18,7 @@ "@codemirror/view": "6.43.6", "@eslint-community/regexpp": "4.12.2", "@lezer/highlight": "1.2.3", + "fflate": "0.8.3", "react": "19.2.7", "react-dom": "19.2.7" }, @@ -35,7 +36,6 @@ "eslint": "10.7.0", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.2", - "fflate": "0.8.3", "globals": "17.7.0", "jsdom": "29.1.1", "prettier": "3.9.5", @@ -2442,7 +2442,6 @@ "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, "license": "MIT" }, "node_modules/file-entry-cache": { diff --git a/package.json b/package.json index 9105b5e..26da57e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "regex-tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Develop, explain, test and apply regular expressions locally in the browser.", "license": "GPL-3.0-or-later", "author": "Albrecht Degering", @@ -32,6 +32,9 @@ "test:conformance": "vitest run tests/conformance", "test:browser": "playwright test", "engines:build": "node scripts/build-engines.mjs", + "engines:pcre2:build": "node scripts/build-engines.mjs --pcre2", + "engines:pcre2:install": "node scripts/install-pcre2-engine.mjs", + "engines:pcre2:verify": "node scripts/verify-engine-assets.mjs --pcre2-pack .engine-build/pcre2", "engines:verify": "node scripts/verify-engine-assets.mjs", "manifest:generate": "node scripts/generate-toolbox-manifest.mjs", "manifest:check": "node scripts/generate-toolbox-manifest.mjs --check", @@ -51,6 +54,7 @@ "@codemirror/view": "6.43.6", "@eslint-community/regexpp": "4.12.2", "@lezer/highlight": "1.2.3", + "fflate": "0.8.3", "react": "19.2.7", "react-dom": "19.2.7" }, @@ -68,7 +72,6 @@ "eslint": "10.7.0", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.2", - "fflate": "0.8.3", "globals": "17.7.0", "jsdom": "29.1.1", "prettier": "3.9.5", diff --git a/public/engines/README.md b/public/engines/README.md index a8ecff3..34b814f 100644 --- a/public/engines/README.md +++ b/public/engines/README.md @@ -1,7 +1,9 @@ # Engine assets -Regex Tools 0.1.0 executes ECMAScript through the browser's native `RegExp` -engine. It therefore ships no external engine binary or WebAssembly pack. +Regex Tools executes ECMAScript through the browser's native `RegExp` engine. +It also ships the declared, self-hosted PCRE2 10.47 WebAssembly pack in +`pcre2/`. -Future flavour packs will be built from pinned, documented open-source -revisions and placed in versioned subdirectories here. +The PCRE2 pack is built from pinned, documented open-source code and +toolchain revisions. `engine-metadata.json`, `SHA256SUMS` and `LICENSE.txt` +travel with the runtime files and are verified by the release gate. diff --git a/public/engines/pcre2/LICENSE.txt b/public/engines/pcre2/LICENSE.txt new file mode 100644 index 0000000..f6fba35 --- /dev/null +++ b/public/engines/pcre2/LICENSE.txt @@ -0,0 +1,104 @@ +PCRE2 Licence +============= + +| SPDX-License-Identifier: | BSD-3-Clause WITH PCRE2-exception | +|---------|-------| + +PCRE2 is a library of functions to support regular expressions whose syntax +and semantics are as close as possible to those of the Perl 5 language. + +Releases 10.00 and above of PCRE2 are distributed under the terms of the "BSD" +licence, as specified below, with one exemption for certain binary +redistributions. The documentation for PCRE2, supplied in the "doc" directory, +is distributed under the same terms as the software itself. The data in the +testdata directory is not copyrighted and is in the public domain. + +The basic library functions are written in C and are freestanding. Also +included in the distribution is a just-in-time compiler that can be used to +optimize pattern matching. This is an optional feature that can be omitted when +the library is built. The just-in-time compiler is separately licensed under the +"2-clause BSD" licence. + + +COPYRIGHT +--------- + +### The basic library functions + + Written by: Philip Hazel + Email local part: Philip.Hazel + Email domain: gmail.com + + Retired from University of Cambridge Computing Service, + Cambridge, England. + + Copyright (c) 1997-2007 University of Cambridge + Copyright (c) 2007-2024 Philip Hazel + All rights reserved. + +### PCRE2 Just-In-Time compilation support + + Written by: Zoltan Herczeg + Email local part: hzmester + Email domain: freemail.hu + + Copyright (c) 2010-2024 Zoltan Herczeg + All rights reserved. + +### Stack-less Just-In-Time compiler + + Written by: Zoltan Herczeg + Email local part: hzmester + Email domain: freemail.hu + + Copyright (c) 2009-2024 Zoltan Herczeg + All rights reserved. + +The code in the `deps/sljit` directory has its own LICENSE file. + +### All other contributions + +Many other contributors have participated in the authorship of PCRE2. As PCRE2 +has never required a Contributor Licensing Agreement, or other copyright +assignment agreement, all contributions have copyright retained by each +original contributor or their employer. + + +THE "BSD" LICENCE +----------------- + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notices, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notices, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of the University of Cambridge nor the names of any + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +EXEMPTION FOR BINARY LIBRARY-LIKE PACKAGES +------------------------------------------ + +The second condition in the BSD licence (covering binary redistributions) does +not apply all the way down a chain of software. If binary package A includes +PCRE2, it must respect the condition, but if package B is software that +includes package A, the condition is not imposed on package B unless it uses +PCRE2 independently. diff --git a/public/engines/pcre2/SHA256SUMS b/public/engines/pcre2/SHA256SUMS new file mode 100644 index 0000000..8ed63d6 --- /dev/null +++ b/public/engines/pcre2/SHA256SUMS @@ -0,0 +1,4 @@ +197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811 LICENSE.txt +8c7f67ad85893fcc545c40d496abcc82d1009b76c9b7bcc7bd9ab92828852d76 engine-metadata.json +d31a5f1e0839955ae8321430eafe65da8cbdef832a1245bb71ccc5bb0d4b6b9c pcre2.mjs +cc7c4b2b0038e137c86d2037e73f2302e56432cb09bc082e4f35bba221dd1889 pcre2.wasm diff --git a/public/engines/pcre2/engine-metadata.json b/public/engines/pcre2/engine-metadata.json new file mode 100644 index 0000000..8f950fe --- /dev/null +++ b/public/engines/pcre2/engine-metadata.json @@ -0,0 +1,90 @@ +{ + "schemaVersion": 1, + "engine": "pcre2", + "engineVersion": "10.47 2025-10-21", + "status": "production-runtime", + "bridge": { + "abiVersion": 3, + "maximumPatternBytes": 1048576, + "maximumSubjectBytes": 16777216, + "maximumReplacementBytes": 262144, + "maximumMatches": 10000, + "maximumCaptureRows": 100000, + "maximumCaptureGroups": 1000, + "maximumTraceEvents": 50000, + "maximumTraceBytes": 10485760, + "maximumTraceMarkBytes": 1024, + "sourceFiles": [ + { + "path": "engines/pcre2/CMakeLists.txt", + "sha256": "670397b070a908b414d4d8ffa7c0a9ed2549b9ee27815a90de5fc9622c6df8fa", + "bytes": 2583 + }, + { + "path": "engines/pcre2/pcre2_bridge.c", + "sha256": "ab5929472d9981eb2d1cc635a4963a7243d5cbb9db6c5ba8460136d4c79b3246", + "bytes": 32530 + }, + { + "path": "engines/pcre2/pcre2_bridge.h", + "sha256": "b8767c0c59229a92d092915fc7b862a22d088a7f184485e4d67e14d6071d7d1c", + "bytes": 7388 + } + ], + "exports": [ + "_free", + "_malloc", + "_regex_pcre2_bridge_abi_version", + "_regex_pcre2_compile_probe", + "_regex_pcre2_config_flags", + "_regex_pcre2_error_message", + "_regex_pcre2_execute", + "_regex_pcre2_self_test", + "_regex_pcre2_substitute", + "_regex_pcre2_trace", + "_regex_pcre2_version" + ] + }, + "source": { + "repository": "https://github.com/PCRE2Project/pcre2.git", + "tag": "pcre2-10.47", + "tagObjectSha1": "cd007b4466798f66d479d1442a407099e7c40050", + "commitSha1": "f454e231fe5006dd7ff8f4693fd2b8eb94333429", + "treeSha1": "81a83a3552bd68d0ea7b7004f8bb6e7892f583ba", + "license": "BSD-3-Clause WITH PCRE2-exception" + }, + "toolchain": { + "emscriptenVersion": "6.0.4", + "emscriptenCompilerRevision": "fe5be6afdff43ad58860d821fcc8572a23f92d19", + "emsdkCommitSha1": "224ec5f9f2f72f09f9ce0e26d66bae7dbd8b692f", + "cmakeVersion": "4.3.4", + "ninjaVersion": "1.13.2" + }, + "configuration": { + "codeUnitWidth": 8, + "unicode": true, + "jit": false, + "threads": false, + "filesystem": false, + "initialMemoryBytes": 16777216, + "maximumMemoryBytes": 268435456 + }, + "sourceDateEpoch": 1760997684, + "files": [ + { + "path": "LICENSE.txt", + "sha256": "197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811", + "bytes": 4011 + }, + { + "path": "pcre2.mjs", + "sha256": "d31a5f1e0839955ae8321430eafe65da8cbdef832a1245bb71ccc5bb0d4b6b9c", + "bytes": 7513 + }, + { + "path": "pcre2.wasm", + "sha256": "cc7c4b2b0038e137c86d2037e73f2302e56432cb09bc082e4f35bba221dd1889", + "bytes": 339494 + } + ] +} diff --git a/public/engines/pcre2/pcre2.mjs b/public/engines/pcre2/pcre2.mjs new file mode 100644 index 0000000..b8f1e04 --- /dev/null +++ b/public/engines/pcre2/pcre2.mjs @@ -0,0 +1,2 @@ +async function createRegexPcre2(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var programArgs=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}programArgs=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b)}function preRun(){}function initRuntime(){runtimeInitialized=true;wasmExports["c"]()}function postRun(){}function abort(what){what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("pcre2.wasm")}return new URL("pcre2.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var getHeapMax=()=>268435456;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var HEAPU8;var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";{}Module["UTF8ToString"]=UTF8ToString;var _regex_pcre2_bridge_abi_version,_regex_pcre2_config_flags,_regex_pcre2_version,_regex_pcre2_error_message,_regex_pcre2_compile_probe,_regex_pcre2_execute,_regex_pcre2_substitute,_regex_pcre2_trace,_regex_pcre2_self_test,_malloc,_free,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_regex_pcre2_bridge_abi_version=Module["_regex_pcre2_bridge_abi_version"]=wasmExports["d"];_regex_pcre2_config_flags=Module["_regex_pcre2_config_flags"]=wasmExports["e"];_regex_pcre2_version=Module["_regex_pcre2_version"]=wasmExports["f"];_regex_pcre2_error_message=Module["_regex_pcre2_error_message"]=wasmExports["g"];_regex_pcre2_compile_probe=Module["_regex_pcre2_compile_probe"]=wasmExports["h"];_regex_pcre2_execute=Module["_regex_pcre2_execute"]=wasmExports["i"];_regex_pcre2_substitute=Module["_regex_pcre2_substitute"]=wasmExports["j"];_regex_pcre2_trace=Module["_regex_pcre2_trace"]=wasmExports["k"];_regex_pcre2_self_test=Module["_regex_pcre2_self_test"]=wasmExports["l"];_malloc=Module["_malloc"]=wasmExports["m"];_free=Module["_free"]=wasmExports["n"];memory=wasmMemory=wasmExports["b"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:_emscripten_resize_heap};async function run(){preRun();if(ABORT)return;initRuntime();postRun()}var wasmExports;wasmExports=await createWasm();await run(); +;return Module}export default createRegexPcre2; diff --git a/public/engines/pcre2/pcre2.wasm b/public/engines/pcre2/pcre2.wasm new file mode 100644 index 0000000000000000000000000000000000000000..2dee69e9c0c2fdfa60a9d174e3e738701643e183 GIT binary patch literal 339494 zcmd?Sdz4?-b>DX%zxQM2H!ol?GXT!L1c@OD5THbY5D7_hC}IE*1j#sTqPl8LIgTy= z2BJAb7*V|>VWh||-AcBSrM66?EK{zsEVojZ?=cwgAzS&UGi&DFbMJZVv-duG@3YT7=O*ud;k{{+Bw`_%6Op!pr2@?&!I_33VmU&P+V!mJ0e) zPk1ml^*{+8CP~&z(|4!uP8#`t@aKd<@<6+t^0J%%@^^sUga%v|jPzup7}D zk({}Flj;or++SYp{!9*vjX*^m)Ck2i@=AJ%D`ZQ7w`lOhZ3|y@-?-*qnca4couV$x z)&0O;kToMK>E1QJ`x`kq$htGi)v9vkA=f;)cE6S=TkqPAT2k?eB0Q<7?WW$TP|>P# z6>XZSMR=A~(eAP;nx~?+Ro7BQt{GdWf_iB6h-KcbDfhDFR+*NSX~xE35b%)tY*Tz% zq&_KXJgEE5)=Z7jm*lW4q))e=>XXZ-0ZpCFRbwv|Q4Lm0C{(M}Quwm&Kl4hm<+Dru zjQbxLTPm`^(w%JHSk}MuwLEPtvWKUV`Y%^ECV$H`t3n2Pj}fAh&v2*M>u|}YB6P6Z3TFtybAnO$aY5dX#QK-M=J6BzM$jh?z6r?e* zq_DcK;c6PW2S@dQUwq)kOx^<#Fa( z!=j}Wcb6>Rzl!-R4^Ov*gcbtJwWz?+M=l_^`=Jy~V;#(VO;9Hcd~|7FAPVO$<=+M5 zTk6rlDawtD07VzW%!;_&;?PUW5=NjT=;`8gnk~&Fv z!QI@xW(qbJF6ca^kk>%nP^Ya-&B~apP?_4OjH=}c^;y2A6TT|cJ0uaR#k-+4b>z@y7*6DaxPa^ zODqfeMtyqx-V%CW^I!b6Y?si@V2aDO0g@XaBQBu&$T?|&=z#N}hG8&dr1F5damI$#N#E3?QAML3 z@irSWPO@Ng8V-Rv7^9Jm11nZ_SB$$O2*_VCZh!zDxeWwi5LC5*!>TKSfcB^=GAbAm zCEYFK>9$QwjiZ2&+cw7#)948Z$y^q|9U~-;8;oH?N%u?bv=(GO(iT4MWRT4NDS`-Q zavj7y1n5vp+4TNoym{2Kvp%~4mR})7HTl1+t{0CO7mJ)_Ku`Ch}Ph% zQ$i+hM($u=lgaFB*|)*I&Vs$FLb(;fd^2_+s~OpC4B}{HXJ+BXI>zN{R*uJJY15fd z5|J9SVsp2nrQOD0j+S<2mUgh3H6hq>wE%p*s?W^rwjZ0@nVH)yE2t>~MaEz>hFzKj zAJ_diZr+^TK9wRAMgpOY5ZvA)4!~Gh!_I?BouUPG>qQ39r4hFELK3j;f88WtEzA>l zJcP!&U_^?z89K05*928ufDUjIUXC04A3|midl;u0EF4l1dVEj>YmjZW3Klib3rr?8kutI=V5TyIKV*eRS}ux{6>ez< ziLzp^6v)?`#@LI{wkhIEKgB0HS> z-2a!1n!0YM=Q*at{ul{ccZg{f2u8Y;gfcUFheI>|gbtp_Y~oV47i6d72xpsyk`jMR z_9%lSLM>+G*33_)se!L9{5$(ev#O~i5w^mQaM!E>PXGzOTKwg4j^D0HFRcHex2g5k zCWUl`X`wXDM7NoqO{4w@ERxtac)Z?8RJGFP<{)UZD1zdFW1blC+Es#u`+gB zO`Zukk*?@wJv--r;nN@ca9z&3cZVlWlzFhD{j-?n|B<{T-|VkTXI7Im%WJJ>yEijC zH<({oTsnAQ`A~hO+vzvfG+6%@5(i?Lcb1y! zU|!s~&|mVG=!eyNQF#_gc-e=o>pt7&I@p%h?=8sfkdjePf}QcMx=%DXd-GO-UmWuC z&^3Sb`?vLU&Yqr1uloF&OiDc)=vnR$TnA-JThqDkR_Pflvz|RY0bxHvnEOtZtw%PS z)b`|lq{3$PrZWa}7t_@2p=T>NnI0>1zsk(y&N}|gL~G3oAE(my2tphRTK8vaY4gS%h-@al$3%+7VaY^S zqrGkxaKkl7K#4l`CmtrVW^HPy8D1^}tMX6Q_j)C%Ca7tNC2ZD)V5nS_yxilWjM;;- zjgl-D38oYoOifoAd-8Y5Wfx6C3#_rni)zorkJ*chS37tnL|}T-QvLPTFo6( z+2)KiCo3JPkL$k0kJ&Y)-LaxgD_Z$oDcGQ(lwc;LF{9i;^I)*;Yi8B=F|#!5qH*c2 z4I~vli{T){f|cU!#5r@SPKjKj>4HgoEHTZy+dS5^W}jS8hPUe(ZXR^_Zg$)Hq)rDz zc%{*OaVb3znWuj&p=W9v@8$n%^R{hY$DskEnR|n(pD22Ud`+FfJOrq_x)u^wO@fA$9cB7aMjXnj-8*8ra{q5N zi}N`X5z}JKb^3?gR3$Z_t#&pqSzohVQ zeTc@-`-6R6h_FI9uQCt&?&+iu(Z3~2K8Xiq>2;ZyAXsoae@*KRu}7ggS>IB9ssD}vTg6X0wEk=?pR>hE=AEp%}9`j2WEMFNjSMGZ4iu5BpCJTX0VJCqCJ z2}0wE20&PF#w}7I#_4%%D(>j)c!-trvM+D)(=2ABHCAMAJRf@^v_RFX!(z@g%%5Ln z4KoEdcarf;7glwbmTNmm7L(--rghmhe1=jD=Japk$Oj#g6WxI;+jOq2wm-*WlRGtA+@>_W368c z1I-wvk{e{%cYIMeOwRFws-ZZc6#sVI!q6R(%Mu4v)LCU~vtP$E@v7`B{=Fn#zPt@A z75j>L9S)4f8{*pqh8GlEU>GqUX%&YIEX@{13L|K26q2Me!w#MUVNo!Xrr%=)z??(9 z9F>-!o2pE6k06%dz%DXFqYY092uWRUXtslRQ_=)heca?r&Rc0jlQeGTIoD+gzH7D_ zxvpZIvNS78t+VA)MocgW7~8TnWE*Ifj8O-X`oR)yb^jnu(@7J~?#(m9Ojr}5!R0>7 z4lcRo|KxX?=9f);XOn@L`0j;%!_P5iW{$1g5-!!ROJ?S}wN5YVu`1bZ;@R%jd$^`q z`uW^%YF!Yys>J{O^Ws5aef-9<{js^NnD7x?pd7|ougJXxEBT-PRa|Jv;8;)&G4(K5 z9HNy{|GA@shb8pEUS9iC#B z(VAu`tTQQJn6XOS$98H%pN6tlzuMO$=H)ebMI7a74<|yDUiFlz$0WTM(gURjq|xPi zzo_&gX-o{Imz7>7y&TeOO0SV#3u&jcBke-^n9|2c9}DSqrPoQX&j$R_qhyS~F%ro3 z#_%^rJzzqKN&Gb`{6VePfxqmH;t$`z6#ht0;g9qb{zy;ZkMtD&NKfI9^c4O`PvWoQ zzBhtB;kQU2ikN{5!zVJL5E%w2!;{!L{iKHQb&>Mnj9t8gAA_0)QT+{@e6} z%50c2R&wh{g&?0q1jSz3NSYcwFYKW{=_%@yo}xbKDe9A+qCV*<>XV+LKI!%DR|aWc z&H+ax6W#)IE+l0`RR_snrW9f5D_ssfBp$i+lT6Sv?fH3(Uw8nSL$hh?vbIJkBC zYt-0Ah?WNa?(WOq5N7@w^lB^0gv*xCFA)mD%ukg7GE=zN4Q6I4X3iGPBt)9N!_A=b&bvJv41QT>j{65cC0f20-g)QMVr=M}N7jXVydy1>vEYb=FRgYT)SGG>2 znIC9%^&`9QL->PnYQ@+M3QERyhk9x8m(wf#1@Qb!!gB=}6g6XJBDe6(8uAaLwzvCY z1R+TNHgQBry~}#kVzG=$K?k`9Cu2mcw{RdT$7Xm&-u8f9W7+pB_~a8GdY=??6DpuP z8~3FhO|q8tK#l%LDz9um7u$_sI5>LKKUi^DPw`Tzo5%MSW^2K03p`k0<_O%{7~Got zFgP>?^T)CF6ucCm1S1BtSh~mDA9f$ooV8e}j=vgut_7rc+<1!GChsT40V23YfapUJB1Yp9BSy*O=OW1b(F zw6CoKIVUje23%|g1LQB$JJt?{#Y_+!q>?8z+#+_lol(!4*0Y)>)^NS1FHIZ3Ad2@j zy21>Fh=C#hBGa>0&7>2)hpX^8$*QG@jT;RWF*s3h{B|CfA*_jH%XQ&sA^|Xq$}a8F z>c18~nYMvK4J|D}-?kOQRqVlx0Up5CAP7d<`TmlTPQAn;H^vY1S6bV#-0xFvnY&^M z;TrBE@^4VgO7Bf_O2A)~WDP?j1CJ>%;+jEJp%80dC~Q?IoQIB}P<>)bEn%rqBIFPR zgr6|rlc`)yo;ezHvnErlIf6$dbtD=0lQE6k;u`Maw`;hDWyChz8?H?nj&MyRh;{vT zVoXgKqWkCjv#_o@H%7CTn|Psc6$uqC)}txhYAgpaA<+Qs)gaQs@E}gBZT4|GZD@qS zX!eiEoErrGdBOFb)66~dKVwP&Le8NAW?Xl53#S9ha*MeZPr9o}qNZf+!1cCpqR=o> za?ACu(s5>TBB-d`Eznw;bm6^mE!eVZ>2Y(Ay1}p<2&{`rR?RY9j?7?i1B6^2UA!Qj zqKB=csuNR%w37M1Z?I|&ix6RP7*EkilCG(nix0A>6BsP`XxiRsF=EjDm{zjL_S(VM zGUss1%D|v@TuW8(wZ#uK{m0TEeR8A^BR`@oy_Q7CuTY6uGbjP%K;o!$^Zgv5VWb&qUnd7)>~J;CtdnRD{e# zuY?kyz{WBjlay+#BoTlrhj~D$+9fWz|Imj&(z;sY`G()+PK#+geW9#vvD8PX=!ZY@ zKJ>LN3dy~FEN%oXg%{G$2!qphCIC{Q_9nLEd~6|IhU@hv*DOnsZJ9zL6Z)sfD)odV zKlPER&9+$2TnPmQg!&sV@FXsRky|yem=REJ6(s&AzCrp2E>Zp$!yVgQ{KH%Rx*mPU zw*N)8QT)IqJ^Ziq{`#fuZ7ih3|E~5|y#G?ET|1xbUD!r$AOwjt#S^n%q$w-Rw?vwX z*#2XuA1``bKaxI<&PcF7%Nc+0axu$28#iz{CH`fub6j^%ZWi;pZVZbBU4J9lWEnB> zKXUqltt0+edYPre#2?$#;*tLisJ*Ow&qJKg=bJdqKwWaVkmJrs?Rkil(te!R`gOUy zj_J&NqUeb_bA*LfyR*>k^=B61Smkw@`})>QJx!7%Mdd2sv~}Ty^ZkLD(I^ap9C(T# zz+lv-s<6PVEHg4CZ7~}e!FxUHYr$juEYY)`A{+oEgHOq4%U0BQDn)}fx2e_d{uF*} z#fSQ}ZTaS1eqEdpN}`{;Qc@Ej0JQB85wy9Qunc7EVAU`xdQu^M{hRHhT^ySeehh7Z zn3I{HwKSxMn^%6D;x5cQn~r}vulAFs_BQ-~wT#p6gxf_1=29Q`8-QvZ2SC*)Iuj5p zR@}6~+tY>RWL?z9f%<4mKNM}r&3Ay#QT`wq690+~{f1lKo2%gt@u+FGkx?LEy?b5t zHRL-qI&}Qaqeh3X{Va3{l)9A|^sl-1ukaT7oNJd{4gX_y!<*6*5~67RQ-TAM6^K@$g%tPcrt5A4?*(QLC~~!4p)L2d z3#<>p$QO3ly+zkDMW{4fhg;jnGR42Xq261limjBIsA8;Cf+jM8KQQ1t!R=RVjSVmDo8U%?bc#MD48sNM6sPyQM)P> zB0Fm_YL~1E9HF5-QM<8@5Ne`wA%c^~3I-u?HkM@e6|*A3*f?lDXT@~N3}k;?h}xZq z5n*qb0pBp-DccC`?~U3G5ien%o`w5{VoeM*_#}!f)2C=P&FB;~oFbjkaqRCzDMHzZ z(aP};xzmVImR&~}4Z%Cc7JD$7jxlOMJZSW3jH*x|0~*@;9GmB8b*~BjI!cvwWhCuQPnp^AxgdujH;MoY|)-hgCn3&i-omI+9V5b;Ley<$mNM~ zD44ybcLbka+t~`tkeXwj05Axj$$!G`fNjQJE|aIuUJDvwOb@l8R2v6auAs|`u%-P6KR=RMnYuziVv@eAtf$ zSSa9iiNLtROe-T%P9XW1`IEEH`TV&ni)yfVyCm}|FI_077a*VZisqNLl_gt|zd zYFoSueNvV5iLg5}^hnxfBlevrr?NrcQGaX$Ek0oN$EqU3s6PHBKI@Qvj zwXdG0yy^a}29uCrC9wJ=O(Ai&ab zlJUdu)j9-4i3l?*KP=C&GbI(|qkNi7Xe}Dh$w%mwhz`_42^GoC2C@?q$CS7*LDc07 zOqmn^K3c_NVE_iuV+S-1oT&#>MyBH1WLHY3Sp4Ed4c|u%64PqxM3VUph0ji$`|DI16yE>jtvv#!5a;WcW3X=VeHPCliWoXY*Y8mwB9PL;3;t>;s%=hpi5 z*!uIK_0torhYatv)~}h`@z+=nr1g(8->{%dI*Py3D$+n7w+k9bga!i;Fsq@UHU&h@ z%6>o7k{4{OoBiMLXEg^y>f z-Y*lO;m^rG6`KuN*vjYds#VBl@1<>3vOEOZsT!A4%?B7x@YWWNL8cDSMQw+Phe<+> zhY>$|NIsFe-u?SsHb;xI9Q!;p6!7SV(MM-A9!q>T@5G6Qmsb%D+WZ$hrs;~cNB$B( zH3cH)yYQC6v*?8N>};^jT`G2w51aw69fx$ZPS-Ku1Qy|!NHR*w3oI#Hq#BwEA3?CL zXQ!!Ab2^(VF|OmSxIr?ZNF`gL|Lllm|RcKnwW?FWXmC~1)oy3+%U z1Vv>}?+n6vyJcrk$%>lRL9CdpFJ>Me|49iY+A+@+okTOH^nn(DitV;0zc2O8Y!YV! z0B~R*Ms5?@P;D(4ji90GiuN^%OjtOfJBY$pF&Bez!?;0k37Mj|Yzu824I79|k(&)z z88b#G+D65H(I;*oSC=`vX z`lgMW+Jtc<(?~_e=P<-rWEG3X%@|=~1Je;;bp>7w%%T#uD~{IzVdC1o2xHyKSc^<% z4aztT_H{&<3I&AW!>qCz!gNdotf`|X0>WY)0nsWK5XNH*vlz(jioX^}*PQPzUD8@C{=zZcV?m=KnP;lTjvb4#nEeZCFGig>)i zeqR$`UrziNBtUE00KCfO^V#Mqne2in{$s6G!+ww(KvVq-ue}-74?KNG>?4p4Yd$pl)j1{KiqzJ zS3^++Z|?5?^zQpEY{Gdg_3+ZN;o=;eFOhoL3OFVZUMd8vx%QS8FKSdL{9M%n-)HDL zM|#k#cuvEVI8vQlB0EvXYxE_u3@Ib@DYzt{VSn6&4zx{y1(XxR-R= zw}Fe^aFNS~%W}A^g^LT9W8tzM4xtd>16v{pcQ(tZ2j<`+xM{FgCUt7J7m2L4H3qUYd z$jWHf%83?_&Izlq8+ILP)}IaRdM{HCUNH90l@wD8AEyNt1~Q+rSD~N}^e302-s+~s z$@=Y8)xP-I7G1UK1fY|x?`c~x0Gi$m008qsLx;pHph8h2tClCv?gfk*A*1#jekVJtNE@WV0 z0qq_S^~zHZVCgf>axv(?$sT7LK?`6pU5D-r2d2A8cHSFhcxol z;)|KY6!a6aF}$vzAh8NxyjvQQQ#3G)hz9DEJj1_&OLpe_^OT)BtvuKNtYw$f9wjZi z==q=Vr$%}H0ugjy({xjFcp>jZBJVI^BwF${H8*;N9T+Abno^=7F>>PhQJcfTAx$(T zavS~w@D$y{L=(fxdYn9w37s`Pe&Dx=2c~@rr4Synns!TNnf|A3yVUGa-nNUL{~3Q0 z%JT;o@}v8NnYP51b*(wsHJ++#O?8cMK^aZbki>_|tqvOG00DWL-rn}CCJ0=^*%IO!0tIj38lK_BVgkj%bbbzZug4g;=Ksde_E_pn zp^o(=ullivLg}jzD9Ja*M6qUchRhcIr=j3#=Hw6VcyI}P*h-#wCc;-Kq>orXc!w&k+P{cl@rHTaU7ipe1nV#L6J z8asSMTGMTpvnWN^2~$F@VS=LqtS?vrO%rUgL&-4N>4*kB>e0loPoXf+;k~p;8n?26iWN2XJXChFd zO&_ES=Rw2)#h+l1{`WVSJBBCKf8KDU7BsnbU;4G>at3OJeE@*_zu2ne4B)UgspAO- z0=FD4n8@p&5{J=RXu zn1Jagj6aBkg!uRh@T3YGr#uvvt;iDL&!xa^rP6F8mO~%A3*a=It`iE54&yM( zpX#b{V#o97cpDk;^oUpzYHSMOqC;2nAnXQ^oDWX-duG6bYe7U6CiFCJv6pCdTY++} zC0a`=Ztt8oEO}Mg@eji!T>cq*-v)l^ivJrg@1|}LLSm@i|Bj923V9VHRaZ)$-kP~o zkLCkvYvL!d`|zJz9j-e5X>)6;l=lsLj4th%*85?LHDVQAiwrq^r!OdTSZA(lS8&_A z(kC<$de8|L(dgieFoVbwIgQT712_(~9ZlJKfrH4}fO|woM#4>Vz)K{t!5MC$y)URe zP#MA(6>14%X;#-DMhYp2JLsCc7fdT*K^TBGc#VFc3=ly)xPwVYe~DTIg+$9?27?9Y zX2HEhbeAVMYeDJ>NVL|vNlJ`u52Tdh1Ef7U@HbQsuuXWNI3wjSc%;wegc59=>`8AC zYWNJ`VUVc){d}ScYSq0O*i;y-&9R+Q#0-<|YjPSAco97R_B8jOGmI#ch~js%;G6C! zFk-U8L6^}ahnWIBBjJc!fQNY4)OJr31HAe>Mq=zfJB&|s^yk!M^@ebX3UxS71G{su z-Ob#8ig8ERV0;}>%NSTS?kA|2{y@J)}U5 z)fZ}@dx8bseVi$USEbhDb5p55u^pvRVD;r?NnM}z80q~`nvq`9NY^B_FBU-pR-#A+ z=$-Wm*|oP%CUL9_l!0|k=*4P-oC{K~7G;EXU0F9ojH<0Diik4g*<`=LI06~racj#2 zM#^mjl`0}wAc=$yJiG-L(0O0BmP6< zK9Ev^Y(z;~{gjaguACxBC`p^QDQobkM2Sgrg_b+s0D}OJ>qsl{evQRRZj8o?*wIN^ zH=6>#Z^sBoQCeq-f(8z>-OZc- ztK#L{?5YL1_Qs7?sBwLALbjkGVGe~lsW?KB`$YN2rvDe{P?V5s1WA_w$HL5PcUJVSYHdKyP|EO5*f5@n2 zVZ>$~OFA2m#%MT=t0;|Sq@|8>%YWwAl$v#!mIfWSINcBz8DL+RYNE$_w%CWfT0f z=ao!Jh#z6tiUYY75K%~v_=eY$)+X)k%X_to?{zTH-a0JqZ!hl&>xSacm3_-q%%>P; zvkG9fI78cq`PeJ_6XFy*4}HXZGMdEQ8vLxFu!lbwQpWM7hN!V_(DjiqjEp|<7ld69 zvVL3D6`XB9Wg|n<+*k-cl*uIlV{eStgqOgLWey0SI2WCB=sdx9CX{_(Gm(!odSSDP zswAsyP9;gCsg>y#m_&Qw^0<|T{0i@2Q-h<|_#xC&j^5(}5aw-m{qFDO83*)#P;!;d zCjNpP0-yP<*4;U)GX(yz_TclOf4^dNTRH)j;iYMhLWEJdf-3QcUGhC;x^>;xUZBm4 zkle2%L!RF}3JzEN+qpgB^gh%3l=r`=vtYS@`%|mkGW*<%W%JSG+sr&+JH-!_%@LW&9JG_{ z9~{cuAun_SJO4=PuH`Zqf(7r!qv?x6D7}vucoYym_rrhvp`3Zxqv;P4q7Rfzq2g<{ zGSRojfBX|z9@1c4KJllAg9kN-%g)36a-`w8F=QZ}CaW>$rmC^wCniA-=H1^*ZLM4V zrSEWEu&t%ZL{wHkAge_)?2Y`cY0%Li1f{YxkDDCCH@q-gYZUr}GZr5wA;<)Sh1KzN z829~al{LM-Lyu(HN;s1a4_~MrGDdhfN{YkCoy(qH&>N2Sp*WLa$$sV{ar*?A!@>v^ zTD(>FmpW-Tx_~;t1thHDl6wrLZjO0@E)P5w6W<0%l2mIr*gO}?@j@3k5|=YAl@?<1 z>{+HHEq3b*Y39}talk6hJq(!jZ3UWxUgHZr!I=~%e->XrA*ax&hDd-{hP5O-8CF?K z7s{Fkr>Q%OWMmv_`cGa~M)2T-Otv6Bxq{y_A58d?!qit0L|UCDCOS;Sl!n7XLulye zn+wWoN0vdJ*gj=OKBsUdX#^*DcvJ$nw>dE53rbY(t=>3Ou1to_y-@jKX5tgo?}vD6F`L6I8ObhNY{6)sPP^zsyHYc;Dm? zVas#Z3}++|c*F7mgGYzQ5R(L(Ct9QB;1ua>HObB@jxn=-a@$8NG~7T_;${e-f=yXr zF|L8L2oN=o0*CH^6633V!kl5TDsBbFwuwCyGOiOR1otGFEDU2zvu;H*HsD#IDsF|W zd*fD^^=M)r>KK;`VHi9%R>tuiS(%AlWM#p9OPF&wMUOA7P^SfhxR#s+jTnYO*;}|i zoT0~uSgeQ&8ZU=p3d3lalrm$1xfb(WwY-1K#J((os7z6PV-`ulxdqOf77=%d5zO14 zxc+`Dnutn~MHNyl$EKWHVjPvN7^*AEddwhcP6R*U!wRpFpDFTFt>P#9!VFRnUXo1G zF?K|7A%SkZARjt++6Ch~7Jc-pAGD#Tw*4`%-wU+0 zKj+Fc+bi6_L}{A`wX}oB`~o9m=E9D?Dx+C;3h6pJ&IYe-xS(GNf95d^2Tkg5y&kwk zX7I*E89%n2&Sh6@?_toL;e=eB>Zy2Pq$U+qXM8eXP1@+2KVbz$dl15^$>{4sBQYdd zMaGJ7?LdS0rJ_K#C_TtEA{(2Rg9K9Xi6Ue*QlZoXPZWflL{`%3K_FUvOf((2zJsB! z>P&61=`egJiDIHZg~lY)aox(TGC?awv8izH+fZr0V>$&JL_jS_z@zkC#sIbDJPbiO zhU$)}bXi_a5e1Tz?1L8W_lc_9FX}6{pdcl@B(!irrV`aD@PrvW%7_yoHW~aa4qw-% z6!?UtlLRH`b36I~)7s5owF@$aL-(aT%{BBT7)l+ZlCfB+od7(`Cyuq_{nUbPp~X6K zXmOh_G?=53JdA8rHF>^t{t90}QDAsoL8r3ybjYEKBhI7!Mq@QRx?3|(r}m?RT`+Rv zftIg*N%WB$`xo=(_39V=Ky&?Lw;dGDHgOS5fKl)I%p1^?XB;xcs~%4t`KLZpQ`%KT zu{uIcxgFH47o)y~EdZl!R^}zt2s9iAVW@&I;z>JlQAD(NA`0xGboVdG$WP3{!^ie_^0N~^Se2DWd3PiLIriAzltkcA#%@64sDNVns3_a7ALoXlyCXJJJZy`ezC zl9{u@y)#?^I8kaF>L_M}Hf>`va4L(OqC0Gy#GSlTEQ&qCZveby`=beL$z*I+8>As} zus!#3Mb?G?>C_ZQ1?khO&cm!40Hl`xV_2i$-1K;KI*UM={f<_HxZ$)@y!6d{(1C2&!Ni7Cpm?Pqkq+<|$hR zMhc@{Q0fy{8MxC#SfTB>$_WlH(dg=H6C+Q7W3L7awaI;9z2|FO5T1z8%wwrs^Gk3T zh%xC!Bd=PI+S&tz%8ol`1Hzyb`*jCzDzQ}XiC`%&($9k&1u!pPmwpY$81IKcmuvHO zj<+?-i8(F)?ASZY+ftljiD8AT&6qG=YsCa-V?gtmU}YA9#{|;qWYUd^o)W#7u&)R9V}e6yAaYF1Dlr=q z10@DAF{i{_Ow21W9}^2oEX2g35{ogxnXS+>CO8iWV#dTlB@V^}2P#3vm^h@wp_m{p z90JC~suHU)v8KdYOdM9?a7-Lg;z&#!RpKZKJh>7CHPj`-u%+X=%#+vg%eHQ(mkmjH zJIM>%js4bsu!+3lNOq1vh;k`NC;M+=%tW2bikTtse$&k%vs6iMlMUQ`1$gK&rvxC9X#0Xze0gj6{1G+r3ImKRyb<$R zot<$td-8M?>uL2!xS}7Zt0Z)b*^&R73#UNfU3q4ZFa;h`%|NMStXXfTP5Oo^D;=oB7DFavEX?oC ztdJ1SYGQG9Ds#%|X|k&*H>abq6bW-#feY+fz{?^oU^dGE9t7NkcZ<{SIMai0m+*>J z@NqF)9akhpcsD=&ju8itI#&F0zc=}c88%~(P?3EgK^S&rKyZ-JX)Pf&{g0*L3NDb4 zETmUuIW&b{;L<*+;X07iA+a%>d3G@V>AsGi!RyE~nPq@E%$Jr}IY*e|)nN>G?7)%z zU{+SQS#?S{n~GYfYS7(hUSYvIyM$Z~HRxP5Zsmz%a(3&nIdJ%X z$L4V8n4jI`pc|b`SJ~IoW7n8}rV}&)YM`|!`p;q&(2@?QX(vh2rqim4Ryd;u#Q>{@ zwB?LWRNfe;PQ2lfG3fXa2h#)^wpG+*`t05VX)t=P`>zu#lZHcS+SjeM7cgh!*f1Wk z8U<$M!TkV!GU^sFj2SAn|47C5n%*27O>gX28reoRRT2^8WffcLs-d9o-j3O7Vse?j z+7HuG{e=IomT~%BZNygCT!F2S*|&6(6I6_wB8jTLGhNM@uUAxR5->|f<0?=@)PCG= z+eR2lke3V{MXDXpp*AoX^FD3a;($SkL=L$@6QJZJR`elg)M}70fJCey6>RY!t}5!& zsfybaF6DZva|4M4pN9{o(+&Jj1Um6yP<&fM$81{N&4wHZq2PH^H9joJg&<0; z6SJ{P3&%{JCG^Q2PLAND3*2#fOeB`p09e~*O_WUW@@GgjpFo)vk}6<6e0HI6ItVfk7s%L-G__FpZ(m(Yk<9JH8y0Z}P2r1zObkM$*?gz}Bn1KkQ*ut@E_}8m7 z2bdApH10=xmC7BTNF&>3?i3`zl*D-J6gBOcqwfUgOdX-NNp@<F-vR{%)+)o0EvGn{QT)w#O4W@T33Oz9p?CXxyy+$3E7$9Nx0il90Eb z`@c@bG@)jbuLos3-t$AdtxM+W5vP0;aDKUh^UD=3i3uH?(+R}KkbnOg_b?lc5!VNF zKJ5WS+2B;UpvDex5-t9?j>0B%Ua5s2b@X@{rh~{fbQXM#Qj5cZJ~`qR{2Zx)Qh}3y zuWJ7H#?4d1qP}`Z-KuRMd6f7sHv{v1sVecMszj?e267poMKA2ZTVZ^&hgvn*MU%fy zxkOc(ds$0BGSAB%kbv(6zd)iD5-ZF%J!e@{hZKW8*B)u78cGdaq)bO|dkA~x&_ziG z0!};CM-z-Q{$E8Q#)^x*8hZEdEwbNW^Y(3i$-hE0_ zH8EmswRi_ghP!OFctG+fyHq?z9??hBWpm|&cN8`8)R-O*=oYPh(ya~d!NGb%Cwda$ z%PIz7Th!Q+w-~b62}~9{v~cOsJ&eflIpg>r>{m-LD*Y zUL+KbF6wwHq`#Cf-&%L)hvj)awiDTSG!)UmCj+R=V1d*WBEf$=h7*JgI(LK)5L0ZY zs6fnA?=aw~v&Pd>#HhJ4ggurl`L8lUZ*1~qI}HaO@TlYPCF=YDo{fUzES9xry(9!o z1SeW@kqg6n*@klXoDmzLVa@{rYE7`7#vx@k#4++F?tvkUnQRS^a*fGcL=VP%d2y#` z_abXXeRq;|qjwZ{xq9K?K7%j?v=VbOfWA;^LPodGBp-4)RJ5@xV z7N~KCKVm{hM_NNsT>-tZ(9H3gyHm}r!hLsgWKgK-m>IhMe+)IL*ja%4CqK8nt%&6R zL60%=55kFE#^i!qWB-AT)MXx|cW22%`GCiYHD$;jhnB}F_cX;YbY889zPzeUCxCh? zJyWt^DPAY~R99ZuMh8C4y0H~1-zX(;7`YE854xU9A4$)+p3D+Ccgo|CAhrsRQHR#R zT9=p|sD@GkseNR9q^$ajgk+{EB5`a-0?P?Ye!_CjRp z+w^a~;kw{3e@Uy*?DjFq2k)FyrxsLs=$QU9ERXDfd*m;{a}vOcP%r_kkT*6EJcir$ z4$0BC=#rG|1?;+qBWgM%;OP5=)1G?^N|{J)^Ygw1hZVex+T^)=m{ix@2kWq{m7EViC~GKH}^#Su~N zlNsyaU_@KSiq078Zk`G|{?~BrWvU;Wmg8=eE%m=(54C+vaq*wjJbzqp>TYU{mFkX# z4NP#NZCi#iJwWgCwHR2Z2MByYRbiFybf<=I?NAlb0&sFMf??8~r8if$e9_v+&}QWVS+g+_X`fV3^`80K>rb*n+n zx&9S~$>R)@HBFlD0Pk283_x@hFT&uzm#YpVx`#j8stJxC*N|sdwYi2i`bYgG7Q)q| z;}x{pab6K&#Ry;@5K+Y8bcp` zPfrz(Wk1<83?k7oxW1Zl)b2IbxoInbh)>_o{}4N+V@@WV%gs6)JuFjE_{ba`@`=>LjFUH=IL zqiv;?0V;}JG$!#(O9m_G9;ntrTepy}sTs7wtpY|ySYwd3X4WzZlyMkS5HFH5O``9* zzH2^CS>>M)H0xZDtvB35lBodj<5>(eW|%yj{5a-8SF%?*U$S_xb)iEEt zOqaXiJPa*axu)VGIL5#%l(E^_(Dg~8VQfU1EMQkQtCN$X7m)WGAY_v6VUTJxiq7o3 z4rMFS!Ltk=okI`@PitcEw5q`)awDssWQZzC1f2E8HM<`mr5v&>)l|OUgX`UbJ;`G&!n4H;{g~S9xuw5$N9Y zCpQNVm92hdZmd|Nlnlyi4hmB39O|&=R;*nu6u1=&))LfqUaj02$nblnYv<@Cs@6IM z&Jo~J7oO_NzT$H^d%A3W`cq`dOlYxU-$B7b?38#vWDYOF*BC}p&{Q>VHA6QvkUryO zmAy?ph!3Q*q-6{~O3JQ-6`$-UboYj_aB+%?6oGac$IBmb*wmvEKF?D(m!0B%2>|)% z^=0yW11s=;&9Aoki16uf)8d@(6?Y2c;#cC>sq4I&zpAQAiX8&`gS>o`@h(#0w0o0J z9xwT9+G%$`e9F!J?oD(E$gE|;&bKJft``(utDGY)Q31Z8{&|KTr;$-3I6zm<*%xx{ zWIIB#$~lRMQ@}A#+v9BhNy7c+WGNkjJ@ii>mEKXr+nYwG#O>d+48C~HzG(vg&B0#G zNvpd@fj0+zSg3aqWA3Pk~>JVbs^+TUq6- zJ9MR=L>6PTXfy_<6uNCD#;4)nr^=Xx5p965=>S3b6h}n1{G~7g=lQy!OheZ*_vyUb zAQWO*#;YCis{st!S5P_k7We2x8QMGI7ZlSlnrh(BLJbIWJd|XYi?i$QareS^HYV`G ziUpT7F&P#|Y)n;x3VRb{nl8#)MgRw6qsJ44atsl09DV>WWF9L=Ih`x7vA4@ApgH;H&4bfwb8}ygoM3T>R{ZoW8S_#cW@G7XVgs7=$?D04!r8C$t zlFT+?F#Apk@F+|E6DcfV4xB*@#UI2SEh>N>stV}bEZg8hN!xsi>K=^u%N&~6FEgi_-3luWp|jLek6ptjQ5B9| z8*idHG_i>$96Po$0uhYGq4)*|W8_E0JMI~XjJpvJYF(;wwv3pdZu@Q~$7X$R9q`Gh z4n9M8YaPnK;5H++8mjxSwh8L!4G~is`^wbA{<4wPPaWPPm2^7KA~pb>>3(a<`fS|yFKT9K)h6f~=q zk#1J232LE<1qZWQ!{;O}Gstn`QIG02dsTw-HufqebTm+7-#D3jj82S8RWf0m29Y4h z)&v=@`o*x+xhOSlF1U|s!hatIDp&t?bSaq##hqgeYw2CR=FdJbPsfy-^ZzsAD1gxf zPqr0Wp%MJ5=5WzJB172mG3CU=oE`ydy{(X+$u}4G{St^=nbz!XhK4skux1jDG#(lYoPfirnRnU z@raOL`Kv)+)9g?2#xzd^j+oJr7m?h=Z-YU(py7W_OU1*@{m>ZS!weJXluoQ(poSRR zI8Jf#Gxy|dhM&O~Y!SspNz48}EJ~K?!Z>$#v1y3~FEVKM(`Pyi3=t@d)zqD_F`=a_ z?lG}@c)m3&@c*-F1BRAYdGd+x=h$VesG<g&-OSdfX!mqTKR|f!8Nxc=T0p7Ca8n$xBbivfV!fc{ zPIAMUSqHRA{fLTk)_7Rqs+skzm0COeBCKRmrqK_p?b76x&Q*)Is$JWD z^ju(Rac{Jem)lAnGZV%lJZ4L18f`88vpr;8cT&Uj?)HNx20NG}u6SNFfud>nq(H*l$&upd5d#Pd`d^gpvhMHLI-5jEF18VDD8LMBcv zKzgE;wFcaGn4g5IL0avaY!(a5a4?8`#%gIE_JqTze~!Owmu&%GS)ch_s!z&hcmD` zop!t1>2=z7PW&`#c_>g8$M5AQ%kDs~^LNaCGX0M=9zA+=s_r9quCLz-bNas>K2FMZ zSExllQjUiMtK4NiyH*l}!gn|z5vw%pa|j>hXn|M@#W*GbmO(27Uw!AP1)NCMRf4*DQRuOVI|1Zh?f83K$7#(`H9juC>{p zV;q2z>2CN87ZMm3rfZx^aT>7N9gbztc1la?%emt9If+$$$hRK!AQvU`Fu7M1urgtZ zR3pKo&ugoWu7=Td2kXmc!o97)&4z)b9b`2;fkvI095`-PrbV-&8ouGCKIwfj>6H2; z6nzkpbgG#MH5+W$2|}p_9pR3@-OZz+%zSFQ`B4$hz7MpWuV||_BtZ3XduE@E<~hpA znZZe*b>ZqdP7CBL@M~32G(p6$7N(Q((AeNu+*23e;dTta7LJJqCevhB>5TeBxF}NN z4fUz1K1psf$A+pE0E>K~lD=v~D#An>k6&02G|c4nB)wN#z|qZ|Lj*gS1mavGRl<`K zaMp%4(}UY^CZBK{Kc=81&^QRJld8qIF*R36Gt|NpYQgDe^@!6L5CYLPPr%`!b{!A4 z4J#X13_$C6Yebn*gRQDTazaeY?qbJ9JaC+Jfi_cEtj6#O|22)_6NuOwXOh$5a#DjU zf!tkN!}uD0FBaNw8Ftvl3?_B0+nw!KsPD<$utbC)g{g?csNlU z2_PKM`;0W7W>xy2(m83q(W*4djnJDk-)dF*kkTs8=USCsQMy4|=ckZn?te0Al3opI zq9yq@GijoZ^!~8YZPJ`}rSuV{JEV_<^iid|q>qNQQ@Tgmg><2GpL7w@rP4E`%aFc9 z=~>cugfu~$Y*Qmmpq<*gQ|UR3ftuK>D7LzE|mkr0)&s6G|_WJ`vI$z(K1}-lkbaZWM@YXZq~EOcQPQRutjPaQ=#4(KFDUX+7s*(S0jWU+%8$pJByQun zRO1Qm-oB$@#z3_Zukp(0MoP_4m;=U$NB3Q}8+l4y&nce zhe5VeHu=;8l0!>4xOkXglI%H+P>C>8y0;oiL6@?Fd0-oun|J6Zy(8hT8)&-ZH9+Hl zWj&w7vKa|R-qT*}AKBxC*H}Fvd);2HTkdt)#eo<6Ijc%0Ml7uYdrDWikMS`wMvmqO zsO+AF$JHeyUAvOA+-o1!E83_`USfH{E2zL%fwUJ9v>Gy*LGm0m!sl}&3U#PuqR$4M zkPZEst-N{@qqo_90F2(|w|&c5CL`F4p9Ely9t$u!W_-2F_JoSDgp0~hWAB^Py=~Lj zM+-@lnTp@d%ofAD@fzJlPro`+#~Jn_l}SZFDzmWHY-NO)edx9;_6Stn2dNWp_d#lW zyDw6+Zw*qJbiGD*U)C1-QJ?nd@i@a?q^1>8QM#|$%BwdqdboCKwV|n%U_*}zp&z7qm_w)7g%_ziHW!SAa&yHK1hAFLEHY;AeGH-ui4!S zXJhrUr^i)>y+|ccI!HpP#Me?{qk*>%QeW9C38w&dkE>lp@=*~49NUZIvzV7>G2ggqp0Ugon;YjtMg-vqVddMvM9}2b z+$E*vf$@kX5nRGlquokqkD@kH4DPa{R6U!|&Dwe$b+je#_D&MbjaD@lB{!phY&Z>c ziN}Q1Wgc>b)W~xMoo=bC0z4dO_bMRC&QZT`uuT1${{rji4~S1YQ>&-pCpyt$>Qygx zz!bhA+olas4I=F3HwTY8Cu%fm-G+X{PuFhZ|L}C2u;TlZ=5_Z_i%aHE75~N~B99g*GsDPj)Oybu`R`TQ1)$%u&$ZEmc8+^Sw z3Cq6E7S*==5SZUR7QOKsH@m+bck`xOMUrjAV++HngaxV^re*#!I+swL)sd2R2C0^2 z$y48ZEMcM}|AdG=r=yOKCFBT3ebqPS8XZfh1yo9n7G1+3rof=})QKWIu~k=4CQl+z z*y*UY>MDtL%dv#vXwz`YA!l>2%*qE;taU12HaLAOVPY$);V8qYLxAlVMPwfCNrxMi zWePCHrg$7|ILYPw7YgefKEPqfd7i-=gsN#>sf+~%J!A`)Rt@o|VW+Yuo2396p~9s5WSZi7(%et`pHgm8 z-J&DoQiG(i_OgU5gS{`O)7`(w<^;N`t$2!aa!ClH(NdezY%n1>)HayV3_GC)8@6o^ zKbP$>L1b!=NfP#oB;j!FU;Q_=^n%tjzJ^0niyA%KYV_4qIK_v>MjplJs96KQ=fBG8 zB*K1YwOmu+1n)uD71oMEHV*au*{|Th!-tJ?SzgdJ_6Wh4$Xa~1>;_T(?@)dof{1he z7e4*557!l9^KN_h?(w@PO2^cXV-I-wti618{8GCth+uFW$1AS`SGT=d_8&)A0a21o_O?JWMH)qFA5jL3*2VOKmm#YHdvX=x}3m}-NK zSca9R)8Rq{vs*ipy_3}GVX;nk*M{X?L}cf>xwlxVxX$)es&O{foB$-EeFO_i9eir)N1gE+ZZ>XN$x5y#hoD%e?E7d07am z7I^()Nr<=Fl5uUE&q$;1GEBTnnBb!oR8DVL+d3x)N6!(l_)bEb`eMBIl!RPUh=2J8 zAW*wp;-Lw*hWojKWkWj?_q=8!AV758A(c@SA@}`&GH$lqIy;N{6axjCxvsl)mI!n% z!#mv?Ys+lp)h?ohwKhmXn+9&i0%23gCgMv=1uS7KsRiL+3FNGVEg-_r244mh!1hY$ zaZ7UT_PbRF#!wjUprcQ^#ohD+Tg)|JGuT3Fm~_ncjPcNa6z?Hd~(Abq6=rTU(zl7^7CA^+xW9e_xxwIl=!mlsfDG^&mh6Huat-x<$i_> z-TkyO96FP|iwj#l&v9w`Weo!Da8{xh5{?8Ld!D6cIq3T~PDDLI&G4`i)~%Wa+Q(I8 zm%Tt-wGrxhl8GWF&ty+<%g6pgjVJV+D9lIFi{bA1N7z2WGu3vM+)wNEcU9GXLe(-P zH%HtLvxHye`)=qz(`Y2P;!;dZTssdZpR}_y0LpId>BklyNeSUKM7^ZF59*Ia>|heu zQ-XI`<$i`{>;9RFVSQg!#LO>oLyK1pmEd%pP-H-Uny+@WiH}$ zru)t{Sn(vR=)FPe1*sh#58UkIWN}Q_<+yUD5Rg6M$MTsxO6IhX!ai2y8YA~p--MCu%Ow8rLcWc?Ryfqx0_d z?T-_E7Flrl$*9r3c@p~^3QU<>4u@9QNf`o1QT-D1S}>=*L$-eE*b8>Q!a{Cr7LGuy zH5L>Ji%2dKqs*!e*2s3zreF)e=>-c-k4cUJ^|Qrs>z6s5k5>zH#|`X;f!zXZjdOvW z2y7+x0h==Wf_+?GX@}G%R>z&t?!`B`yMWQH7Uv0(;ggkbg)V#nZ_`8}d)jPGi|v~R z1-ly`Le&));WiX(@o>Otan{}M9@^0nu$LGN?0YEQoTESJj*aX!cemTzDIVv^1N=S6 z-`&+1M7A~z71$xyWWh;M#^L!^hNis-Gz5w$5M=>{979>)Bk zS||qr9=sCHAK-a&Fi$h|aJ6_)#gw7p9^~L_i))73QpFrKp!~|va1W55 z^W4Gp(sDmJ%!C9T-Zzj z`H04XON_M-!Hm*B2Tl3d#EVWo4(|Un263(BChS#1tszf`uUADII}wDGL4+ zgMW;_JNa|`X|K&AOqm93sAt9`&8_k;rc6krz6pr5);f=B@Am9N*+pkhHW2c_crILY zTv2@gw10xbui~8G0E(eJx>{WJPx_;dnq|r2MKo>CbKzZ?S@W zy~iv1{83Ax#oqytEx5enPRf4aY*HD`oI=dOC?JI02}-eg3&3FHX)E`BzJ##Lk)4*& z_~1--5e`xJmTlqI4$e@L%`KQK{x8^fboF4-+2;l?+XG|(Q&s&M2->PpDp;zVXDJ;! z%~Y^!>cE(9(Qj+1H-Q8@p+jI8Q>vv{m2i>W}iv27uEgU$wd}u0h(SEL6 zdGhjw$1i<8-T(Lb=fCf{#~wR>{_Fz}oIb_Ro9=(;48IRO_?EXk^45pXz4_y8i9N4c1rA4R!%VOI>zKg-8~I^ zGOzyv!2`0J+2?i4-KV%EJJe+so>u%jGn^+FjIz_U-8H>d&JaM1<<9nh#qMF;%J6S7 zA`&b!ygGd9vbLoiHfLRU%9MaCl5Uu6OvzMjhy*;TPd!ku_mhatzx6)aRxu)(sPn39 z`%ZCZ@B1~Ju$-@2>KdgLq(~?6NibHpk3ISC_VlFbj-5)6_<_E?q9H1yTJ_5!x4R7q zI=ee3Ap=Z|I-d(7Mp~!$6BS0!mIfIIVnq${kradmuLE{3FtNQ(wjjb?=+M7&L-IjQnT+1OXyWrDY2|LAq;m2{L|y-LDMhRvQ;@`U2^G@vRxQfR-eOwwWp#wd z=aiP1`YtX)waIVY(LtuA;F5z-sKPQ`)hwrlE;*Au#vRpiC&ysh@t>$ioyEeYFt@=J zPqQ@7>4VxUTlc@j=l+ZpT--2=DS8c63BlAXaTAyNpM?kCl zHUa*3m0js}Yoe*CrU2O)F~NIeSCEaJ*zpbOiOKnzQbN;ae|uc3nz4!FS(u2!IRVCX z!Jar@_DpbWF`%k!dJoL_hfr4PfyG+)K0OWMtpIM{7Qk)LwFkiG4Z#0Kby~|JFq+cZ zI@|lNd47O?OTF{)G`R^PfP8eI`FUb?B-MDmDrc z#$-Xa#6nSzmkQZ}SOnH_bMXP#GqJK4`_r96z-{(Noijh3j)jH- z$U#CL#0Rk}>~unm@IVY=eP@`ctAyY>KGVggM$+Ej#ZhMfd|!Y$K-}H(_6^U-A^q`aVN9LQ)N~(@BEZ9199yy3l{lVA6>xN$k*v_h7csr*tZ<{Y{w9y3lbN$P(?p3Td zF1Cjdb5s_PU=>g%VqR5<8go{U?sS-Vpnf;HuWQNh@ru_#zuMMGQn{hMg!#i3V%wBCT$gD(xixZFCDX|lOlp@CPl=1 zO+;WDNyd*Gs8AxgAdamhfglJb)kr1)vP1^sEe_o%Kqe)GRV>!iPFgD4!?9Mf9k<9* zpM8lW$6u-D#9p!39*&)%vN&ufr3O3vM!d`29;u-!#eM429*&)qAG72Bu&tRg%`q5z zId)Qxz}i}6)^{^1xh^W~Kl)igrB)OVg-pJZZ({Vv)WqoLiAFzmxyps{vj>+`qo4e_ z8qW-u7`fwOx5cGZKp7kT%mE?>Ik_Yd8aXf?{cL-UJ$Q9oR+Vb>BWlNBybfG4`ZeaG zyoG<~y}Qu=l#NDHQvV-&Zv!OPb=`Nq?w*gH>F$~CnE^1s0O0G!02qP*0g@mH0+hr@ z0Rw^n2-2prTAQ(HTejpG($YW>!c8Osk|j%)WrvaFSaEDiuJ!K9S=Fv9<*IdMoic07 zTah=;E|c}{dL1QZr4(DvRyp!IPP~aM?(cujeIMQZdN`n{k17vXoPHno-H&t5Jzw|S zb8ngMar>M=M8mv0>Aam*RBQ7&XAK$pj5lCvG5n}ooP)U8S6k1ey`d+wFX3pXn*%(7 z>{X?#Xx1`oTL)$}=dnA|4nVf1Zlxq-jWfgRDHNWQerIqn?enCaq>ED?^~IPJgAmcW zEcy)Nq{b?}w~8?vdB@guG@M8Xfg#JPL>-8^InOi=IIsQ$LU8t5XzX5 zBtW1KS~)L+qtWYWQx9dxlvOCBx=jBmJX8VDmwZq1ZJ^}ti%aA~VWn%Odh0oXlszFa zH!nZiLtl<{`76&glngR(T-weQYgs;-GA&bwDb8|{bg@)t$aqByx#)c{B^&O|2jaaH zO?d+Amf}Vr+Zbfvu#_{MZ8F!z{PfIFLHd#DVmvcWU_n~OOYP__WAZsCVEr}K#r$YZ z=cUFCwbTY1R}ZMnB42E~pO7_1zRiLzHfuUV(8U<3ElA7Wz{G`TL~>^tj7^Cum;9V$ zP&VC2muIRyCbI0_d)ZGXw3Aq!EUPBFG;y5NImAV4hAM@w!wh|z3_6?>H&2EYiZ^?+ z>+Hgl)Ma4I1v4$!f#;Sz&ruV5t+pZ<%noxyk4z{ikf3ui#(S8)Wk8d0=OV9yk)xrh zO@y{N7FOgTm@#i`y{R-OuHc6!;t;E?A2KQKI;RGxwaL@QJYX7A_`ES7`9SNrtj_m3 ztF52K)$I0EtNHUZVCE%Cgi^DRJc?dT#T_{~3xt%2iC4M1SGr1gJc^O4Wx zBPS=3k6chtqCXooxZz4>lcfab3bTpX8pdjiWYy9!nmI>T%}8czC0Vu-to*-IY5uH@ z$_#G#enoz6)b`933@JvgSOTjRi0as-*7s@HAWM`WygKS(m(s8B*OQb9o2z|wNj!s% zI@kc_-?dN*Ga+qZW>U_4z}Hh(s}`{jCd^Wbk)dIJRkg4tYFf6Ry;DDF-Jqe zdM!nRi4>Dc+Arv`Qp9r_tqR!C$N)kx0`L4Pkw5Pec5gPnwbCQbO#G|#}LgWxGj7HbND zukXsF%%FrZab$X@?eWe(%QR0HI++$9njVlgcJQM~o1)cONAFBWFd%u{?o7P^p;GCc zI3|j{&oxh%KyG&yy#b^*29(k_KObFeeTT#_6Ufrw5h7xC5%4QDKSu~5rRrlAz~4^V zKDDT!97RVtkLRz5@w_cCo*8F6kZNE&b1)wI;fx24?~F&xrpIn@ltS^A=3j-m#B+r^ zCCOwq5;Aofw#XF`AeUO-NmU#gCz+5lA$|W{rA)}*q^wvx%$U%38WU39N(Ed&PYBcN zg0i{cp(Yr-)FN9I;ACMNngZZI!6%c+FaBHJkiQGtiD!SyssSt?w_G4F64ox$ui**2 zAwtWTl1*<_USE?&(EPZn^X7OG+UZc|i`LGkvv%4%`3PV|NW%!_ZL5fB(7SG)44Luu?i8GTN-LJ@ z@?DBfI&gw;&^w9Zj4jwkZ=30Q;sHzY*SINqt=myhMkhG>sKk^@DR%DY3u;;@d!v%|hg5JAhReB*0(1>+!6wE!6Y11(O!M(ENvK5Rf&KQilbj zsaxiGeNBSkyDbm|rOF8+d|n_3o&a;9uE%Di6UZZ3&&#V)wKC#h97!`D{`-H>f05Woq@o!7BTR3IQwhc;mYDRc0y= zsJ*hW(K-%XW?{iRdD=WQYWZUoTLinuzRW<@xYD|(06Aj3fV)GEHZsV~vh;dFFJf7_&ZG-m%#PgWp`$MAt zO#ws+07O$!m@@Cr;rrBu)?EeYy|3@hk$@gHh#V;t`Ws|Q1seO8@H3j)l-oM%y#>o+ zFLo}xV>Yen0*wm>E;bJB8yxh|t z!b-#a@(7D26$68g4pT`!XpyS4Ns=BdklJJuoRydDGz7_m{msX1pu`Pa56QTRGQlR( z-!vn)nkJs%DbUW91#AFC5J`v@NKQNNza}uHv8>O`jZM}js+Fnn>4@~7O{MB)fsQJa zHy7ov#t(NPas<^ej5I{S^b{YXVWORaG8}BSgu(dqZSqr6r>$%1`=qYD*Y-DL1hh}p zcM&5u?Kj&GvELR_qlzCTSj-^tqZD~9Xgz5?4lxGLD%Wrt&jCDuDk{4jQvDi0zxvwM zsGkAGsyP~_BcPyNqMRdb{s4>~3_~c)Nipp23x!r8IA!PyInSy>iYx*%~=25(`RO=s3@F63Q zkHhH!D*!50g;@nHx>_ZySZ8H^{VO(r3iw)e#q&;rOa50%bA@DL8xy{7$F(RCHbMTA z9v(hNQo!|@y4V%}#IdHd7pIMWgUQC!yt8`i?^kq6sU6My_bY36R_hPcSihx@B_H#w zb|cxlfjcyu&KNUVsC$6Z*!Q^eV3p$)_Ts=&tKGKMxpRGHYC^rK*Og%ts-`zkYMk0A#M}@vd-1BJOb2j`xHsjvG=P{Qxk8u(ed%L~Yy)8;swE-rfKNHhMOeERw}k zEo=Y?g8{o}#0YTQ01U#G$LV}kN$$O&^0@s6HZAKos%(NGaW9$X^VUf&4^iW$RjxNl4Lqj$DC}kEDHEXHBEVP)zgzje+7(@0QkBwEZFyUX~oexF3 zC+L6&X%tw6mVB;pPJrs1GgdM0{TNT{8)6h%+R-QaaLzuQ&$~ZC+C!zL+8TeD3=Ba) zN*RST7>Nn3@-{rgg1i0AM@dr7==7&%u3}RhAC-4&nslaj<8bns^!1hHQs3m$y6j2; zRDnpj&hb-(&9c&fE+%O$>Go7_3OxqNDIrh@oAsx}GEn3qbCB>C51X~_OSt@nof-8Y zBWomNs+`uJ2fs4Sim)J&{4Alg%?oy|bZhtEY50%u*GFT;RZ8Mn7gk#rfgto0FVKOn zM5{W{0Yf#RKfWSRZ{B33iuOez5IsLZwwo)M+bmbaBnbE~G;$D?tD@Q?80(R;R`OHp znr*Ti#@s*rNs4A z?0DRex_KIa_7AQ*1@Dpb%+7!oSb;je7QfOM-e!6@J-O1lGe+Y;?XX_cK?@cnLRx;7 zkRwuh670ZM+=80-)cRws!v>`(|G1J;=#qH1(V zp0W2xJwVR0&e11D0SHo^`lPQP8uycqihrxMsPg#b;*-a<*n-+j@HNrjSCXHG+nb`J zKOX09*j#O05*GPsUMYTYS>@Q~o-zm==$C~j{v5A71}*T$T@9j1&M#5!8-t3QntCb= z^Ka8`^D-|uFoPa|*~bEUg+pGb9wxnh$)<7rn&!R-NnfRFf;xNv`p_^gVNOsTbCJ^h zoqNn#2#DFVlY+@p-5Fn5Rg1I?CZn`v*sbhuITWo~vU#o|Dmgl8Qkr%?ic5u2@WK#; zEw|dWXkxIc8OuzK)zzps#rI5#LE=b?#PNvU(?83wuH@E{0{YEdLh_j2t_fKHfZD4;o@8}rT3DXjXbJz<8pl#wlqsN$9$&Y@ZF4v2Gg>gW< zjN2F?JB?6h-}5y!V9cGFz1ed}u^{y+Bj9=t<%FB zJ;Ldswqs>d{tR}kz_GQVjA+S-&PlqlkK;6Vg2S`AIcGP=q93iG-$417>G1P5sh;I) zlhzC{CfY^>jyO4|KFK>dI9z|gh&

41xZRIV?08Xfl@CNIw=^`q~1anycYz1&(Pu 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 o7hj{Fef)6{f0S(}ukX?G5Hj 1) { + throw new Error( + "Usage: node scripts/install-pcre2-engine.mjs [.engine-build/pcre2]", + ); +} +const source = path.resolve(root, arguments_[0] ?? ".engine-build/pcre2"); +const result = await installPcre2Pack(source, root); +console.log( + `Installed verified PCRE2 ${result.metadata.engineVersion} runtime assets at ${result.output}.`, +); diff --git a/scripts/package-release.mjs b/scripts/package-release.mjs index e3d62aa..6fe7b91 100644 --- a/scripts/package-release.mjs +++ b/scripts/package-release.mjs @@ -18,6 +18,7 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const zipEpoch = new Date(1980, 0, 1, 0, 0, 0); const gplVersion3TextSha256 = "fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0"; +const expectedApplicationVersion = "0.2.0"; const requiredRootFiles = [ "LICENSE", "README.md", @@ -332,13 +333,27 @@ async function collectReleaseEntries() { await readFile(path.join(root, "package.json"), "utf8"), ); if ( - packageJson.version !== "0.1.0" || + packageJson.version !== expectedApplicationVersion || packageJson.license !== "GPL-3.0-or-later" || packageJson.repository?.url !== "git+https://git.add-ideas.de/zemion/regex-tools.git" ) { throw new Error("Package version, licence or source identity drifted."); } + const sourceIdentity = await readFile(path.join(root, "SOURCE.md"), "utf8"); + if ( + !sourceIdentity.includes( + `- Release version: \`${expectedApplicationVersion}\``, + ) || + !sourceIdentity.includes( + `- Release tag: \`v${expectedApplicationVersion}\``, + ) || + !sourceIdentity.includes( + "- Repository: ", + ) + ) { + throw new Error("SOURCE.md release identity drifted."); + } const licence = await readFile(path.join(root, "LICENSE"), "utf8"); if ( !licence.includes("GNU GENERAL PUBLIC LICENSE") || @@ -371,6 +386,14 @@ async function collectReleaseEntries() { "CHANGELOG.md", "SOURCE.md", "THIRD_PARTY_NOTICES.md", + "engines/README.md", + "engines/pcre2/LICENSE.txt", + "engines/pcre2/SHA256SUMS", + "engines/pcre2/engine-metadata.json", + "engines/pcre2/pcre2.mjs", + "engines/pcre2/pcre2.wasm", + "LICENSES/Emscripten-MIT.txt", + "LICENSES/PCRE2.txt", "LICENSES/build/rolldown-1.1.5/LICENSE", "LICENSES/build/vite-8.1.5/LICENSE.md", ]) { diff --git a/scripts/pcre2-codegen-golden.test.ts b/scripts/pcre2-codegen-golden.test.ts new file mode 100644 index 0000000..7963518 --- /dev/null +++ b/scripts/pcre2-codegen-golden.test.ts @@ -0,0 +1,110 @@ +import { createHash } from "node:crypto"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; +import { + generatePcre2C, + type Pcre2CGenerationRequest, +} from "../src/regex/codegen/pcre2-c"; + +function request(operation: "match" | "replace"): Pcre2CGenerationRequest { + return { + flavour: "pcre2", + flavourVersion: "PCRE2 10.47 8-bit WebAssembly", + operation, + pattern: String.raw`(?\p{L}+)-(\d+)`, + flags: ["g"], + options: { + matchLimit: 1_000_000, + depthLimit: 1_000, + heapLimitKib: 32_768, + }, + subject: "x Grüße-42 y alpha-7", + scanAll: true, + ...(operation === "replace" ? { replacement: "$2:$" } : {}), + maximumMatches: 100, + maximumCaptureRows: 1_000, + maximumOutputBytes: 4_096, + }; +} + +function sha256(value: string): string { + return createHash("sha256").update(value).digest("hex"); +} + +const exactToolchainPrefix = process.env.PCRE2_CODEGEN_PREFIX; +const compileWithExactPcre2 = exactToolchainPrefix ? it : it.skip; + +describe("PCRE2 C code-generation golden", () => { + it("retains a reviewed deterministic source identity", () => { + expect(sha256(generatePcre2C(request("match")).source)).toBe( + "22b91d578a449b0ed5c30e6eadad30f19a2bcb26ac9db355c8ec7a69328675da", + ); + expect(sha256(generatePcre2C(request("replace")).source)).toBe( + "ab90a0d9831ea7b7145aa0072ac352b42baf9a795319d81f531b0f341b04a636", + ); + }); + + compileWithExactPcre2( + "compiles and executes against the explicitly supplied PCRE2 10.47 C toolchain", + async () => { + if (!exactToolchainPrefix) return; + const header = path.join(exactToolchainPrefix, "include", "pcre2.h"); + const library = + process.env.PCRE2_CODEGEN_LIBRARY ?? + path.join(exactToolchainPrefix, "lib64", "libpcre2-8.a"); + await expect(readFile(header)).resolves.toBeInstanceOf(Buffer); + await expect(readFile(library)).resolves.toBeInstanceOf(Buffer); + const directory = await mkdtemp( + path.join(tmpdir(), "regex-tools-codegen-"), + ); + try { + for (const operation of ["match", "replace"] as const) { + const generated = generatePcre2C(request(operation)); + const source = path.join(directory, generated.fileName); + const executable = path.join(directory, `pcre2-${operation}`); + await writeFile(source, generated.source); + const compiled = spawnSync( + process.env.CC ?? "cc", + [ + "-std=c17", + "-Wall", + "-Wextra", + "-Werror", + `-I${path.join(exactToolchainPrefix, "include")}`, + source, + library, + "-o", + executable, + ], + { encoding: "utf8" }, + ); + expect( + `${compiled.stdout}${compiled.stderr}`, + `${operation} fixture did not compile`, + ).toBe(""); + expect(compiled.status).toBe(0); + const executed = spawnSync(executable, [], { encoding: "utf8" }); + expect( + executed.status, + `${operation} fixture failed: ${executed.stdout}${executed.stderr}`, + ).toBe(0); + if (operation === "match") { + expect(executed.stdout).toContain("match 1: UTF-8 bytes 2..12"); + expect(executed.stdout).toContain("group 1 / word"); + expect(executed.stdout).toContain( + "completed: 2 match(es), 4 capture row(s)", + ); + } else { + expect(executed.stdout).toBe("x 42:Grüße y 7:alpha"); + expect(executed.stderr).toContain("completed: 2 substitution(s)"); + } + } + } finally { + await rm(directory, { recursive: true, force: true }); + } + }, + ); +}); diff --git a/scripts/pcre2-engine-pack.mjs b/scripts/pcre2-engine-pack.mjs new file mode 100644 index 0000000..fbad0b6 --- /dev/null +++ b/scripts/pcre2-engine-pack.mjs @@ -0,0 +1,1018 @@ +import { spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + access, + constants as fsConstants, + copyFile, + lstat, + mkdir, + mkdtemp, + readFile, + readdir, + realpath, + rename, + rm, + stat, + writeFile, +} from "node:fs/promises"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +export const PCRE2_LOCK = Object.freeze({ + bridgeAbi: 3, + maximumPatternBytes: 1_048_576, + maximumSubjectBytes: 16_777_216, + maximumReplacementBytes: 262_144, + maximumMatches: 10_000, + maximumCaptureRows: 100_000, + maximumCaptureGroups: 1_000, + maximumTraceEvents: 50_000, + maximumTraceBytes: 10_485_760, + maximumTraceMarkBytes: 1_024, + sourceDateEpoch: 1_760_997_684, + source: Object.freeze({ + repository: "https://github.com/PCRE2Project/pcre2.git", + tag: "pcre2-10.47", + tagObject: "cd007b4466798f66d479d1442a407099e7c40050", + commit: "f454e231fe5006dd7ff8f4693fd2b8eb94333429", + tree: "81a83a3552bd68d0ea7b7004f8bb6e7892f583ba", + version: "10.47 2025-10-21", + license: "BSD-3-Clause WITH PCRE2-exception", + criticalFiles: Object.freeze({ + "CMakeLists.txt": + "74f65935e2b1120e7d7aeb4be81755e0f7a875732467ae64a9cc6ee7174fa5ba", + "LICENCE.md": + "197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811", + "src/pcre2.h.generic": + "058462dc030e845ee627e19ac36347f561f5cc44d89eaf1c0010f2b363ab68d5", + }), + }), + emscripten: Object.freeze({ + version: "6.0.4", + compilerRevision: "fe5be6afdff43ad58860d821fcc8572a23f92d19", + repositoryCommit: "224ec5f9f2f72f09f9ce0e26d66bae7dbd8b692f", + emccSha256: + "498eba9b6aacffca1c358ba340fc7b37015d7867cb211c7f2d45784ee9310f03", + emcmakeSha256: + "18c8397cf60835533e0db88d1172e04fa0b753dea68e978149d923a9e6378af3", + }), + cmakeVersion: "4.3.4", + ninjaVersion: "1.13.2", +}); + +const PACK_FILES = Object.freeze([ + "LICENSE.txt", + "SHA256SUMS", + "engine-metadata.json", + "pcre2.mjs", + "pcre2.wasm", +]); +const CHECKSUM_FILES = Object.freeze([ + "LICENSE.txt", + "engine-metadata.json", + "pcre2.mjs", + "pcre2.wasm", +]); +const BRIDGE_SOURCE_FILES = Object.freeze([ + "engines/pcre2/CMakeLists.txt", + "engines/pcre2/pcre2_bridge.c", + "engines/pcre2/pcre2_bridge.h", +]); +const EXPORTED_FUNCTIONS = Object.freeze([ + "_free", + "_malloc", + "_regex_pcre2_bridge_abi_version", + "_regex_pcre2_compile_probe", + "_regex_pcre2_config_flags", + "_regex_pcre2_error_message", + "_regex_pcre2_execute", + "_regex_pcre2_self_test", + "_regex_pcre2_substitute", + "_regex_pcre2_trace", + "_regex_pcre2_version", +]); + +function sha256(data) { + return createHash("sha256").update(data).digest("hex"); +} + +async function sha256File(file) { + return sha256(await readFile(file)); +} + +function commandLabel(command, arguments_) { + return [command, ...arguments_] + .map((part) => + /^[A-Za-z0-9_./:=,+-]+$/u.test(part) ? part : JSON.stringify(part), + ) + .join(" "); +} + +async function run(command, arguments_, options = {}) { + const child = spawn(command, arguments_, { + cwd: options.cwd, + env: options.env, + stdio: ["ignore", "pipe", "pipe"], + }); + const stdout = []; + const stderr = []; + child.stdout.on("data", (chunk) => stdout.push(chunk)); + child.stderr.on("data", (chunk) => stderr.push(chunk)); + + const result = await new Promise((resolve, reject) => { + child.once("error", reject); + child.once("close", (code, signal) => resolve({ code, signal })); + }); + const output = Buffer.concat(stdout).toString("utf8"); + const errorOutput = Buffer.concat(stderr).toString("utf8"); + if (result.code !== 0) { + const detail = [output, errorOutput].filter(Boolean).join("\n").trim(); + throw new Error( + `${commandLabel(command, arguments_)} failed with ${ + result.signal ? `signal ${result.signal}` : `exit code ${result.code}` + }${detail ? `:\n${detail}` : "."}`, + ); + } + return { stdout: output, stderr: errorOutput }; +} + +async function assertRealDirectory(candidate, label) { + const details = await lstat(candidate).catch(() => null); + if (!details?.isDirectory() || details.isSymbolicLink()) { + throw new Error(`${label} must be a real directory: ${candidate}`); + } + return realpath(candidate); +} + +async function assertRegularFile(candidate, label) { + const details = await lstat(candidate).catch(() => null); + if (!details?.isFile() || details.isSymbolicLink()) { + throw new Error( + `${label} must be a regular, non-symlink file: ${candidate}`, + ); + } + return realpath(candidate); +} + +async function assertHash(file, expected, label) { + const actual = await sha256File(file); + if (actual !== expected) { + throw new Error( + `${label} SHA-256 mismatch: expected ${expected}, got ${actual}.`, + ); + } +} + +async function resolveExecutable(name, environment = process.env) { + const searchPath = environment.PATH ?? ""; + for (const directory of searchPath.split(path.delimiter)) { + if (!directory) continue; + const candidate = path.join(directory, name); + try { + await access(candidate, fsConstants.X_OK); + const details = await stat(candidate); + if (details.isFile()) return realpath(candidate); + } catch { + // Continue through PATH. + } + } + throw new Error(`${name} is required on PATH for the explicit PCRE2 build.`); +} + +function singleVersionLine(output) { + return output.trim().split(/\r?\n/u)[0] ?? ""; +} + +async function verifyPcre2Source(sourceDirectory) { + const source = await assertRealDirectory(sourceDirectory, "PCRE2 source"); + const git = await resolveExecutable("git"); + const status = await run( + git, + ["status", "--porcelain=v1", "--untracked-files=all"], + { cwd: source }, + ); + if (status.stdout !== "") { + throw new Error("The PCRE2 source checkout must be completely clean."); + } + + const checks = [ + [["rev-parse", "HEAD"], PCRE2_LOCK.source.commit, "PCRE2 commit"], + [["rev-parse", "HEAD^{tree}"], PCRE2_LOCK.source.tree, "PCRE2 source tree"], + [ + ["rev-parse", `refs/tags/${PCRE2_LOCK.source.tag}^{tag}`], + PCRE2_LOCK.source.tagObject, + "PCRE2 signed tag object", + ], + [ + ["rev-parse", `refs/tags/${PCRE2_LOCK.source.tag}^{commit}`], + PCRE2_LOCK.source.commit, + "PCRE2 tag target", + ], + ]; + for (const [arguments_, expected, label] of checks) { + const { stdout } = await run(git, arguments_, { cwd: source }); + const actual = stdout.trim(); + if (actual !== expected) { + throw new Error( + `${label} mismatch: expected ${expected}, got ${actual}.`, + ); + } + } + + for (const [relativePath, expected] of Object.entries( + PCRE2_LOCK.source.criticalFiles, + )) { + await assertHash( + path.join(source, relativePath), + expected, + `PCRE2 ${relativePath}`, + ); + } + return source; +} + +async function verifyEmscripten(emccFile) { + const emcc = await assertRegularFile(emccFile, "emcc"); + const emcmake = await assertRegularFile( + path.join(path.dirname(emcc), "emcmake"), + "emcmake", + ); + await assertHash(emcc, PCRE2_LOCK.emscripten.emccSha256, "emcc"); + await assertHash(emcmake, PCRE2_LOCK.emscripten.emcmakeSha256, "emcmake"); + + const version = await run(emcc, ["--version"]); + const expectedVersion = + `emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) ` + + `${PCRE2_LOCK.emscripten.version} (${PCRE2_LOCK.emscripten.compilerRevision})`; + if (singleVersionLine(version.stdout) !== expectedVersion) { + throw new Error( + `Emscripten identity mismatch: expected ${expectedVersion}, got ${singleVersionLine( + version.stdout, + )}.`, + ); + } + + const git = await resolveExecutable("git"); + const repository = ( + await run(git, ["rev-parse", "--show-toplevel"], { + cwd: path.dirname(emcc), + }) + ).stdout.trim(); + const commit = ( + await run(git, ["rev-parse", "HEAD"], { cwd: repository }) + ).stdout.trim(); + if (commit !== PCRE2_LOCK.emscripten.repositoryCommit) { + throw new Error( + `Emscripten repository mismatch: expected ${PCRE2_LOCK.emscripten.repositoryCommit}, got ${commit}.`, + ); + } + const tagTarget = ( + await run( + git, + ["rev-parse", `refs/tags/${PCRE2_LOCK.emscripten.version}^{commit}`], + { + cwd: repository, + }, + ) + ).stdout.trim(); + if (tagTarget !== PCRE2_LOCK.emscripten.repositoryCommit) { + throw new Error( + "The Emscripten 6.0.4 tag does not resolve to the pinned commit.", + ); + } + const status = await run( + git, + ["status", "--porcelain=v1", "--untracked-files=all"], + { cwd: repository }, + ); + if (status.stdout !== "") { + throw new Error("The Emscripten checkout must be completely clean."); + } + return { emcc, emcmake }; +} + +async function verifyNativeBuildTools() { + const cmake = await resolveExecutable("cmake"); + const ninja = await resolveExecutable("ninja"); + const cmakeOutput = await run(cmake, ["--version"]); + const ninjaOutput = await run(ninja, ["--version"]); + if ( + singleVersionLine(cmakeOutput.stdout) !== + `cmake version ${PCRE2_LOCK.cmakeVersion}` + ) { + throw new Error( + `The PCRE2 pack requires CMake ${PCRE2_LOCK.cmakeVersion}.`, + ); + } + if (ninjaOutput.stdout.trim() !== PCRE2_LOCK.ninjaVersion) { + throw new Error( + `The PCRE2 pack requires Ninja ${PCRE2_LOCK.ninjaVersion}.`, + ); + } + return { cmake, ninja }; +} + +function parseValue(argv, index, option) { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`${option} requires a path.`); + } + return value; +} + +export function parsePcre2BuildArguments(argv) { + if (argv.length === 0) return { mode: "ecmascript" }; + if (argv.length === 1 && argv[0] === "--help") return { mode: "help" }; + + let pcre2 = false; + let sourceDirectory; + let emccFile; + let force = false; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--pcre2") { + if (pcre2) throw new Error("--pcre2 may be supplied only once."); + pcre2 = true; + } else if (argument === "--source-dir") { + if (sourceDirectory) + throw new Error("--source-dir may be supplied only once."); + sourceDirectory = parseValue(argv, index, argument); + index += 1; + } else if (argument === "--emcc") { + if (emccFile) throw new Error("--emcc may be supplied only once."); + emccFile = parseValue(argv, index, argument); + index += 1; + } else if (argument === "--force") { + if (force) throw new Error("--force may be supplied only once."); + force = true; + } else { + throw new Error(`Unknown engine-build argument: ${argument}`); + } + } + if (!pcre2) { + throw new Error( + "External engine options require the explicit --pcre2 gate.", + ); + } + if (!sourceDirectory || !emccFile) { + throw new Error( + "The offline PCRE2 build requires both --source-dir and --emcc; it never downloads them.", + ); + } + return { mode: "pcre2", sourceDirectory, emccFile, force }; +} + +async function bridgeSourceFiles(root) { + return Promise.all( + BRIDGE_SOURCE_FILES.map(async (relativePath) => { + const contents = await readFile(path.join(root, relativePath)); + return { + path: relativePath, + sha256: sha256(contents), + bytes: contents.byteLength, + }; + }), + ); +} + +async function describedFile(directory, name) { + const contents = await readFile(path.join(directory, name)); + return { path: name, sha256: sha256(contents), bytes: contents.byteLength }; +} + +async function expectedMetadata(root, packDirectory) { + const files = await Promise.all( + ["LICENSE.txt", "pcre2.mjs", "pcre2.wasm"].map((name) => + describedFile(packDirectory, name), + ), + ); + return { + schemaVersion: 1, + engine: "pcre2", + engineVersion: PCRE2_LOCK.source.version, + status: "production-runtime", + bridge: { + abiVersion: PCRE2_LOCK.bridgeAbi, + maximumPatternBytes: PCRE2_LOCK.maximumPatternBytes, + maximumSubjectBytes: PCRE2_LOCK.maximumSubjectBytes, + maximumReplacementBytes: PCRE2_LOCK.maximumReplacementBytes, + maximumMatches: PCRE2_LOCK.maximumMatches, + maximumCaptureRows: PCRE2_LOCK.maximumCaptureRows, + maximumCaptureGroups: PCRE2_LOCK.maximumCaptureGroups, + maximumTraceEvents: PCRE2_LOCK.maximumTraceEvents, + maximumTraceBytes: PCRE2_LOCK.maximumTraceBytes, + maximumTraceMarkBytes: PCRE2_LOCK.maximumTraceMarkBytes, + sourceFiles: await bridgeSourceFiles(root), + exports: [...EXPORTED_FUNCTIONS], + }, + source: { + repository: PCRE2_LOCK.source.repository, + tag: PCRE2_LOCK.source.tag, + tagObjectSha1: PCRE2_LOCK.source.tagObject, + commitSha1: PCRE2_LOCK.source.commit, + treeSha1: PCRE2_LOCK.source.tree, + license: PCRE2_LOCK.source.license, + }, + toolchain: { + emscriptenVersion: PCRE2_LOCK.emscripten.version, + emscriptenCompilerRevision: PCRE2_LOCK.emscripten.compilerRevision, + emsdkCommitSha1: PCRE2_LOCK.emscripten.repositoryCommit, + cmakeVersion: PCRE2_LOCK.cmakeVersion, + ninjaVersion: PCRE2_LOCK.ninjaVersion, + }, + configuration: { + codeUnitWidth: 8, + unicode: true, + jit: false, + threads: false, + filesystem: false, + initialMemoryBytes: 16_777_216, + maximumMemoryBytes: 268_435_456, + }, + sourceDateEpoch: PCRE2_LOCK.sourceDateEpoch, + files, + }; +} + +function deterministicJson(value) { + return `${JSON.stringify(value, null, 2)}\n`; +} + +async function expectedChecksums(packDirectory) { + const entries = await Promise.all( + CHECKSUM_FILES.map( + async (name) => + `${await sha256File(path.join(packDirectory, name))} ${name}`, + ), + ); + return `${entries.join("\n")}\n`; +} + +function equalJson(left, right) { + return JSON.stringify(left) === JSON.stringify(right); +} + +async function smokePcre2Pack(packDirectory) { + const wasm = await readFile(path.join(packDirectory, "pcre2.wasm")); + if ( + wasm.byteLength < 8 || + !wasm.subarray(0, 4).equals(Buffer.from([0x00, 0x61, 0x73, 0x6d])) || + !WebAssembly.validate(wasm) + ) { + throw new Error("pcre2.wasm is not a valid WebAssembly module."); + } + + const moduleUrl = `${pathToFileURL(path.join(packDirectory, "pcre2.mjs")).href}?sha256=${sha256( + await readFile(path.join(packDirectory, "pcre2.mjs")), + )}`; + const imported = await import(moduleUrl); + if (typeof imported.default !== "function") { + throw new Error("pcre2.mjs does not export its Emscripten factory."); + } + const diagnostics = []; + const module = await imported.default({ + locateFile(name) { + return path.join(packDirectory, name); + }, + print() {}, + printErr(line) { + diagnostics.push(String(line)); + }, + }); + + if (module._regex_pcre2_bridge_abi_version() !== PCRE2_LOCK.bridgeAbi) { + throw new Error("The WebAssembly bridge ABI version is incorrect."); + } + if (module._regex_pcre2_config_flags() !== 1) { + throw new Error( + "The WebAssembly pack must have Unicode enabled and JIT disabled.", + ); + } + if ( + module.UTF8ToString(module._regex_pcre2_version()) !== + PCRE2_LOCK.source.version + ) { + throw new Error("The WebAssembly PCRE2 version is incorrect."); + } + if (module._regex_pcre2_self_test() !== 0) { + throw new Error( + `The WebAssembly bridge self-test failed: ${diagnostics.join("\n")}`, + ); + } + + const pattern = new TextEncoder().encode("(?\\p{L}+)-(?\\d+)"); + const patternPointer = module._malloc(pattern.byteLength); + const resultPointer = module._malloc(20); + if (!patternPointer || !resultPointer) { + if (patternPointer) module._free(patternPointer); + if (resultPointer) module._free(resultPointer); + throw new Error("The WebAssembly smoke test could not allocate memory."); + } + try { + module.HEAPU8.set(pattern, patternPointer); + module.HEAPU8.fill(0, resultPointer, resultPointer + 20); + const status = module._regex_pcre2_compile_probe( + patternPointer, + pattern.byteLength, + 0x20 | 0x40, + resultPointer, + ); + const result = new DataView(module.HEAPU8.buffer, resultPointer, 20); + if ( + status !== 0 || + result.getInt32(0, true) !== 0 || + result.getUint32(8, true) !== 2 || + result.getUint32(12, true) !== 2 + ) { + throw new Error("The WebAssembly compile-probe result is incorrect."); + } + + module.HEAPU8.fill(0, resultPointer, resultPointer + 20); + const unsupportedStatus = module._regex_pcre2_compile_probe( + patternPointer, + pattern.byteLength, + 0x80000000, + resultPointer, + ); + if (unsupportedStatus !== -10002 || result.getInt32(0, true) !== -10002) { + throw new Error("The WebAssembly bridge accepted an unknown option bit."); + } + + module.HEAPU8[patternPointer] = 0x28; + module.HEAPU8.fill(0, resultPointer, resultPointer + 20); + const invalidStatus = module._regex_pcre2_compile_probe( + patternPointer, + 1, + 0, + resultPointer, + ); + if ( + invalidStatus <= 0 || + result.getInt32(0, true) !== invalidStatus || + result.getUint32(4, true) > 1 + ) { + throw new Error( + "The WebAssembly bridge did not preserve the compile error.", + ); + } + + if ( + module._regex_pcre2_compile_probe(0, 1, 0, resultPointer) !== -10001 || + module._regex_pcre2_compile_probe( + patternPointer, + PCRE2_LOCK.maximumPatternBytes + 1, + 0, + resultPointer, + ) !== -10003 + ) { + throw new Error("The WebAssembly bridge input limits are not enforced."); + } + } finally { + module._free(resultPointer); + module._free(patternPointer); + } + + const runPattern = new TextEncoder().encode("(?😀)|(?\\p{L}+)"); + const runSubject = new TextEncoder().encode("x😀 Grüße"); + const runPointers = []; + const allocate = (bytes) => { + const pointer = module._malloc(Math.max(1, bytes)); + if (!pointer) throw new Error("The PCRE2 runtime smoke allocation failed."); + runPointers.push(pointer); + return pointer; + }; + try { + const runPatternPointer = allocate(runPattern.byteLength); + const runSubjectPointer = allocate(runSubject.byteLength); + const limitsPointer = allocate(20); + const recordsPointer = allocate(20 * 16); + const namesPointer = allocate(10 * 12); + const nameBytesPointer = allocate(256); + const runResultPointer = allocate(60); + module.HEAPU8.set(runPattern, runPatternPointer); + module.HEAPU8.set(runSubject, runSubjectPointer); + let view = new DataView(module.HEAPU8.buffer); + view.setUint32(limitsPointer, 10, true); + view.setUint32(limitsPointer + 4, 100, true); + view.setUint32(limitsPointer + 8, 1_000_000, true); + view.setUint32(limitsPointer + 12, 1_000, true); + view.setUint32(limitsPointer + 16, 32_768, true); + module.HEAPU8.fill(0, runResultPointer, runResultPointer + 60); + const runStatus = module._regex_pcre2_execute( + runPatternPointer, + runPattern.byteLength, + runSubjectPointer, + runSubject.byteLength, + 0x20 | 0x40 | 0x100, + limitsPointer, + recordsPointer, + 20, + namesPointer, + 10, + nameBytesPointer, + 256, + runResultPointer, + ); + view = new DataView(module.HEAPU8.buffer); + if ( + runStatus !== 0 || + view.getInt32(runResultPointer, true) !== 0 || + view.getUint32(runResultPointer + 12, true) !== 2 || + view.getUint32(runResultPointer + 20, true) !== 3 || + view.getUint32(runResultPointer + 24, true) !== 9 || + view.getUint32(recordsPointer + 3 * 16 + 8, true) !== 1 || + view.getUint32(recordsPointer + 3 * 16 + 12, true) !== 5 + ) { + throw new Error("The WebAssembly bounded-match smoke result is wrong."); + } + + const replacementPattern = new TextEncoder().encode("(?\\p{L}+)"); + const replacementSubject = new TextEncoder().encode("Grüße 42"); + const replacement = new TextEncoder().encode("${word}!"); + const replacementPatternPointer = allocate(replacementPattern.byteLength); + const replacementSubjectPointer = allocate(replacementSubject.byteLength); + const replacementPointer = allocate(replacement.byteLength); + const outputPointer = allocate(65); + module.HEAPU8.set(replacementPattern, replacementPatternPointer); + module.HEAPU8.set(replacementSubject, replacementSubjectPointer); + module.HEAPU8.set(replacement, replacementPointer); + module.HEAPU8.fill(0, runResultPointer, runResultPointer + 60); + const substituteStatus = module._regex_pcre2_substitute( + replacementPatternPointer, + replacementPattern.byteLength, + replacementSubjectPointer, + replacementSubject.byteLength, + replacementPointer, + replacement.byteLength, + 0x20 | 0x40 | 0x100, + limitsPointer, + recordsPointer, + 20, + namesPointer, + 10, + nameBytesPointer, + 256, + outputPointer, + 64, + runResultPointer, + ); + view = new DataView(module.HEAPU8.buffer); + const outputLength = view.getUint32(runResultPointer + 48, true); + const output = new TextDecoder().decode( + module.HEAPU8.slice(outputPointer, outputPointer + outputLength), + ); + if ( + substituteStatus !== 0 || + output !== "Grüße! 42" || + view.getUint32(runResultPointer + 52, true) !== 1 || + view.getUint32(runResultPointer + 56, true) !== 0 + ) { + throw new Error( + "The WebAssembly bounded-substitution smoke result is wrong.", + ); + } + module.HEAPU8.fill(0, runResultPointer, runResultPointer + 60); + const missingOutputStatus = module._regex_pcre2_substitute( + replacementPatternPointer, + replacementPattern.byteLength, + replacementSubjectPointer, + replacementSubject.byteLength, + replacementPointer, + replacement.byteLength, + 0x20 | 0x40, + limitsPointer, + recordsPointer, + 20, + namesPointer, + 10, + nameBytesPointer, + 256, + 0, + 0, + runResultPointer, + ); + if ( + missingOutputStatus !== -10001 || + view.getInt32(runResultPointer, true) !== -10001 + ) { + throw new Error( + "The WebAssembly substitution bridge accepted a missing NUL byte.", + ); + } + + const traceEventsPointer = allocate(64 * 32); + const traceMarksPointer = allocate(256); + const traceResultPointer = allocate(52); + view.setUint32(limitsPointer, 64, true); + view.setUint32(limitsPointer + 4, 4_096, true); + module.HEAPU8.fill(0, traceResultPointer, traceResultPointer + 52); + const traceStatus = module._regex_pcre2_trace( + runPatternPointer, + runPattern.byteLength, + runSubjectPointer, + runSubject.byteLength, + 0x20 | 0x40, + limitsPointer, + traceEventsPointer, + 64, + traceMarksPointer, + 256, + traceResultPointer, + ); + view = new DataView(module.HEAPU8.buffer); + const traceEventCount = view.getUint32(traceResultPointer + 20, true); + if ( + traceStatus !== 0 || + view.getInt32(traceResultPointer, true) !== 0 || + view.getInt32(traceResultPointer + 12, true) <= 0 || + traceEventCount === 0 || + view.getUint32(traceResultPointer + 24, true) !== traceEventCount || + view.getUint32(traceResultPointer + 36, true) !== 0 || + view.getUint32(traceResultPointer + 44, true) !== traceEventCount - 1 || + view.getUint32(traceResultPointer + 48, true) !== 1 + ) { + throw new Error("The WebAssembly automatic-callout trace is wrong."); + } + + view.setUint32(limitsPointer, 1, true); + module.HEAPU8.fill(0, traceResultPointer, traceResultPointer + 52); + const boundedTraceStatus = module._regex_pcre2_trace( + runPatternPointer, + runPattern.byteLength, + runSubjectPointer, + runSubject.byteLength, + 0x20 | 0x40, + limitsPointer, + traceEventsPointer, + 64, + traceMarksPointer, + 256, + traceResultPointer, + ); + if ( + boundedTraceStatus !== 0 || + view.getInt32(traceResultPointer, true) !== 0 || + view.getInt32(traceResultPointer + 12, true) !== -37 || + view.getUint32(traceResultPointer + 20, true) !== 1 || + view.getUint32(traceResultPointer + 24, true) !== 2 || + view.getUint32(traceResultPointer + 36, true) !== 1 || + view.getUint32(traceResultPointer + 44, true) !== 0 + ) { + throw new Error("The WebAssembly trace cap is not authoritative."); + } + } finally { + for (const pointer of runPointers.reverse()) module._free(pointer); + } +} + +export async function verifyPcre2Pack(packDirectory, root) { + const repositoryRoot = await assertRealDirectory( + root, + "Regex Tools repository", + ); + const pack = await assertRealDirectory(packDirectory, "PCRE2 pack"); + const entries = (await readdir(pack, { withFileTypes: true })).sort( + (left, right) => + left.name < right.name ? -1 : left.name > right.name ? 1 : 0, + ); + if ( + entries.length !== PACK_FILES.length || + entries.some( + (entry, index) => + entry.name !== PACK_FILES[index] || + !entry.isFile() || + entry.isSymbolicLink(), + ) + ) { + throw new Error( + `The staged PCRE2 pack must contain only: ${PACK_FILES.join(", ")}.`, + ); + } + + await assertHash( + path.join(pack, "LICENSE.txt"), + PCRE2_LOCK.source.criticalFiles["LICENCE.md"], + "staged PCRE2 licence", + ); + const moduleText = await readFile(path.join(pack, "pcre2.mjs"), "utf8"); + if ( + moduleText.includes("sourceMappingURL") || + moduleText.includes("/mnt/") || + moduleText.includes("/home/") + ) { + throw new Error("pcre2.mjs leaks a build path or source-map reference."); + } + + let metadata; + try { + metadata = JSON.parse( + await readFile(path.join(pack, "engine-metadata.json"), "utf8"), + ); + } catch (error) { + throw new Error("engine-metadata.json is not valid JSON.", { + cause: error, + }); + } + const expected = await expectedMetadata(repositoryRoot, pack); + if (!equalJson(metadata, expected)) { + throw new Error( + "The staged PCRE2 metadata does not match the pinned build contract.", + ); + } + const checksums = await readFile(path.join(pack, "SHA256SUMS"), "utf8"); + if (checksums !== (await expectedChecksums(pack))) { + throw new Error("The staged PCRE2 SHA256SUMS file is incorrect."); + } + await smokePcre2Pack(pack); + return metadata; +} + +export async function installPcre2Pack(packDirectory, root) { + const repositoryRoot = await assertRealDirectory( + root, + "Regex Tools repository", + ); + const sourcePack = await assertRealDirectory( + packDirectory, + "verified PCRE2 pack", + ); + const metadata = await verifyPcre2Pack(sourcePack, repositoryRoot); + const engineRoot = await assertRealDirectory( + path.join(repositoryRoot, "public", "engines"), + "public engine directory", + ); + const target = path.join(engineRoot, "pcre2"); + const stage = await mkdtemp(path.join(engineRoot, ".pcre2-install-")); + const targetDetails = await lstat(target).catch(() => null); + if ( + targetDetails && + (!targetDetails.isDirectory() || targetDetails.isSymbolicLink()) + ) { + await rm(stage, { recursive: true, force: true }); + throw new Error("public/engines/pcre2 must be a real directory."); + } + try { + for (const name of PACK_FILES) { + await copyFile( + path.join(sourcePack, name), + path.join(stage, name), + fsConstants.COPYFILE_EXCL, + ); + } + await verifyPcre2Pack(stage, repositoryRoot); + if (targetDetails) { + const backup = path.join(engineRoot, `.pcre2-replaced-${process.pid}`); + await rename(target, backup); + try { + await rename(stage, target); + } catch (error) { + await rename(backup, target); + throw error; + } + await rm(backup, { recursive: true }); + } else { + await rename(stage, target); + } + return { output: target, metadata }; + } finally { + await rm(stage, { recursive: true, force: true }); + } +} + +function cleanBuildEnvironment() { + const environment = { ...process.env }; + for (const variable of [ + "CFLAGS", + "CPPFLAGS", + "CXXFLAGS", + "DESTDIR", + "EMCC_CFLAGS", + "EMMAKEN_CFLAGS", + "LDFLAGS", + ]) { + delete environment[variable]; + } + environment.LANG = "C"; + environment.LC_ALL = "C"; + environment.SOURCE_DATE_EPOCH = String(PCRE2_LOCK.sourceDateEpoch); + environment.TZ = "UTC"; + environment.ZERO_AR_DATE = "1"; + return environment; +} + +async function ensurePrivateBuildRoot(root) { + const buildRoot = path.join(root, ".engine-build"); + const existing = await lstat(buildRoot).catch(() => null); + if (existing && (!existing.isDirectory() || existing.isSymbolicLink())) { + throw new Error(".engine-build must be a real directory."); + } + await mkdir(buildRoot, { recursive: true }); + return realpath(buildRoot); +} + +function assertGeneratedChild(buildRoot, candidate) { + const relative = path.relative(buildRoot, candidate); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + throw new Error( + `Refusing to remove a path outside .engine-build: ${candidate}`, + ); + } +} + +export async function buildPcre2Pack(options, root) { + const repositoryRoot = await assertRealDirectory( + root, + "Regex Tools repository", + ); + const source = await verifyPcre2Source(options.sourceDirectory); + const { emcmake } = await verifyEmscripten(options.emccFile); + const { cmake, ninja } = await verifyNativeBuildTools(); + const buildRoot = await ensurePrivateBuildRoot(repositoryRoot); + const output = path.join(buildRoot, "pcre2"); + const outputDetails = await lstat(output).catch(() => null); + if (outputDetails && !options.force) { + throw new Error( + `${output} already exists; pass --force to replace that exact staged pack.`, + ); + } + + const work = await mkdtemp(path.join(buildRoot, ".pcre2-work-")); + const stage = await mkdtemp(path.join(buildRoot, ".pcre2-stage-")); + assertGeneratedChild(buildRoot, work); + assertGeneratedChild(buildRoot, stage); + const environment = cleanBuildEnvironment(); + try { + await run( + emcmake, + [ + cmake, + "-S", + path.join(repositoryRoot, "engines", "pcre2"), + "-B", + work, + "-G", + "Ninja", + "-DCMAKE_BUILD_TYPE=Release", + `-DCMAKE_MAKE_PROGRAM=${ninja}`, + `-DPCRE2_SOURCE_DIR=${source}`, + `-DREGEX_PCRE2_OUTPUT_DIR=${stage}`, + ], + { cwd: repositoryRoot, env: environment }, + ); + await run( + cmake, + ["--build", work, "--target", "regex-pcre2-pack", "--parallel", "1"], + { + cwd: repositoryRoot, + env: environment, + }, + ); + + const generatedEntries = (await readdir(stage)).sort(); + if ( + generatedEntries.length !== 2 || + generatedEntries[0] !== "pcre2.mjs" || + generatedEntries[1] !== "pcre2.wasm" + ) { + throw new Error( + `Emscripten produced unexpected files: ${generatedEntries.join(", ") || "(none)"}.`, + ); + } + await copyFile( + path.join(source, "LICENCE.md"), + path.join(stage, "LICENSE.txt"), + fsConstants.COPYFILE_EXCL, + ); + const metadata = await expectedMetadata(repositoryRoot, stage); + await writeFile( + path.join(stage, "engine-metadata.json"), + deterministicJson(metadata), + { encoding: "utf8", flag: "wx" }, + ); + await writeFile( + path.join(stage, "SHA256SUMS"), + await expectedChecksums(stage), + { encoding: "utf8", flag: "wx" }, + ); + await verifyPcre2Pack(stage, repositoryRoot); + + if (outputDetails) { + const backup = path.join(buildRoot, `.pcre2-replaced-${process.pid}`); + assertGeneratedChild(buildRoot, backup); + await rename(output, backup); + try { + await rename(stage, output); + } catch (error) { + await rename(backup, output); + throw error; + } + await rm(backup, { recursive: true }); + } else { + await rename(stage, output); + } + return { output, metadata }; + } finally { + await rm(work, { recursive: true, force: true }); + await rm(stage, { recursive: true, force: true }); + } +} diff --git a/scripts/pcre2-engine-pack.test.ts b/scripts/pcre2-engine-pack.test.ts new file mode 100644 index 0000000..28294fc --- /dev/null +++ b/scripts/pcre2-engine-pack.test.ts @@ -0,0 +1,89 @@ +// @vitest-environment node + +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + PCRE2_LOCK, + parsePcre2BuildArguments, + verifyPcre2Pack, +} from "./pcre2-engine-pack.mjs"; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +describe("PCRE2 engine-pack gate", () => { + it("keeps the default build on the checked-in production-assets path", () => { + expect(parsePcre2BuildArguments([])).toEqual({ mode: "ecmascript" }); + }); + + it("requires explicit offline source and compiler paths", () => { + expect(() => parsePcre2BuildArguments(["--pcre2"])).toThrow( + "requires both --source-dir and --emcc", + ); + expect(() => + parsePcre2BuildArguments([ + "--source-dir", + "/source", + "--emcc", + "/compiler", + ]), + ).toThrow("explicit --pcre2 gate"); + }); + + it("parses the exact staged-build contract", () => { + expect( + parsePcre2BuildArguments([ + "--pcre2", + "--source-dir", + "/source", + "--emcc", + "/compiler", + "--force", + ]), + ).toEqual({ + mode: "pcre2", + sourceDirectory: "/source", + emccFile: "/compiler", + force: true, + }); + }); + + it.each([ + ["--unknown"], + ["--pcre2", "--pcre2"], + ["--pcre2", "--source-dir"], + ["--pcre2", "--force", "--force"], + ])("rejects ambiguous arguments %j", (...arguments_) => { + expect(() => parsePcre2BuildArguments(arguments_)).toThrow(); + }); + + it("pins source, signed-tag object and compiler identities", () => { + expect(PCRE2_LOCK.source.tag).toBe("pcre2-10.47"); + expect(PCRE2_LOCK.source.tagObject).toHaveLength(40); + expect(PCRE2_LOCK.source.commit).toHaveLength(40); + expect(PCRE2_LOCK.source.tree).toHaveLength(40); + expect(PCRE2_LOCK.emscripten.version).toBe("6.0.4"); + expect(PCRE2_LOCK.emscripten.emccSha256).toHaveLength(64); + expect(PCRE2_LOCK.bridgeAbi).toBe(3); + expect(PCRE2_LOCK.maximumSubjectBytes).toBe(16 * 1024 * 1024); + expect(PCRE2_LOCK.maximumMatches).toBe(10_000); + }); + + it("rejects an incomplete staged pack before attempting to load it", async () => { + const directory = await mkdtemp(path.join(tmpdir(), "regex-pcre2-test-")); + temporaryDirectories.push(directory); + + await expect(verifyPcre2Pack(directory, process.cwd())).rejects.toThrow( + "must contain only", + ); + }); +}); diff --git a/scripts/serve-test.mjs b/scripts/serve-test.mjs index f888692..90db867 100644 --- a/scripts/serve-test.mjs +++ b/scripts/serve-test.mjs @@ -13,6 +13,7 @@ const mediaTypes = new Map([ [".css", "text/css; charset=utf-8"], [".html", "text/html; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], + [".mjs", "text/javascript; charset=utf-8"], [".json", "application/json; charset=utf-8"], [".svg", "image/svg+xml"], [".wasm", "application/wasm"], diff --git a/scripts/verify-engine-assets.mjs b/scripts/verify-engine-assets.mjs index 363ee04..bacda69 100644 --- a/scripts/verify-engine-assets.mjs +++ b/scripts/verify-engine-assets.mjs @@ -1,8 +1,25 @@ import { lstat, readFile, readdir } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { verifyPcre2Pack } from "./pcre2-engine-pack.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const arguments_ = process.argv.slice(2); + +if (arguments_.length > 0) { + if (arguments_.length !== 2 || arguments_[0] !== "--pcre2-pack") { + throw new Error( + "Usage: node scripts/verify-engine-assets.mjs [--pcre2-pack PATH]", + ); + } + const pack = path.resolve(root, arguments_[1]); + const metadata = await verifyPcre2Pack(pack, root); + console.log( + `Verified staged PCRE2 ${metadata.engineVersion} pack and WebAssembly bridge smoke test.`, + ); + process.exit(0); +} + const engineDirectory = path.join(root, "dist", "engines"); const details = await lstat(engineDirectory); if (details.isSymbolicLink() || !details.isDirectory()) { @@ -14,21 +31,21 @@ const entries = (await readdir(engineDirectory, { withFileTypes: true })).sort( left.name < right.name ? -1 : left.name > right.name ? 1 : 0, ); if ( - entries.length !== 1 || + entries.length !== 2 || !entries[0]?.isFile() || - entries[0].name !== "README.md" + entries[0].name !== "README.md" || + !entries[1]?.isDirectory() || + entries[1].name !== "pcre2" ) { throw new Error( - "Regex Tools 0.1.0 must not ship an undeclared external engine pack.", + "The production build must contain only README.md and the declared PCRE2 pack.", ); } const notice = await readFile(path.join(engineDirectory, "README.md"), "utf8"); -if ( - !notice.includes("native `RegExp`") || - !notice.includes("no external engine") -) { +if (!notice.includes("native `RegExp`") || !notice.includes("PCRE2 10.47")) { throw new Error("The engine asset notice does not describe this release."); } +await verifyPcre2Pack(path.join(engineDirectory, "pcre2"), root); -console.log("Verified ECMAScript-only engine assets (no WebAssembly pack)."); +console.log("Verified native ECMAScript and bundled PCRE2 engine assets."); diff --git a/src/components/AnalysisPanel.css b/src/components/AnalysisPanel.css new file mode 100644 index 0000000..2408fb1 --- /dev/null +++ b/src/components/AnalysisPanel.css @@ -0,0 +1,521 @@ +.analysis-dialog { + width: min(90rem, calc(100% - 2rem)); +} + +.analysis-panel { + --analysis-high: var(--toolbox-danger); + --analysis-warning: var(--regex-warning); + --analysis-notice: var(--toolbox-accent); + min-height: 0; +} + +.analysis-view-tabs { + display: flex; + gap: 0.25rem; + padding: 0.65rem 0.75rem 0; + border-bottom: 1px solid var(--toolbox-border); + background: var(--toolbox-surface-soft); +} + +.analysis-view-tabs button { + border: 1px solid transparent; + border-radius: calc(var(--toolbox-radius) * 0.7) + calc(var(--toolbox-radius) * 0.7) 0 0; + padding: 0.55rem 0.8rem; + background: transparent; + color: var(--toolbox-muted); + font-weight: 750; +} + +.analysis-view-tabs button[aria-selected="true"] { + border-color: var(--toolbox-border); + border-bottom-color: var(--toolbox-surface); + background: var(--toolbox-surface); + color: var(--toolbox-accent); +} + +.analysis-content { + display: grid; + gap: 0.8rem; + padding: 0.75rem; +} + +.analysis-section, +.analysis-result-block { + display: grid; + gap: 0.75rem; + border: 1px solid var(--toolbox-border); + border-radius: calc(var(--toolbox-radius) * 0.82); + background: var(--toolbox-surface); +} + +.analysis-section > header, +.analysis-result-block > header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.75rem; + border-bottom: 1px solid var(--toolbox-border); + background: var(--toolbox-surface-soft); +} + +.analysis-section > header h3, +.analysis-result-block > header h3 { + margin: 0.12rem 0 0; + font-size: 0.96rem; +} + +.analysis-section > header > span, +.analysis-result-block > header > span { + color: var(--toolbox-muted); + font-size: 0.72rem; + font-weight: 700; + text-align: right; +} + +.analysis-summary, +.analysis-advisory, +.analysis-stop-reason, +.analysis-error, +.analysis-unavailable { + margin: 0; + padding: 0.7rem 0.75rem; + color: var(--toolbox-muted); + font-size: 0.79rem; + line-height: 1.5; +} + +.analysis-advisory, +.analysis-stop-reason { + border-left: 3px solid var(--toolbox-accent); + background: var(--toolbox-accent-soft); +} + +.analysis-unavailable, +.analysis-error { + margin: 0.75rem 0.75rem 0; + border: 1px solid + color-mix(in srgb, var(--toolbox-danger) 45%, var(--toolbox-border)); + border-radius: calc(var(--toolbox-radius) * 0.72); + background: color-mix( + in srgb, + var(--toolbox-danger) 8%, + var(--toolbox-surface) + ); + color: color-mix(in srgb, var(--toolbox-danger) 78%, var(--toolbox-text)); +} + +.analysis-status { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 0.6rem; + margin: 0.75rem 0.75rem 0; + padding: 0.55rem 0.7rem; + border: 1px solid var(--toolbox-border); + border-radius: calc(var(--toolbox-radius) * 0.72); + background: var(--toolbox-surface-soft); + font-size: 0.78rem; +} + +.analysis-status > span { + border-radius: 99rem; + padding: 0.2rem 0.42rem; + background: var(--toolbox-accent-soft); + color: var(--toolbox-accent); + font-size: 0.65rem; + font-weight: 800; + text-transform: uppercase; +} + +.analysis-status > strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.analysis-progress { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 0.6rem; + margin: 0.5rem 0.75rem 0; + color: var(--toolbox-muted); + font-size: 0.7rem; +} + +.analysis-progress progress { + width: 100%; + accent-color: var(--toolbox-accent); +} + +.analysis-findings { + display: grid; + gap: 0.55rem; + padding: 0 0.75rem; +} + +.analysis-finding { + overflow: hidden; + border: 1px solid var(--toolbox-border); + border-left-width: 4px; + border-radius: calc(var(--toolbox-radius) * 0.72); + background: var(--toolbox-surface-soft); +} + +.analysis-finding.is-high { + border-left-color: var(--analysis-high); +} + +.analysis-finding.is-warning { + border-left-color: var(--analysis-warning); +} + +.analysis-finding.is-notice { + border-left-color: var(--analysis-notice); +} + +.analysis-finding-title { + display: grid; + width: 100%; + grid-template-columns: auto minmax(0, 1fr); + align-items: start; + gap: 0.55rem; + border: 0; + padding: 0.65rem; + background: transparent; + color: var(--toolbox-text); + text-align: left; +} + +button.analysis-finding-title:hover { + background: var(--toolbox-accent-soft); +} + +.analysis-finding-title > span:last-child { + display: grid; + gap: 0.12rem; +} + +.analysis-finding-title small { + color: var(--toolbox-muted); + font-size: 0.67rem; + font-weight: 500; +} + +.analysis-risk-level, +.analysis-run-badge { + border: 1px solid currentColor; + border-radius: 99rem; + padding: 0.2rem 0.42rem; + font-size: 0.62rem; + font-weight: 850; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.analysis-risk-level.is-high { + color: var(--analysis-high); +} + +.analysis-risk-level.is-warning { + color: color-mix(in srgb, var(--analysis-warning) 78%, var(--toolbox-text)); +} + +.analysis-risk-level.is-notice { + color: var(--analysis-notice); +} + +.analysis-run-badge.is-complete { + color: color-mix(in srgb, var(--regex-mint) 72%, var(--toolbox-text)); +} + +.analysis-run-badge.is-timeout, +.analysis-run-badge.is-error { + color: var(--toolbox-danger); +} + +.analysis-run-badge.is-cancelled, +.analysis-run-badge.is-wall-time-limit { + color: var(--analysis-warning); +} + +.analysis-finding > p { + padding: 0 0.65rem 0.65rem; + color: var(--toolbox-text); + font-size: 0.78rem; + line-height: 1.45; +} + +.analysis-finding dl { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); + margin: 0; + border-top: 1px solid var(--toolbox-border); +} + +.analysis-finding dl > div { + padding: 0.6rem; +} + +.analysis-finding dt, +.analysis-result-meta dt, +.analysis-input-summary dt { + color: var(--toolbox-muted); + font-size: 0.64rem; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.analysis-finding dd, +.analysis-result-meta dd, +.analysis-input-summary dd { + margin: 0.2rem 0 0; + overflow-wrap: anywhere; + font-size: 0.74rem; + line-height: 1.4; +} + +.analysis-details { + margin: 0 0.75rem 0.75rem; + color: var(--toolbox-muted); + font-size: 0.75rem; +} + +.analysis-details summary { + cursor: pointer; + font-weight: 750; +} + +.analysis-details li, +.analysis-limitations li { + margin-block: 0.35rem; + line-height: 1.4; +} + +.analysis-growth-text-controls, +.analysis-number-controls { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + gap: 0.55rem; + padding: 0 0.75rem; +} + +.analysis-growth-text-controls > label, +.analysis-number-controls > label { + display: grid; + align-content: start; + gap: 0.25rem; +} + +.analysis-growth-text-controls label > span, +.analysis-number-controls label > span { + color: var(--toolbox-muted); + font-size: 0.68rem; + font-weight: 750; +} + +.analysis-run-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0 0.75rem 0.75rem; +} + +.analysis-result-block { + margin: 0 0.75rem 0.75rem; +} + +.analysis-result-meta, +.analysis-input-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + margin: 0; + padding: 0 0.75rem; +} + +.analysis-result-meta > div, +.analysis-input-summary > div { + padding: 0.55rem; + border-bottom: 1px solid var(--toolbox-border); +} + +.analysis-table-scroll { + overflow: auto; + border-block: 1px solid var(--toolbox-border); +} + +.analysis-table-scroll table { + min-width: 48rem; +} + +.analysis-table-scroll th, +.analysis-table-scroll td { + white-space: nowrap; +} + +.analysis-chart-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + gap: 0.65rem; + padding: 0 0.75rem; +} + +.analysis-chart { + display: grid; + min-width: 0; + gap: 0.5rem; + margin: 0; + padding: 0.65rem; + border: 1px solid var(--toolbox-border); + border-radius: calc(var(--toolbox-radius) * 0.72); + background: var(--toolbox-surface-soft); +} + +.analysis-chart figcaption { + display: grid; + gap: 0.12rem; +} + +.analysis-chart figcaption strong { + font-size: 0.78rem; +} + +.analysis-chart figcaption span { + color: var(--toolbox-muted); + font-size: 0.65rem; +} + +.analysis-chart svg { + width: 100%; + height: 10rem; + overflow: visible; +} + +.analysis-chart-axis { + fill: none; + stroke: var(--toolbox-border); + stroke-width: 1; + vector-effect: non-scaling-stroke; +} + +.analysis-chart-line { + fill: none; + stroke: var(--toolbox-accent); + stroke-width: 2; + vector-effect: non-scaling-stroke; +} + +.analysis-chart circle { + fill: var(--toolbox-surface); + stroke: var(--toolbox-accent); + stroke-width: 2; +} + +.analysis-empty-chart { + display: grid; + min-height: 10rem; + place-items: center; + color: var(--toolbox-muted); + font-size: 0.75rem; +} + +.analysis-outcome-plot { + display: flex; + min-height: 10rem; + align-items: end; + gap: 0.22rem; +} + +.analysis-outcome-plot > span { + min-width: 0.35rem; + max-width: 2rem; + height: 45%; + flex: 1; + border-radius: 0.2rem 0.2rem 0 0; + background: var(--toolbox-muted); +} + +.analysis-outcome-plot > span.is-match { + height: 88%; + background: var(--regex-mint); +} + +.analysis-outcome-plot > span.is-no-match { + background: var(--toolbox-accent); +} + +.analysis-outcome-plot > span.is-timeout { + height: 100%; + background: var(--toolbox-danger); +} + +.analysis-outcome-plot > span.is-crash, +.analysis-outcome-plot > span.is-compile-error { + height: 100%; + background: var(--analysis-warning); +} + +.analysis-outcome-key { + display: flex; + flex-wrap: wrap; + gap: 0.4rem 0.7rem; + color: var(--toolbox-muted); + font-size: 0.64rem; +} + +.analysis-outcome-key span::before { + display: inline-block; + width: 0.55rem; + height: 0.55rem; + margin-right: 0.25rem; + border-radius: 0.15rem; + background: var(--toolbox-muted); + content: ""; +} + +.analysis-outcome-key .is-match::before { + background: var(--regex-mint); +} + +.analysis-outcome-key .is-no-match::before { + background: var(--toolbox-accent); +} + +.analysis-outcome-key .is-timeout::before { + background: var(--toolbox-danger); +} + +.analysis-outcome-key .is-crash::before { + background: var(--analysis-warning); +} + +.analysis-limitations { + margin: 0; + padding: 0 1.6rem 0.75rem 2rem; + color: var(--toolbox-muted); + font-size: 0.7rem; +} + +@media (max-width: 40rem) { + .analysis-section > header, + .analysis-result-block > header { + align-items: start; + flex-direction: column; + } + + .analysis-section > header > span, + .analysis-result-block > header > span { + text-align: left; + } + + .analysis-status { + grid-template-columns: 1fr; + } + + .analysis-status > strong { + white-space: normal; + } +} diff --git a/src/components/AnalysisPanel.test.tsx b/src/components/AnalysisPanel.test.tsx new file mode 100644 index 0000000..d89a369 --- /dev/null +++ b/src/components/AnalysisPanel.test.tsx @@ -0,0 +1,248 @@ +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { AnalysisWorkerClient } from "../regex/analysis/AnalysisSupervisor"; +import type { + BenchmarkWorkerSample, + GrowthWorkerSample, +} from "../regex/analysis/analysis.types"; +import { WorkerRequestError } from "../regex/execution/WorkerSupervisor"; +import { EcmaScriptSyntaxProvider } from "../regex/syntax/providers/ecmascript/EcmaScriptSyntaxProvider"; +import { AnalysisPanel } from "./AnalysisPanel"; + +const provider = new EcmaScriptSyntaxProvider(); + +function benchmarkSample(milliseconds = 1): BenchmarkWorkerSample { + return { + accepted: true, + effectiveFlags: "dg", + subjectBytes: 5, + subjectUtf16: 5, + compileMs: milliseconds, + firstMatchMs: milliseconds + 1, + allMatchesMs: milliseconds + 2, + replacementMs: milliseconds + 3, + throughputBytesPerSecond: 1024 * 1024, + matchCount: 1, + matched: true, + matchCollectionTruncated: false, + replacementOutputUtf16: 1, + }; +} + +function growthSample(executionMs: number): GrowthWorkerSample { + return { + accepted: true, + effectiveFlags: "d", + subjectBytes: 5, + subjectUtf16: 5, + executionMs, + matchCount: 0, + matched: false, + matchCollectionTruncated: false, + }; +} + +function worker( + overrides: Partial = {}, +): AnalysisWorkerClient { + return { + identity: vi.fn().mockResolvedValue({ + flavour: "ecmascript", + engineName: "Native ECMAScript RegExp", + engineVersion: "Fixture Browser 1", + runtimeVersion: "Fixture Browser 1", + nativeOffsetUnit: "utf16", + }), + benchmarkSample: vi.fn().mockResolvedValue(benchmarkSample()), + growthProbe: vi.fn().mockResolvedValue(growthSample(1)), + cancel: vi.fn(), + dispose: vi.fn(), + ...overrides, + }; +} + +async function renderPanel( + overrides: Partial[0]> = {}, +) { + const pattern = overrides.pattern ?? "(a+)+$"; + const flavour = overrides.flavour ?? "ecmascript"; + const syntax = + overrides.syntax ?? + (await provider.parsePattern({ + flavour: "ecmascript", + flavourVersion: "2025", + pattern, + flags: [], + options: {}, + })); + const currentWorker = worker(); + const rendered = render( + currentWorker} + {...overrides} + />, + ); + return { ...rendered, currentWorker, syntax }; +} + +afterEach(() => { + cleanup(); +}); + +describe("AnalysisPanel", () => { + it("shows flavour-scoped advisory static findings and selects their source range", async () => { + const onSelectPatternRange = vi.fn(); + await renderPanel({ onSelectPatternRange }); + + expect( + screen.getByRole("heading", { name: "Potential risk findings" }), + ).toBeInTheDocument(); + expect(screen.getByText("Potential nested-quantifier risk")).toBeVisible(); + expect(screen.getByText(/potential risks found/u)).toBeVisible(); + await userEvent.click( + screen.getByRole("button", { + name: /Potential nested-quantifier risk/u, + }), + ); + expect(onSelectPatternRange).toHaveBeenCalledWith({ + startUtf16: 0, + endUtf16: 5, + }); + }); + + it("runs bounded cold and warm benchmark samples and renders p95", async () => { + const { currentWorker } = await renderPanel(); + await userEvent.click(screen.getByRole("tab", { name: "Benchmark" })); + await userEvent.clear( + screen.getByRole("spinbutton", { name: "Measured samples" }), + ); + await userEvent.type( + screen.getByRole("spinbutton", { name: "Measured samples" }), + "3", + ); + await userEvent.click( + screen.getByRole("button", { name: "Run bounded benchmark" }), + ); + + const table = await screen.findByRole("table", { + name: "Cold and warm benchmark metrics", + }); + expect(table).toHaveTextContent("Warm p95"); + expect(table).toHaveTextContent("Compile"); + expect(screen.getByText(/Fixture Browser 1/u)).toBeVisible(); + expect(screen.getAllByText(/3 measured/u)).toHaveLength(2); + expect(currentWorker.identity).toHaveBeenCalled(); + expect(currentWorker.benchmarkSample).toHaveBeenCalledTimes(7); + }); + + it("renders separate timing and outcome plots for bounded growth", async () => { + const samples = [growthSample(0.5), growthSample(10)]; + const currentWorker = worker({ + growthProbe: vi + .fn() + .mockImplementation(() => Promise.resolve(samples.shift()!)), + }); + await renderPanel({ createSupervisor: () => currentWorker }); + await userEvent.click( + screen.getByRole("button", { name: "Run bounded growth probe" }), + ); + + expect( + await screen.findByRole("img", { + name: /Execution-time growth plot/u, + }), + ).toBeVisible(); + expect( + screen.getByRole("img", { name: /Outcome plot with 2 samples/u }), + ).toBeVisible(); + expect(screen.getByText("Observed disproportionate growth")).toBeVisible(); + expect( + screen.getByRole("table", { name: "Dynamic growth samples" }), + ).toHaveTextContent("Normalized growth"); + }); + + it("does not apply ECMAScript analysis to PCRE2", async () => { + const currentWorker = worker(); + await renderPanel({ + flavour: "pcre2", + createSupervisor: () => currentWorker, + }); + + expect(screen.getByRole("alert")).toHaveTextContent( + "supports ECMAScript 2025 only", + ); + expect( + screen.getByRole("button", { name: "Run bounded growth probe" }), + ).toBeDisabled(); + expect(currentWorker.identity).not.toHaveBeenCalled(); + }); + + it("never renders benchmark results against a changed subject", async () => { + const rendered = await renderPanel(); + await userEvent.click(screen.getByRole("tab", { name: "Benchmark" })); + await userEvent.click( + screen.getByRole("button", { name: "Run bounded benchmark" }), + ); + expect( + await screen.findByRole("table", { + name: "Cold and warm benchmark metrics", + }), + ).toBeVisible(); + + rendered.rerender( + rendered.currentWorker} + />, + ); + + expect( + screen.queryByRole("table", { + name: "Cold and warm benchmark metrics", + }), + ).not.toBeInTheDocument(); + }); + + it("terminates the active worker when the user cancels", async () => { + let rejectSample: ((reason: Error) => void) | undefined; + const pending = new Promise((_resolve, reject) => { + rejectSample = reject; + }); + const currentWorker = worker({ + benchmarkSample: vi.fn().mockReturnValue(pending), + cancel: vi.fn().mockImplementation(() => { + rejectSample?.( + new WorkerRequestError("cancelled", "fixture cancellation"), + ); + }), + }); + await renderPanel({ createSupervisor: () => currentWorker }); + await userEvent.click(screen.getByRole("tab", { name: "Benchmark" })); + await userEvent.click( + screen.getByRole("button", { name: "Run bounded benchmark" }), + ); + await userEvent.click( + await screen.findByRole("button", { name: "Cancel benchmark" }), + ); + + await waitFor(() => expect(currentWorker.cancel).toHaveBeenCalled()); + expect(await screen.findAllByText(/Benchmark cancelled/u)).toHaveLength(2); + expect(currentWorker.dispose).toHaveBeenCalled(); + }); +}); diff --git a/src/components/AnalysisPanel.tsx b/src/components/AnalysisPanel.tsx new file mode 100644 index 0000000..6230986 --- /dev/null +++ b/src/components/AnalysisPanel.tsx @@ -0,0 +1,1245 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + DEFAULT_BENCHMARK_SETTINGS, + DEFAULT_GROWTH_SETTINGS, +} from "../regex/analysis/analysis-limits"; +import { + runGrowthAnalysis, + runRegexBenchmark, +} from "../regex/analysis/analysis-runner"; +import { + AnalysisSupervisor, + type AnalysisWorkerClient, +} from "../regex/analysis/AnalysisSupervisor"; +import type { + AnalysisProgress, + GrowthAnalysisResult, + GrowthSettings, + MetricStatistics, + RegexBenchmarkResult, + RegexRiskFinding, +} from "../regex/analysis/analysis.types"; +import { analyseStaticRisk } from "../regex/analysis/static-risk"; +import { + DEFAULT_REGEX_LIMITS, + utf8ByteLength, +} from "../regex/execution/request-limits"; +import type { RegexFlavourId } from "../regex/model/flavour"; +import type { RegexSyntaxResult, SourceRange } from "../regex/model/syntax"; +import "./AnalysisPanel.css"; + +type AnalysisView = "risk" | "benchmark"; +type ActiveRun = "benchmark" | "growth"; + +export interface AnalysisPanelProps { + readonly active: boolean; + readonly flavour: RegexFlavourId; + readonly pattern: string; + readonly flags: readonly string[]; + readonly subject: string; + readonly replacement: string; + readonly scanAll: boolean; + readonly syntax?: RegexSyntaxResult; + readonly onClose?: () => void; + readonly onSelectPatternRange?: (range: SourceRange) => void; + readonly createSupervisor?: () => AnalysisWorkerClient; +} + +function formatMilliseconds(value: number | undefined): string { + if (value === undefined) return "—"; + if (value > 0 && value < 0.01) return "<0.01 ms"; + return `${value.toFixed(2)} ms`; +} + +function formatThroughput(value: number | undefined): string { + if (value === undefined) return "—"; + return `${(value / (1024 * 1024)).toFixed(2)} MiB/s`; +} + +function metricValue( + statistic: MetricStatistics | undefined, + key: "minimum" | "median" | "p95" | "maximum", + throughput: boolean, +): string { + const value = statistic?.[key]; + return throughput ? formatThroughput(value) : formatMilliseconds(value); +} + +interface MetricRow { + readonly label: string; + readonly cold?: number; + readonly warm?: MetricStatistics; + readonly throughput?: boolean; + readonly unavailableReason?: string; +} + +function BenchmarkTable({ result }: { readonly result: RegexBenchmarkResult }) { + const cold = result.coldSample; + const rows: readonly MetricRow[] = [ + { + label: "Worker cold start", + cold: result.coldStartMs, + }, + { + label: "Compile", + cold: cold?.compileMs, + warm: result.warmStatistics.compileMs, + }, + { + label: "First match", + cold: cold?.firstMatchMs, + warm: result.warmStatistics.firstMatchMs, + }, + { + label: "All matches", + cold: cold?.allMatchesMs, + warm: result.warmStatistics.allMatchesMs, + }, + { + label: "Replacement", + cold: cold?.replacementMs, + warm: result.warmStatistics.replacementMs, + unavailableReason: cold?.replacementSkippedReason, + }, + { + label: "All-match throughput", + cold: cold?.throughputBytesPerSecond, + warm: result.warmStatistics.throughputBytesPerSecond, + throughput: true, + }, + ]; + + return ( +

+ + + + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + + + ))} + +
MetricCold / first useWarm minimumWarm medianWarm p95Warm maximumSamples
{row.label} + {row.throughput + ? formatThroughput(row.cold) + : formatMilliseconds(row.cold)} + + {metricValue(row.warm, "minimum", row.throughput === true)} + + {metricValue(row.warm, "median", row.throughput === true)} + {metricValue(row.warm, "p95", row.throughput === true)} + {metricValue(row.warm, "maximum", row.throughput === true)} + {row.warm?.count.toLocaleString() ?? "—"}
+
+ ); +} + +function BenchmarkSummary({ + result, +}: { + readonly result: RegexBenchmarkResult; +}) { + return ( +
+
+
+

Actual native engine · bounded worker

+

Benchmark result

+
+ + {result.status.replaceAll("-", " ")} + +
+
+
+
Engine
+
+ {result.identity + ? `${result.identity.engineName} · ${result.identity.engineVersion}` + : "Worker identity unavailable"} +
+
+
+
Subject
+
+ {result.coldSample + ? `${result.coldSample.subjectBytes.toLocaleString()} bytes · ${result.coldSample.subjectUtf16.toLocaleString()} UTF-16 units` + : "No completed cold sample"} +
+
+
+
Output count
+
+ {result.coldSample + ? `${result.coldSample.matchCount.toLocaleString()} matches${ + result.coldSample.matchCollectionTruncated + ? " · bounded prefix" + : "" + }` + : "Unavailable"} +
+
+
+
Sample count
+
+ {result.completedMeasuredIterations.toLocaleString()} measured ·{" "} + {result.completedWarmups.toLocaleString()} warm-up +
+
+
+
Wall time
+
{formatMilliseconds(result.wallTimeMs)}
+
+
+
Effective flags
+
+ {result.coldSample?.effectiveFlags || "(none)"} +
+
+
+ {result.stoppedReason ? ( +

{result.stoppedReason}

+ ) : null} + +
    + {result.warnings.map((warning) => ( +
  • {warning}
  • + ))} +
+
+ ); +} + +function RiskFindingCard({ + finding, + onSelectPatternRange, +}: { + readonly finding: RegexRiskFinding; + readonly onSelectPatternRange?: (range: SourceRange) => void; +}) { + const content = ( + <> + + {finding.severity} + + + {finding.title} + + {finding.evidence.replaceAll("-", " ")} · {finding.confidence}{" "} + confidence · UTF-16 {finding.range.startUtf16}… + {finding.range.endUtf16} + + + + ); + return ( +
+ {onSelectPatternRange ? ( + + ) : ( +
{content}
+ )} +

{finding.explanation}

+
+
+
Example risk
+
{finding.exampleRisk}
+
+
+
Suggested investigation
+
{finding.suggestedInvestigation}
+
+
+
Limitations
+
{finding.limitations}
+
+
+
+ ); +} + +function TimeGrowthPlot({ result }: { readonly result: GrowthAnalysisResult }) { + const measured = result.samples.filter( + ( + sample, + ): sample is (typeof result.samples)[number] & { + readonly executionMs: number; + } => sample.executionMs !== undefined, + ); + const maximumTime = Math.max( + 0.01, + ...measured.map((sample) => sample.executionMs), + ); + const points = measured + .map((sample, index) => { + const x = + measured.length === 1 ? 50 : (index / (measured.length - 1)) * 100; + const y = 100 - (sample.executionMs / maximumTime) * 92; + return `${x.toFixed(2)},${y.toFixed(2)}`; + }) + .join(" "); + + return ( +
+
+ Execution time by generated input + + Maximum observed {formatMilliseconds(maximumTime)} · separate from + outcome status + +
+ {measured.length > 0 ? ( + + + + {points.split(" ").map((point) => { + const [x, y] = point.split(","); + return ( + + ); + })} + + ) : ( +

No completed timing sample.

+ )} +
+ ); +} + +function OutcomeGrowthPlot({ + result, +}: { + readonly result: GrowthAnalysisResult; +}) { + return ( +
+
+ Match and worker outcome by generated input + Timeout is distinct from non-match and crash +
+
+ {result.samples.map((sample) => ( + + ))} +
+ +
+ ); +} + +function GrowthSummary({ + result, + onSelectPatternRange, +}: { + readonly result: GrowthAnalysisResult; + readonly onSelectPatternRange?: (range: SourceRange) => void; +}) { + return ( +
+
+
+

+ Generated subject family · bounded observation +

+

Dynamic growth result

+
+ + {result.status.replaceAll("-", " ")} + +
+

{result.stoppedReason}

+
+ + +
+
+ + + + + + + + + + + + + {result.samples.map((sample) => ( + + + + + + + + + ))} + +
RepetitionsInput bytesTimeMatch resultStatusNormalized growth
{sample.repetitions.toLocaleString()}{sample.inputBytes.toLocaleString()}{formatMilliseconds(sample.executionMs)} + {sample.matched === undefined + ? "—" + : sample.matched + ? `${sample.matchCount?.toLocaleString() ?? "≥1"} match(es)` + : "no match"} + {sample.status.replaceAll("-", " ")} + {sample.normalizedGrowth === undefined + ? "—" + : `${sample.normalizedGrowth.toFixed(2)}×`} +
+
+ {result.dynamicFindings.map((finding) => ( + + ))} +

+ These observations apply only to this generated input family, browser + runtime and selected limits. They are not proof of general complexity or + safety. +

+
+ ); +} + +function numericValue( + value: string, + label: string, + minimum: number, + maximum: number, +): number { + const parsed = Number(value); + if ( + value.trim() === "" || + !Number.isFinite(parsed) || + parsed < minimum || + parsed > maximum + ) { + throw new RangeError(`${label} must be from ${minimum} to ${maximum}.`); + } + return parsed; +} + +export function AnalysisPanel({ + active, + flavour, + pattern, + flags, + subject, + replacement, + scanAll, + syntax, + onClose, + onSelectPatternRange, + createSupervisor = () => new AnalysisSupervisor(), +}: AnalysisPanelProps) { + const [view, setView] = useState("risk"); + const [activeRun, setActiveRun] = useState(); + const [progressState, setProgressState] = useState(); + const [message, setMessage] = useState( + "Analysis has not run for this configuration.", + ); + const [error, setError] = useState(); + const [benchmarkRecord, setBenchmarkRecord] = useState<{ + readonly configurationKey: symbol; + readonly result: RegexBenchmarkResult; + }>(); + const [growthRecord, setGrowthRecord] = useState<{ + readonly configurationKey: symbol; + readonly result: GrowthAnalysisResult; + }>(); + const [warmupIterations, setWarmupIterations] = useState( + String(DEFAULT_BENCHMARK_SETTINGS.warmupIterations), + ); + const [measuredIterations, setMeasuredIterations] = useState( + String(DEFAULT_BENCHMARK_SETTINGS.measuredIterations), + ); + const [benchmarkTimeoutMs, setBenchmarkTimeoutMs] = useState( + String(DEFAULT_BENCHMARK_SETTINGS.sampleTimeoutMs), + ); + const [growthPrefix, setGrowthPrefix] = useState( + DEFAULT_GROWTH_SETTINGS.prefix, + ); + const [growthFragment, setGrowthFragment] = useState( + DEFAULT_GROWTH_SETTINGS.repeatedFragment, + ); + const [growthSuffix, setGrowthSuffix] = useState( + DEFAULT_GROWTH_SETTINGS.suffix, + ); + const [growthStart, setGrowthStart] = useState( + String(DEFAULT_GROWTH_SETTINGS.startingRepetitions), + ); + const [growthMaximum, setGrowthMaximum] = useState( + String(DEFAULT_GROWTH_SETTINGS.maximumRepetitions), + ); + const [growthMultiplier, setGrowthMultiplier] = useState( + String(DEFAULT_GROWTH_SETTINGS.multiplier), + ); + const [growthSteps, setGrowthSteps] = useState( + String(DEFAULT_GROWTH_SETTINGS.maximumSteps), + ); + const [growthTimeoutMs, setGrowthTimeoutMs] = useState( + String(DEFAULT_GROWTH_SETTINGS.sampleTimeoutMs), + ); + const [growthMaximumBytes, setGrowthMaximumBytes] = useState( + String(DEFAULT_GROWTH_SETTINGS.maximumSubjectBytes), + ); + const [growthThreshold, setGrowthThreshold] = useState( + String(DEFAULT_GROWTH_SETTINGS.normalizedGrowthThreshold), + ); + const supervisor = useRef(undefined); + const abortController = useRef(undefined); + const generation = useRef(0); + + const syntaxCurrent = + syntax?.accepted === true && + syntax.root.raw === pattern && + syntax.root.support.flavour === flavour; + const analysisAvailable = flavour === "ecmascript" && syntaxCurrent; + const staticReport = useMemo( + () => + syntaxCurrent && syntax + ? analyseStaticRisk({ + flavour, + root: syntax.root, + flags, + scanAll, + replacement, + }) + : undefined, + [flavour, flags, replacement, scanAll, syntax, syntaxCurrent], + ); + const configurationKey = useMemo( + () => + Symbol( + `analysis-${flavour}-${flags.join("")}-${pattern.length}-${subject.length}-${replacement.length}-${String(scanAll)}`, + ), + [flavour, flags, pattern, replacement, scanAll, subject], + ); + const previousConfiguration = useRef(configurationKey); + const benchmarkResult = + benchmarkRecord?.configurationKey === configurationKey + ? benchmarkRecord.result + : undefined; + const growthResult = + growthRecord?.configurationKey === configurationKey + ? growthRecord.result + : undefined; + + const disposeActive = useCallback((abort = true) => { + if (abort) abortController.current?.abort(); + supervisor.current?.cancel(); + supervisor.current?.dispose(); + supervisor.current = undefined; + abortController.current = undefined; + }, []); + + useEffect(() => { + if (active) return; + const stoppedGeneration = generation.current + 1; + generation.current = stoppedGeneration; + disposeActive(); + globalThis.queueMicrotask(() => { + if (generation.current === stoppedGeneration) setActiveRun(undefined); + }); + }, [active, disposeActive]); + + useEffect(() => { + if (previousConfiguration.current === configurationKey) return; + previousConfiguration.current = configurationKey; + const stoppedGeneration = generation.current + 1; + generation.current = stoppedGeneration; + disposeActive(); + globalThis.queueMicrotask(() => { + if (generation.current !== stoppedGeneration) return; + setActiveRun(undefined); + setBenchmarkRecord(undefined); + setGrowthRecord(undefined); + setError(undefined); + setMessage( + "Configuration changed; previous timing results were cleared.", + ); + }); + }, [configurationKey, disposeActive]); + + useEffect( + () => () => { + generation.current += 1; + disposeActive(); + }, + [disposeActive], + ); + + const beginRun = useCallback( + (kind: ActiveRun) => { + disposeActive(); + const currentSupervisor = createSupervisor(); + const controller = new AbortController(); + supervisor.current = currentSupervisor; + abortController.current = controller; + generation.current += 1; + setActiveRun(kind); + setError(undefined); + setProgressState(undefined); + setMessage( + kind === "benchmark" + ? "Starting bounded benchmark…" + : "Starting bounded dynamic growth analysis…", + ); + return { + currentSupervisor, + controller, + runGeneration: generation.current, + }; + }, + [createSupervisor, disposeActive], + ); + + const finishRun = useCallback( + (runGeneration: number, currentSupervisor: AnalysisWorkerClient) => { + currentSupervisor.dispose(); + if (supervisor.current === currentSupervisor) { + supervisor.current = undefined; + abortController.current = undefined; + } + if (generation.current === runGeneration) { + setActiveRun(undefined); + setProgressState(undefined); + } + }, + [], + ); + + const runBenchmark = async () => { + if (!analysisAvailable) return; + const run = beginRun("benchmark"); + try { + const result = await runRegexBenchmark( + { + flavour: "ecmascript", + pattern, + flags, + subject, + replacement, + scanAll, + maximumMatches: DEFAULT_REGEX_LIMITS.maximumMatches, + settings: { + warmupIterations: numericValue( + warmupIterations, + "Warm-up iterations", + 0, + 100, + ), + measuredIterations: numericValue( + measuredIterations, + "Measured iterations", + 1, + 1_000, + ), + sampleTimeoutMs: numericValue( + benchmarkTimeoutMs, + "Sample timeout", + 25, + DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs, + ), + maximumWallTimeMs: DEFAULT_REGEX_LIMITS.maximumBenchmarkWallTimeMs, + }, + }, + run.currentSupervisor, + { + signal: run.controller.signal, + onProgress: (next) => { + if (generation.current !== run.runGeneration) return; + setProgressState(next); + setMessage(next.message); + }, + }, + ); + if (generation.current !== run.runGeneration) return; + setBenchmarkRecord({ configurationKey, result }); + setMessage( + result.status === "complete" + ? `Benchmark complete with ${result.completedMeasuredIterations.toLocaleString()} measured warm samples.` + : (result.stoppedReason ?? + `Benchmark stopped with status ${result.status}.`), + ); + } catch (cause) { + if (generation.current !== run.runGeneration) return; + const next = + cause instanceof Error ? cause.message : "Benchmark could not start."; + setError(next); + setMessage("Benchmark settings require review."); + } finally { + finishRun(run.runGeneration, run.currentSupervisor); + } + }; + + const growthSettings = (): GrowthSettings => ({ + prefix: growthPrefix, + repeatedFragment: growthFragment, + suffix: growthSuffix, + startingRepetitions: numericValue( + growthStart, + "Starting repetitions", + 1, + 10_000_000, + ), + maximumRepetitions: numericValue( + growthMaximum, + "Maximum repetitions", + 1, + 10_000_000, + ), + multiplier: numericValue(growthMultiplier, "Growth multiplier", 1.1, 10), + maximumSteps: numericValue(growthSteps, "Maximum steps", 1, 24), + sampleTimeoutMs: numericValue( + growthTimeoutMs, + "Sample timeout", + 25, + DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs, + ), + maximumWallTimeMs: DEFAULT_REGEX_LIMITS.maximumBenchmarkWallTimeMs, + maximumSubjectBytes: numericValue( + growthMaximumBytes, + "Maximum generated bytes", + 1, + DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes, + ), + normalizedGrowthThreshold: numericValue( + growthThreshold, + "Growth threshold", + 1.1, + 100, + ), + }); + + const runGrowth = async () => { + if (!analysisAvailable) return; + const run = beginRun("growth"); + try { + const result = await runGrowthAnalysis( + { + flavour: "ecmascript", + pattern, + flags, + scanAll, + maximumMatches: DEFAULT_REGEX_LIMITS.maximumMatches, + settings: growthSettings(), + }, + run.currentSupervisor, + { + signal: run.controller.signal, + onProgress: (next) => { + if (generation.current !== run.runGeneration) return; + setProgressState(next); + setMessage(next.message); + }, + }, + ); + if (generation.current !== run.runGeneration) return; + setGrowthRecord({ configurationKey, result }); + setMessage(result.stoppedReason); + } catch (cause) { + if (generation.current !== run.runGeneration) return; + const next = + cause instanceof Error + ? cause.message + : "Growth analysis could not start."; + setError(next); + setMessage("Growth settings require review."); + } finally { + finishRun(run.runGeneration, run.currentSupervisor); + } + }; + + const cancel = () => { + abortController.current?.abort(); + supervisor.current?.cancel(); + setMessage( + activeRun === "benchmark" + ? "Cancelling benchmark and terminating its worker…" + : "Cancelling growth analysis and terminating its worker…", + ); + }; + + const progressMaximum = Math.max(1, progressState?.total ?? 1); + const progressValue = Math.min( + progressMaximum, + progressState?.completed ?? 0, + ); + + return ( +
+
+
+

Advisory structure + bounded observations

+

Performance & risk analysis

+
+
+ ECMAScript-specific + {onClose ? ( + + ) : null} +
+
+ +
+ + +
+ + {flavour !== "ecmascript" ? ( +

+ The current analyser supports ECMAScript 2025 only. It will not apply + ECMAScript heuristics or native-browser timing claims to {flavour}. +

+ ) : !syntaxCurrent ? ( +

+ Analysis requires a current pattern accepted by the ECMAScript syntax + provider. +

+ ) : null} + +
+ {activeRun ? "running" : "ready"} + {message} +
+ {progressState ? ( +
+ + + {progressState.completed.toLocaleString()} /{" "} + {progressState.total.toLocaleString()} + +
+ ) : null} + {error ? ( +

+ {error} +

+ ) : null} + + {view === "risk" ? ( +
+
+
+
+

Static · normalized ECMAScript AST

+

Potential risk findings

+
+ {staticReport ? ( + + {staticReport.analysedNodes.toLocaleString()} nodes reviewed + + ) : null} +
+ {staticReport ? ( + <> +

{staticReport.summary}

+
+ {staticReport.findings.map((finding) => ( + + ))} +
+
+ Static analyser scope and limitations +
    + {staticReport.limitations.map((limitation) => ( +
  • {limitation}
  • + ))} +
+
+ + ) : ( +

+ Static findings are unavailable until the active pattern has a + current supported syntax tree. +

+ )} +
+ +
+
+
+

+ Dynamic · killable native-engine worker +

+

Bounded growth probe

+
+ 30 s aggregate hard stop +
+

+ Subjects are built as prefix + repeated fragment + suffix. Use a + representative near-miss; generation stops on timeout, selected + growth threshold, byte bound, repetition bound, step bound or + cancellation. +

+
+ + + +
+
+ + + + + + + +
+
+ + {activeRun === "growth" ? ( + + ) : null} +
+ {growthResult ? ( + + ) : null} +
+
+ ) : ( +
+
+
+
+

+ Cold first use + warm measured samples +

+

Bounded benchmark

+
+ + {utf8ByteLength(subject).toLocaleString()} subject bytes + +
+

+ Each timing sample measures compile, first match, all matches and + replacement separately in the native ECMAScript worker. Worker + startup is reported only as cold start. Trace mode is never used. +

+
+ + + +
+
+
+
Pattern
+
+ {pattern.length.toLocaleString()} UTF-16 units · flags{" "} + {flags.join("") || "(none)"} +
+
+
+
Subject
+
+ {subject.length.toLocaleString()} UTF-16 units ·{" "} + {utf8ByteLength(subject).toLocaleString()} bytes +
+
+
+
Replacement
+
+ {replacement.length.toLocaleString()} UTF-16 units · output + estimate is bounded before timing +
+
+
+
+ + {activeRun === "benchmark" ? ( + + ) : null} +
+ {benchmarkResult ? ( + + ) : null} +
+
+ )} +
+ ); +} diff --git a/src/components/CapabilityPanel.test.tsx b/src/components/CapabilityPanel.test.tsx index 431bf72..eca41a8 100644 --- a/src/components/CapabilityPanel.test.tsx +++ b/src/components/CapabilityPanel.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; import type { RegexExecutionResult } from "../regex/model/match"; import type { RegexSyntaxResult } from "../regex/model/syntax"; import { CapabilityPanel } from "./CapabilityPanel"; @@ -78,11 +79,66 @@ describe("CapabilityPanel", () => { ).toBeInTheDocument(); expect(screen.getByText("Fixture engine")).toBeInTheDocument(); expect(screen.getByText("123")).toBeInTheDocument(); + expect( + screen.getByText("Not separately reported by this adapter"), + ).toBeInTheDocument(); + expect(screen.getByText("1", { selector: "dd" })).toBeInTheDocument(); expect(screen.getByText("UTF-8 bytes")).toBeInTheDocument(); + expect(screen.getByText("fixture")).toBeInTheDocument(); const replacement = screen.getByText("Replacement").closest("div"); expect(replacement).toHaveTextContent("Unavailable"); const compilation = screen.getByText("Compilation").closest("div"); expect(compilation).toHaveTextContent("Available"); + const generatedCases = screen.getByText("Generated cases").closest("div"); + expect(generatedCases).toHaveTextContent( + "deterministic AST candidates, retained only after actual-engine verification", + ); + const formatting = screen.getByText("Pattern formatting").closest("div"); + expect(formatting).toHaveTextContent( + "grammar-backed literal/control escaping with mandatory exact-snapshot validation", + ); + }); + + it("does not advertise ECMAScript generation for the partial PCRE2 syntax provider", () => { + render( + , + ); + + const generatedCases = screen.getByText("Generated cases").closest("div"); + expect(generatedCases).toHaveTextContent( + "Unavailable — the partial PCRE2 provider does not expose a complete generation AST", + ); + const formatting = screen.getByText("Pattern formatting").closest("div"); + expect(formatting).toHaveTextContent( + "Unavailable — no complete PCRE2 grammar-backed formatter is implemented", + ); + }); + + it("offers an accessible close control when used as a modal panel", async () => { + const onClose = vi.fn(); + const user = userEvent.setup(); + render( + , + ); + + await user.click( + screen.getByRole("button", { name: "Close capabilities" }), + ); + expect(onClose).toHaveBeenCalledOnce(); }); }); diff --git a/src/components/CapabilityPanel.tsx b/src/components/CapabilityPanel.tsx index 2a4e8eb..fe527be 100644 --- a/src/components/CapabilityPanel.tsx +++ b/src/components/CapabilityPanel.tsx @@ -1,4 +1,4 @@ -import { REGEXPP_VERSION, SYNTAX_PROFILE } from "../version"; +import { SYNTAX_PROFILE } from "../version"; import type { RegexExecutionResult } from "../regex/model/match"; import type { RegexSyntaxResult } from "../regex/model/syntax"; import type { RegexEngineCapabilities } from "../regex/model/flavour"; @@ -35,23 +35,27 @@ function offsetLabel( export function CapabilityPanel({ syntax, execution, + onClose, }: { readonly syntax?: RegexSyntaxResult; readonly execution?: RegexExecutionResult; + readonly onClose?: () => void; }) { const capabilities = execution?.engine.capabilities; + const activeFlavour = + execution?.engine.flavour ?? syntax?.root.support.flavour; const rows = [ [ "Flavour", execution?.engine.flavour ?? syntax?.root.support.flavour ?? - "ECMAScript (awaiting workers)", + "Awaiting flavour workers", ], [ "Syntax provider", syntax ? `${syntax.provider.id} ${syntax.provider.version}` - : `regexpp ${REGEXPP_VERSION} (awaiting syntax worker)`, + : "Awaiting syntax worker result", ], ["Syntax profile", SYNTAX_PROFILE], [ @@ -65,9 +69,20 @@ export function CapabilityPanel({ execution?.engine.engineName ?? "Awaiting first engine result", ], [ - "Engine/runtime version", + "Engine version", execution?.engine.engineVersion ?? "Shown after first execution", ], + [ + "Runtime version", + execution?.engine.runtimeVersion ?? + (execution + ? "Not separately reported by this adapter" + : "Shown after first execution"), + ], + [ + "Adapter version", + execution?.engine.adapterVersion ?? "Shown after first execution", + ], ["Native offsets", offsetLabel(execution?.engine.offsetUnit)], ["Compilation", capability(capabilities, "compilation")], ["Matching", capability(capabilities, "matching")], @@ -90,6 +105,38 @@ export function CapabilityPanel({ ), ], ["Benchmark", capability(capabilities, "benchmark")], + [ + "Static risk analysis", + activeFlavour === "ecmascript" + ? "Available — advisory ECMAScript 2025 heuristics" + : activeFlavour === "pcre2" + ? "Unavailable — ECMAScript heuristics are not applied to PCRE2" + : "Awaiting flavour worker result", + ], + [ + "Generated cases", + activeFlavour === "ecmascript" + ? "Available — deterministic AST candidates, retained only after actual-engine verification" + : activeFlavour === "pcre2" + ? "Unavailable — the partial PCRE2 provider does not expose a complete generation AST" + : "Awaiting flavour worker result", + ], + [ + "Pattern formatting", + activeFlavour === "ecmascript" + ? "Available — grammar-backed literal/control escaping with mandatory exact-snapshot validation" + : activeFlavour === "pcre2" + ? "Unavailable — no complete PCRE2 grammar-backed formatter is implemented" + : "Awaiting flavour worker result", + ], + [ + "Known syntax gaps", + syntax + ? syntax.coverage.unsupportedConstructs.length > 0 + ? syntax.coverage.unsupportedConstructs.join("; ") + : "None reported by the active provider" + : "Awaiting syntax worker result", + ], ] as const; return (
Current provider and adapter metadata

Capabilities

- Community build +
+ Community build + {onClose ? ( + + ) : null} +
{rows.map(([term, value]) => ( @@ -112,10 +172,11 @@ export function CapabilityPanel({ ))}

- Next flavour: official PCRE2 10.47 WebAssembly with actual callout - traces. Python, Go, Rust, .NET and Java remain unavailable until their - named runtimes pass the same worker, offset, conformance and licensing - gates. + PCRE2 10.47 matching and substitution run in the bundled WebAssembly + worker. Its separate trace worker exposes bounded reported automatic + callouts; movement classifications remain explicitly derived. Python, + Go, Rust, .NET and Java remain unavailable until their named runtimes + pass the same worker, offset, conformance and licensing gates.

); diff --git a/src/components/ComparisonCodePanel.css b/src/components/ComparisonCodePanel.css new file mode 100644 index 0000000..7311987 --- /dev/null +++ b/src/components/ComparisonCodePanel.css @@ -0,0 +1,331 @@ +.comparison-code-dialog { + width: min(88rem, calc(100% - 2rem)); +} + +.comparison-code-panel { + max-height: calc(100vh - 2rem); + overflow: auto; +} + +.comparison-tabs { + display: flex; + gap: 0.35rem; + padding: 0.7rem 0.8rem 0; +} + +.comparison-tabs button { + border: 1px solid var(--toolbox-border); + border-radius: calc(var(--toolbox-radius) * 0.72); + padding: 0.55rem 0.8rem; + background: var(--toolbox-surface); + color: var(--toolbox-text); + font-weight: 750; +} + +.comparison-tabs button[aria-selected="true"] { + border-color: var(--toolbox-accent); + background: var(--toolbox-accent-soft); +} + +.comparison-config, +.comparison-tab-panel { + display: grid; + gap: 0.8rem; + padding: 0.8rem; +} + +.comparison-config { + border-bottom: 1px solid var(--toolbox-border); + background: var(--toolbox-surface-soft); +} + +.comparison-config-row, +.comparison-actions { + display: flex; + flex-wrap: wrap; + align-items: end; + gap: 0.65rem; +} + +.comparison-config label, +.comparison-limit-grid label, +.comparison-text-field { + display: grid; + gap: 0.3rem; + color: var(--toolbox-muted); + font-size: 0.75rem; + font-weight: 700; +} + +.comparison-config select, +.comparison-config input, +.comparison-config textarea { + border: 1px solid var(--toolbox-border); + border-radius: calc(var(--toolbox-radius) * 0.65); + padding: 0.5rem 0.6rem; + background: var(--toolbox-surface); + color: var(--toolbox-text); +} + +.comparison-config textarea { + min-height: 4.8rem; + resize: vertical; + font-family: ui-monospace, monospace; + font-size: 0.78rem; + line-height: 1.45; +} + +.comparison-scan-control { + display: flex !important; + align-items: center; + padding-bottom: 0.5rem; +} + +.comparison-variant-grid, +.comparison-side-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.8rem; +} + +.comparison-flag-selector, +.comparison-limit-grid { + min-width: 0; + margin: 0; + border: 1px solid var(--toolbox-border); + border-radius: calc(var(--toolbox-radius) * 0.7); + padding: 0.55rem; +} + +.comparison-flag-selector legend, +.comparison-limit-grid legend { + padding-inline: 0.3rem; + color: var(--toolbox-muted); + font-size: 0.72rem; + font-weight: 750; +} + +.comparison-flag-selector { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.comparison-flag-selector label { + display: inline-flex; + align-items: center; + gap: 0.25rem; + border: 1px solid var(--toolbox-border); + border-radius: 99rem; + padding: 0.25rem 0.45rem; + background: var(--toolbox-surface); +} + +.comparison-flag-selector span { + font-size: 0.67rem; +} + +.comparison-limit-grid { + display: grid; + grid-template-columns: repeat(6, minmax(6rem, 1fr)); + gap: 0.5rem; +} + +.comparison-subject textarea { + min-height: 7rem; +} + +.comparison-actions p { + margin: 0; + color: var(--toolbox-muted); + font-size: 0.78rem; +} + +.comparison-panel-status.status-error, +.comparison-error { + color: var(--toolbox-danger); +} + +.comparison-result { + display: grid; + gap: 0.8rem; +} + +.comparison-verdict, +.comparison-reasons, +.comparison-differences, +.comparison-alignments, +.generated-code-result { + border: 1px solid var(--toolbox-border); + border-radius: calc(var(--toolbox-radius) * 0.78); + padding: 0.8rem; + background: var(--toolbox-surface); +} + +.comparison-verdict h3, +.comparison-verdict p, +.comparison-verdict small, +.comparison-reasons h4, +.comparison-differences h4, +.comparison-alignments h4, +.generated-code-result h3, +.generated-code-result p { + margin: 0; +} + +.comparison-verdict { + display: grid; + gap: 0.3rem; + border-left: 4px solid var(--regex-success); +} + +.comparison-verdict.verdict-not-comparable { + border-left-color: var(--regex-warning); +} + +.comparison-verdict.verdict-different-for-current-input { + border-left-color: var(--toolbox-danger); +} + +.comparison-verdict p, +.comparison-verdict small, +.comparison-reasons, +.comparison-differences > p, +.comparison-alignments > p, +.generated-code-result li { + color: var(--toolbox-muted); + font-size: 0.77rem; + line-height: 1.5; +} + +.comparison-reasons ul, +.generated-code-result ul { + margin-bottom: 0; +} + +.comparison-side-card { + min-width: 0; + overflow: hidden; + border: 1px solid var(--toolbox-border); + border-radius: calc(var(--toolbox-radius) * 0.78); + background: var(--toolbox-surface); +} + +.comparison-side-card > header, +.generated-code-result > header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.7rem; + padding: 0.7rem; + border-bottom: 1px solid var(--toolbox-border); +} + +.comparison-side-card h4, +.comparison-side-card p { + margin: 0; +} + +.comparison-side-card dl { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: 0; +} + +.comparison-side-card dl > div { + min-width: 0; + padding: 0.55rem 0.7rem; + border-bottom: 1px solid var(--toolbox-border); +} + +.comparison-side-card dt { + color: var(--toolbox-muted); + font-size: 0.67rem; + font-weight: 750; + text-transform: uppercase; +} + +.comparison-side-card dd { + margin: 0.15rem 0 0; + overflow-wrap: anywhere; + font-size: 0.76rem; +} + +.comparison-side-card details, +.comparison-side-card > .comparison-error { + padding: 0.55rem 0.7rem; + font-size: 0.74rem; +} + +.comparison-side-card details ul { + margin-bottom: 0; +} + +.comparison-differences, +.comparison-alignments { + overflow: auto; +} + +.comparison-differences h4, +.comparison-alignments h4 { + margin-bottom: 0.65rem; +} + +.comparison-differences td code { + display: block; + max-width: 22rem; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.comparison-limit-note { + margin-bottom: 0 !important; +} + +.generated-code-result { + min-width: 0; + padding: 0; + overflow: hidden; +} + +.generated-code-result > p, +.generated-code-result > ul { + margin-inline: 0.8rem; +} + +.generated-code-result pre { + max-height: 34rem; + overflow: auto; + margin: 0; + border-top: 1px solid var(--toolbox-border); + padding: 0.8rem; + background: #111827; + color: #e5e7eb; + font-size: 0.72rem; + line-height: 1.45; +} + +.comparison-empty-state { + margin: 0; + padding: 1.2rem; + color: var(--toolbox-muted); + text-align: center; +} + +@media (max-width: 64rem) { + .comparison-limit-grid { + grid-template-columns: repeat(3, minmax(6rem, 1fr)); + } +} + +@media (max-width: 48rem) { + .comparison-variant-grid, + .comparison-side-grid, + .comparison-side-card dl { + grid-template-columns: 1fr; + } + + .comparison-limit-grid { + grid-template-columns: repeat(2, minmax(6rem, 1fr)); + } +} diff --git a/src/components/ComparisonCodePanel.test.tsx b/src/components/ComparisonCodePanel.test.tsx new file mode 100644 index 0000000..4750f33 --- /dev/null +++ b/src/components/ComparisonCodePanel.test.tsx @@ -0,0 +1,82 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { ComparisonCodePanel } from "./ComparisonCodePanel"; + +function renderPanel() { + const onClose = vi.fn(); + render( + \\p{Letter}+)"} + subject="Grüße" + replacement="$!" + scanAll + timeoutMs={2_000} + onClose={onClose} + />, + ); + return { onClose }; +} + +describe("ComparisonCodePanel", () => { + it("exposes comparison and reviewed PCRE2 C generation through one usable path", () => { + renderPanel(); + + expect( + screen.getByRole("tab", { name: "Compare engines" }), + ).toHaveAttribute("aria-selected", "true"); + expect( + screen.getByRole("textbox", { name: "Shared comparison pattern" }), + ).toHaveValue("(?\\p{Letter}+)"); + expect( + screen.getByRole("button", { name: "Run comparison" }), + ).toBeEnabled(); + + fireEvent.click(screen.getByRole("tab", { name: "PCRE2 C code" })); + fireEvent.click( + screen.getByRole("button", { name: "Generate reviewed C17" }), + ); + + expect(screen.getByText("PCRE2 10.47 8-bit · all-matches")).toBeVisible(); + expect(screen.getByText("Exact UTF-8 byte arrays")).toBeVisible(); + expect( + screen.getByText(/PCRE2_UTF and PCRE2_UCP are mandatory/u), + ).toBeVisible(); + const source = screen.getByText( + /This generated program requires PCRE2 10\.47 exactly/u, + ); + expect(source).toHaveTextContent("pcre2_set_match_limit"); + expect(source).not.toHaveTextContent("(?\\p{Letter}+)"); + }); + + it("uses the explicit PCRE2 variant without rewriting it", () => { + renderPanel(); + fireEvent.change(screen.getByLabelText("Pattern model"), { + target: { value: "variants" }, + }); + const pcrePattern = screen.getByLabelText("PCRE2 pattern variant"); + fireEvent.change(pcrePattern, { + target: { value: "(?\\w++)" }, + }); + fireEvent.click(screen.getByRole("tab", { name: "PCRE2 C code" })); + fireEvent.click( + screen.getByRole("button", { name: "Generate reviewed C17" }), + ); + + const source = screen.getByText( + /This generated program requires PCRE2 10\.47 exactly/u, + ); + expect(source).toHaveTextContent("0x2b, 0x2b"); + expect(source).not.toHaveTextContent("(?\\w++)"); + }); + + it("closes through the dialog action", () => { + const { onClose } = renderPanel(); + fireEvent.click( + screen.getByRole("button", { name: "Close compare and generate" }), + ); + expect(onClose).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/components/ComparisonCodePanel.tsx b/src/components/ComparisonCodePanel.tsx new file mode 100644 index 0000000..c9c7295 --- /dev/null +++ b/src/components/ComparisonCodePanel.tsx @@ -0,0 +1,884 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { writeClipboardText } from "../browser/clipboard"; +import { + AVAILABLE_REGEX_FLAVOURS, + defaultRegexOptions, +} from "../regex/flavours/flavour-registry"; +import { ComparisonOrchestrator } from "../regex/comparison/ComparisonOrchestrator"; +import type { + ComparisonFlavour, + ComparisonPatternModel, + RegexComparisonResult, +} from "../regex/comparison/comparison.types"; +import { + generatePcre2C, + type GeneratedPcre2CProgram, +} from "../regex/codegen/pcre2-c"; +import { + DEFAULT_REGEX_LIMITS, + utf8ByteLength, +} from "../regex/execution/request-limits"; +import type { + RegexEngineOptions, + RegexFlavourId, +} from "../regex/model/flavour"; +import "./ComparisonCodePanel.css"; + +const ECMASCRIPT = AVAILABLE_REGEX_FLAVOURS.require("ecmascript"); +const PCRE2 = AVAILABLE_REGEX_FLAVOURS.require("pcre2"); +const MAXIMUM_RENDERED_DIFFERENCES = 250; +const MAXIMUM_RENDERED_ALIGNMENTS = 100; + +type PanelTab = "compare" | "code"; +type PanelStatus = + | { readonly kind: "idle"; readonly message: string } + | { readonly kind: "running"; readonly message: string } + | { readonly kind: "ready"; readonly message: string } + | { readonly kind: "error"; readonly message: string }; + +export interface ComparisonCodePanelProps { + readonly activeFlavour: RegexFlavourId; + readonly activeFlags: readonly string[]; + readonly activeOptions: RegexEngineOptions; + readonly pattern: string; + readonly subject: string; + readonly replacement: string; + readonly scanAll: boolean; + readonly timeoutMs: number; + readonly onClose: () => void; +} + +function initialFlags( + activeFlavour: RegexFlavourId, + activeFlags: readonly string[], + flavour: ComparisonFlavour, +): readonly string[] { + const definition = flavour === "ecmascript" ? ECMASCRIPT : PCRE2; + return activeFlavour === flavour + ? definition.flags + .map((flag) => flag.value) + .filter((flag) => activeFlags.includes(flag)) + : definition.defaultFlags; +} + +function initialOptions( + activeFlavour: RegexFlavourId, + activeOptions: RegexEngineOptions, +): RegexEngineOptions { + return activeFlavour === "pcre2" ? activeOptions : defaultRegexOptions(PCRE2); +} + +function toggleFlag( + current: readonly string[], + flag: string, + enabled: boolean, + flavour: ComparisonFlavour, +): readonly string[] { + const definition = flavour === "ecmascript" ? ECMASCRIPT : PCRE2; + const selected = new Set(current); + if (enabled) { + selected.add(flag); + for (const group of definition.mutuallyExclusiveFlags ?? []) { + if (!group.includes(flag)) continue; + for (const incompatible of group) { + if (incompatible !== flag) selected.delete(incompatible); + } + } + } else { + selected.delete(flag); + } + return definition.flags + .map((candidate) => candidate.value) + .filter((candidate) => selected.has(candidate)); +} + +function preview(value: string | undefined, maximum = 240): string { + if (value === undefined) return "—"; + return value.length <= maximum + ? value + : `${value.slice(0, maximum)}… (${value.length.toLocaleString()} UTF-16 units)`; +} + +function downloadSource(program: GeneratedPcre2CProgram): void { + const url = URL.createObjectURL( + new Blob([program.source], { type: "text/x-c;charset=utf-8" }), + ); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = program.fileName; + anchor.click(); + window.setTimeout(() => URL.revokeObjectURL(url), 1_000); +} + +function engineExecution(result: RegexComparisonResult["sides"][number]) { + return result.runtime.replacement?.execution ?? result.runtime.execution; +} + +function SideResult({ + side, +}: { + readonly side: RegexComparisonResult["sides"][number]; +}) { + const execution = engineExecution(side); + const diagnostics = [ + ...(side.syntax.pattern?.diagnostics ?? []), + ...(side.syntax.replacement?.diagnostics ?? []), + ...(execution?.diagnostics ?? []), + ]; + return ( +
+
+
+

Exact request {side.requestIdentity}

+

+ {side.input.flavour === "ecmascript" ? "ECMAScript" : "PCRE2"} +

+
+ + {side.runtime.status} + +
+
+
+
Pattern syntax
+
+ {side.syntax.pattern + ? side.syntax.pattern.accepted + ? "Accepted" + : "Rejected" + : side.syntax.status} +
+
+
+
Provider
+
+ {side.syntax.pattern + ? `${side.syntax.pattern.provider.id} ${side.syntax.pattern.provider.version}` + : "Unavailable"} +
+
+
+
Engine compile
+
+ {execution + ? execution.accepted + ? "Accepted" + : "Rejected" + : side.runtime.status} +
+
+
+
Engine identity
+
+ {execution + ? `${execution.engine.engineName} · ${execution.engine.engineVersion}` + : "Unavailable"} +
+
+
+
Exact flags
+
+ user {side.input.flags.join("") || "none"} · effective{" "} + {execution?.flags.effectiveFlags || "unavailable"} +
+
+
+
Options
+
+ {JSON.stringify(side.input.options)} +
+
+
+
Matches
+
{execution?.matches.length.toLocaleString() ?? "Unavailable"}
+
+
+
Offsets
+
+ native {execution?.engine.offsetUnit ?? "unavailable"} · comparison + UTF-16 +
+
+ {side.runtime.replacement ? ( +
+
Replacement
+
+ {side.runtime.replacement.outputBytes.toLocaleString()} bytes + {side.runtime.replacement.truncated ? " · incomplete" : ""} +
+
+ ) : null} +
+ {side.syntax.pattern?.coverage.unsupportedConstructs.length ? ( +
+ Provider coverage gaps +
    + {side.syntax.pattern.coverage.unsupportedConstructs.map((gap) => ( +
  • {gap}
  • + ))} +
+
+ ) : null} + {diagnostics.length > 0 ? ( +
+ {diagnostics.length.toLocaleString()} diagnostic(s) +
    + {diagnostics.slice(0, 100).map((diagnostic) => ( +
  • + {diagnostic.severity}: {diagnostic.message} +
  • + ))} +
+
+ ) : null} + {side.syntax.error || side.runtime.error ? ( +

+ {side.syntax.error ?? side.runtime.error} +

+ ) : null} +
+ ); +} + +function ComparisonResultView({ + result, +}: { + readonly result: RegexComparisonResult; +}) { + const visibleDifferences = result.differences.slice( + 0, + MAXIMUM_RENDERED_DIFFERENCES, + ); + const visibleAlignments = result.matchAlignments.slice( + 0, + MAXIMUM_RENDERED_ALIGNMENTS, + ); + return ( +
+
+

Bounded comparison verdict

+

{result.status.replaceAll("-", " ")}

+

{result.notices[0]?.message}

+ + {result.elapsedMs.toFixed(1)} ms wall time ·{" "} + {result.totalDifferences.toLocaleString()} recorded difference(s) + +
+ {result.notComparable.length > 0 ? ( +
+

Why this run is not comparable

+
    + {result.notComparable.map((reason, index) => ( +
  • + {reason.code.replaceAll("-", " ")}:{" "} + {reason.message} +
  • + ))} +
+
+ ) : null} +
+ + +
+
+

Syntax, execution and replacement differences

+ {visibleDifferences.length === 0 ? ( +

+ No retained semantic differences for this subject. Engine metadata + can still differ. +

+ ) : ( + + + + + + + + + + + {visibleDifferences.map((difference, index) => ( + + + + + + + ))} + +
KindFindingECMAScriptPCRE2
{difference.kind.replaceAll("-", " ")}{difference.summary} + {preview(difference.left)} + + {preview(difference.right)} +
+ )} + {visibleDifferences.length < result.totalDifferences ? ( +

+ Rendering the first {visibleDifferences.length.toLocaleString()} of{" "} + {result.totalDifferences.toLocaleString()} differences. +

+ ) : null} +
+ {visibleAlignments.length > 0 ? ( +
+

Match alignment by normalized editor range

+ + + + + + + + + + + {visibleAlignments.map((alignment, index) => ( + + + + + + + ))} + +
AlignmentECMAScript UTF-16PCRE2 UTF-16Equal
{alignment.alignment.replaceAll("-", " ")} + {alignment.left + ? `${alignment.left.range.startUtf16}–${alignment.left.range.endUtf16}` + : "—"} + + {alignment.right + ? `${alignment.right.range.startUtf16}–${alignment.right.range.endUtf16}` + : "—"} + {alignment.equal ? "yes" : "no"}
+ {visibleAlignments.length < result.totalMatchAlignments ? ( +

+ Rendering the first {visibleAlignments.length.toLocaleString()} of{" "} + {result.totalMatchAlignments.toLocaleString()} alignments. +

+ ) : null} +
+ ) : null} +
+ ); +} + +export function ComparisonCodePanel({ + activeFlavour, + activeFlags, + activeOptions, + pattern: initialPattern, + subject: initialSubject, + replacement: initialReplacement, + scanAll: initialScanAll, + timeoutMs: initialTimeoutMs, + onClose, +}: ComparisonCodePanelProps) { + const [tab, setTab] = useState("compare"); + const [patternModel, setPatternModel] = + useState("shared"); + const [sharedPattern, setSharedPattern] = useState(initialPattern); + const [ecmaPattern, setEcmaPattern] = useState(initialPattern); + const [pcrePattern, setPcrePattern] = useState(initialPattern); + const [subject, setSubject] = useState(initialSubject); + const [operation, setOperation] = useState<"match" | "replace">("match"); + const [ecmaReplacement, setEcmaReplacement] = useState(initialReplacement); + const [pcreReplacement, setPcreReplacement] = useState(initialReplacement); + const [ecmaFlags, setEcmaFlags] = useState(() => + initialFlags(activeFlavour, activeFlags, "ecmascript"), + ); + const [pcreFlags, setPcreFlags] = useState(() => + initialFlags(activeFlavour, activeFlags, "pcre2"), + ); + const [pcreOptions, setPcreOptions] = useState(() => + initialOptions(activeFlavour, activeOptions), + ); + const [scanAll, setScanAll] = useState(initialScanAll); + const [timeoutMs, setTimeoutMs] = useState(initialTimeoutMs); + const [maximumMatches, setMaximumMatches] = useState(1_000); + const [maximumCaptureRows, setMaximumCaptureRows] = useState(10_000); + const [maximumOutputBytes, setMaximumOutputBytes] = useState(1024 * 1024); + const [status, setStatus] = useState({ + kind: "idle", + message: "Configure two exact engine requests.", + }); + const [result, setResult] = useState(); + const [generated, setGenerated] = useState(); + const [generatedStatus, setGeneratedStatus] = useState(""); + const orchestrator = useRef(undefined); + const comparisonRevision = useRef(0); + + useEffect( + () => () => { + comparisonRevision.current += 1; + orchestrator.current?.dispose(); + orchestrator.current = undefined; + }, + [], + ); + + const pcreOptionValues = useMemo( + () => ({ + matchLimit: Number(pcreOptions.matchLimit), + depthLimit: Number(pcreOptions.depthLimit), + heapLimitKib: Number(pcreOptions.heapLimitKib), + }), + [pcreOptions], + ); + + const compare = async () => { + const revision = ++comparisonRevision.current; + setResult(undefined); + setStatus({ + kind: "running", + message: "Running independently killable ECMAScript and PCRE2 workers…", + }); + try { + orchestrator.current ??= new ComparisonOrchestrator(); + const comparison = await orchestrator.current.compare({ + schemaVersion: 1, + patternModel, + operation, + subject, + scanAll, + maximumMatches, + maximumCaptureRows, + maximumOutputBytes, + timeoutMs, + sides: [ + { + flavour: "ecmascript", + flavourVersion: ECMASCRIPT.defaultVersion, + pattern: patternModel === "shared" ? sharedPattern : ecmaPattern, + flags: ecmaFlags, + options: {}, + ...(operation === "replace" + ? { replacement: ecmaReplacement } + : {}), + }, + { + flavour: "pcre2", + flavourVersion: PCRE2.defaultVersion, + pattern: patternModel === "shared" ? sharedPattern : pcrePattern, + flags: pcreFlags, + options: pcreOptionValues, + ...(operation === "replace" + ? { replacement: pcreReplacement } + : {}), + }, + ], + }); + if (revision !== comparisonRevision.current) return; + setResult(comparison); + setStatus({ + kind: "ready", + message: + comparison.status === "not-comparable" + ? "Both side outcomes are retained; equivalence is not claimed." + : "Both exact requests completed.", + }); + } catch (error) { + if (revision !== comparisonRevision.current) return; + setStatus({ + kind: "error", + message: error instanceof Error ? error.message : String(error), + }); + } + }; + + const cancel = () => { + comparisonRevision.current += 1; + orchestrator.current?.cancel(); + setStatus({ + kind: "idle", + message: "Comparison cancelled; both active workers were terminated.", + }); + }; + + const generate = () => { + setGenerated(undefined); + setGeneratedStatus(""); + try { + const program = generatePcre2C({ + flavour: "pcre2", + flavourVersion: PCRE2.defaultVersion, + operation, + pattern: patternModel === "shared" ? sharedPattern : pcrePattern, + flags: pcreFlags, + options: pcreOptionValues, + subject, + scanAll, + ...(operation === "replace" ? { replacement: pcreReplacement } : {}), + maximumMatches, + maximumCaptureRows, + maximumOutputBytes, + }); + setGenerated(program); + setGeneratedStatus( + "Generated locally from the exact PCRE2 snapshot shown above.", + ); + } catch (error) { + setGeneratedStatus( + error instanceof Error ? error.message : String(error), + ); + } + }; + + const setPcreOption = (name: string, value: number) => { + setPcreOptions((current) => ({ ...current, [name]: value })); + }; + + return ( +
+
+
+

Evidence, not automatic translation

+

Compare & generate

+
+ +
+
+ + +
+
+
+ + + +
+ {patternModel === "shared" ? ( +