// Regex Tools Go engine bridge. // // The WebAssembly module exposes two fixed callbacks. User-controlled values // enter only through JSON data and are handed to the standard-library regexp // package; no Go or JavaScript source is evaluated. package main import ( "bytes" "encoding/base64" "encoding/json" "fmt" "regexp" "runtime" "sort" "strings" "syscall/js" "unicode" "unicode/utf8" ) const ( bridgeABIVersion = 1 expectedGoVersion = "go1.26.5" maximumPatternBytes = 256 * 1024 maximumSubjectBytes = 16 * 1024 * 1024 maximumReplacementBytes = 256 * 1024 maximumMatches = 10_000 maximumCaptureRows = 100_000 maximumCaptureGroups = 1_000 maximumOutputBytes = 64 * 1024 * 1024 maximumRequestJSONBytes = 6*(maximumSubjectBytes+maximumPatternBytes+maximumReplacementBytes) + 64*1024 maximumErrorMessageBytes = 1_024 bridgeInstallerGlobalName = "__regexToolsInstallGoBridge" ) type bridgeRequest struct { ABI int `json:"abi"` Operation string `json:"operation"` Pattern string `json:"pattern"` Flags string `json:"flags"` Options map[string]json.RawMessage `json:"options"` Subject string `json:"subject"` ScanAll bool `json:"scanAll"` MaximumMatches int `json:"maximumMatches"` MaximumCaptureRows int `json:"maximumCaptureRows"` Replacement *string `json:"replacement,omitempty"` MaximumOutputBytes *int `json:"maximumOutputBytes,omitempty"` } type bridgeFailure struct { ABI int `json:"abi"` OK bool `json:"ok"` Phase string `json:"phase"` Code string `json:"code"` Message string `json:"message"` Position *int `json:"position,omitempty"` } type nativeMatch struct { Span [2]int `json:"span"` Captures []*[2]int `json:"captures"` } type bridgeSuccess struct { ABI int `json:"abi"` OK bool `json:"ok"` GroupCount int `json:"groupCount"` GroupNames [][2]any `json:"groupNames"` Matches []nativeMatch `json:"matches"` ResultsTruncated bool `json:"resultsTruncated"` OutputBase64 *string `json:"outputBase64,omitempty"` OutputBytes *int `json:"outputBytes,omitempty"` OutputTruncated *bool `json:"outputTruncated,omitempty"` } type identityEnvelope struct { ABI int `json:"abi"` OK bool `json:"ok"` Identity map[string]any `json:"identity"` } func boundedMessage(value string) string { if len(value) <= maximumErrorMessageBytes { return value } end := maximumErrorMessageBytes - len("…") for end > 0 && !utf8.ValidString(value[:end]) { end-- } return value[:end] + "…" } func marshal(value any) string { encoded, err := json.Marshal(value) if err == nil { return string(encoded) } fallback, _ := json.Marshal(bridgeFailure{ ABI: bridgeABIVersion, OK: false, Phase: "bridge", Code: "serialization-error", Message: "The Go bridge could not serialize its bounded result.", }) return string(fallback) } func failure(phase, code string, err any) string { return marshal(bridgeFailure{ ABI: bridgeABIVersion, OK: false, Phase: phase, Code: code, Message: boundedMessage(fmt.Sprint(err)), }) } func decodeRequest(encoded string) (bridgeRequest, string) { var request bridgeRequest if len(encoded) > maximumRequestJSONBytes { return request, failure("bridge", "request-limit", "The encoded request exceeds the bridge limit.") } decoder := json.NewDecoder(strings.NewReader(encoded)) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil { return request, failure("bridge", "invalid-request", err) } if decoder.More() { return request, failure("bridge", "invalid-request", "The encoded request contains trailing JSON values.") } return request, "" } func validateRequest(request bridgeRequest) string { if request.ABI != bridgeABIVersion { return failure("bridge", "abi-mismatch", "The bridge ABI version is unsupported.") } if request.Operation != "execute" && request.Operation != "replace" { return failure("bridge", "invalid-operation", "The bridge operation is unsupported.") } if len(request.Options) != 0 { return failure("bridge", "unsupported-option", "Go regexp does not accept engine options.") } if !utf8.ValidString(request.Pattern) || len(request.Pattern) > maximumPatternBytes { return failure("bridge", "pattern-limit", "The pattern is invalid UTF-8 or exceeds the bridge limit.") } if !utf8.ValidString(request.Subject) || len(request.Subject) > maximumSubjectBytes { return failure("bridge", "subject-limit", "The subject is invalid UTF-8 or exceeds the bridge limit.") } if request.MaximumMatches < 1 || request.MaximumMatches > maximumMatches || request.MaximumCaptureRows < 1 || request.MaximumCaptureRows > maximumCaptureRows { return failure("bridge", "result-limit", "The requested result bounds are outside the bridge contract.") } seenFlags := make(map[rune]bool) for _, flag := range request.Flags { if !strings.ContainsRune("gimsU", flag) || seenFlags[flag] { return failure("bridge", "unsupported-flag", "The request contains an unsupported or duplicate Go flag.") } seenFlags[flag] = true } if request.Operation == "execute" { if request.Replacement != nil || request.MaximumOutputBytes != nil { return failure("bridge", "invalid-request", "Execution requests cannot contain replacement fields.") } return "" } if request.Replacement == nil || request.MaximumOutputBytes == nil { return failure("bridge", "invalid-request", "Replacement requests require a template and output bound.") } if !utf8.ValidString(*request.Replacement) || len(*request.Replacement) > maximumReplacementBytes { return failure("bridge", "replacement-limit", "The replacement is invalid UTF-8 or exceeds the bridge limit.") } if *request.MaximumOutputBytes < 1 || *request.MaximumOutputBytes > maximumOutputBytes { return failure("bridge", "output-limit", "The output bound is outside the bridge contract.") } return "" } func compilePattern(pattern, flags string) (*regexp.Regexp, error) { var modifiers strings.Builder for _, flag := range "imsU" { if strings.ContainsRune(flags, flag) { modifiers.WriteRune(flag) } } if modifiers.Len() != 0 { pattern = "(?" + modifiers.String() + ")" + pattern } return regexp.Compile(pattern) } func materializeMatch(indices []int, groupCount int) nativeMatch { record := nativeMatch{ Span: [2]int{indices[0], indices[1]}, Captures: make([]*[2]int, groupCount), } for group := 0; group < groupCount; group++ { start := indices[2+group*2] end := indices[3+group*2] if start >= 0 { span := [2]int{start, end} record.Captures[group] = &span } } return record } func collectMatches( compiled *regexp.Regexp, subject string, global bool, maximumMatchCount int, maximumRows int, ) ([][]int, bool) { rowsPerMatch := compiled.NumSubexp() if rowsPerMatch < 1 { rowsPerMatch = 1 } allowed := maximumMatchCount if rowBound := maximumRows / rowsPerMatch; rowBound < allowed { allowed = rowBound } probeLimit := allowed + 1 var indices [][]int if global { indices = compiled.FindAllStringSubmatchIndex(subject, probeLimit) } else if candidate := compiled.FindStringSubmatchIndex(subject); candidate != nil { indices = [][]int{candidate} } if len(indices) <= allowed { return indices, false } return indices[:allowed], true } type boundedOutput struct { buffer bytes.Buffer maximum int truncated bool } func (output *boundedOutput) append(value string) { if output.truncated || value == "" { return } remaining := output.maximum - output.buffer.Len() if len(value) <= remaining { output.buffer.WriteString(value) return } end := remaining for end > 0 && !utf8.ValidString(value[:end]) { end-- } output.buffer.WriteString(value[:end]) output.truncated = true } func extractReplacementReference(value string) (name string, number int, rest string, ok bool) { if value == "" { return } braced := false if value[0] == '{' { braced = true value = value[1:] } index := 0 for index < len(value) { character, size := utf8.DecodeRuneInString(value[index:]) if !unicode.IsLetter(character) && !unicode.IsDigit(character) && character != '_' { break } index += size } if index == 0 { return } name = value[:index] if braced { if index >= len(value) || value[index] != '}' { return } index++ } number = 0 for position := 0; position < len(name); position++ { if name[position] < '0' || name[position] > '9' || number >= 1e8 { number = -1 break } number = number*10 + int(name[position]) - '0' } if name[0] == '0' && len(name) > 1 { number = -1 } rest = value[index:] ok = true return } func expandReplacementBounded( output *boundedOutput, compiled *regexp.Regexp, template, subject string, match []int, ) { for len(template) > 0 && !output.truncated { before, after, found := strings.Cut(template, "$") if !found { break } output.append(before) if output.truncated { return } template = after if template != "" && template[0] == '$' { output.append("$") template = template[1:] continue } name, number, rest, ok := extractReplacementReference(template) if !ok { output.append("$") continue } template = rest if number >= 0 { if 2*number+1 < len(match) && match[2*number] >= 0 { output.append(subject[match[2*number]:match[2*number+1]]) } continue } for index, candidate := range compiled.SubexpNames() { if name == candidate && 2*index+1 < len(match) && match[2*index] >= 0 { output.append(subject[match[2*index]:match[2*index+1]]) break } } } output.append(template) } func replaceBounded( compiled *regexp.Regexp, subject, replacement string, indices [][]int, maximumBytes int, ) (string, int, bool) { output := boundedOutput{maximum: maximumBytes} cursor := 0 for _, match := range indices { output.append(subject[cursor:match[0]]) expandReplacementBounded(&output, compiled, replacement, subject, match) cursor = match[1] } output.append(subject[cursor:]) value := output.buffer.Bytes() return base64.StdEncoding.EncodeToString(value), len(value), output.truncated } func run(encoded string) string { request, requestError := decodeRequest(encoded) if requestError != "" { return requestError } if validationError := validateRequest(request); validationError != "" { return validationError } compiled, err := compilePattern(request.Pattern, request.Flags) if err != nil { return failure("compile", "compile-error", err) } groupCount := compiled.NumSubexp() if groupCount > maximumCaptureGroups { return failure( "compile", "capture-group-limit", fmt.Sprintf("The pattern declares %d capture groups; the bridge limit is %d.", groupCount, maximumCaptureGroups), ) } names := make([][2]any, 0) for number, name := range compiled.SubexpNames() { if number > 0 && name != "" { names = append(names, [2]any{number, name}) } } sort.Slice(names, func(left, right int) bool { return names[left][0].(int) < names[right][0].(int) }) global := strings.ContainsRune(request.Flags, 'g') indices, resultsTruncated := collectMatches( compiled, request.Subject, global, request.MaximumMatches, request.MaximumCaptureRows, ) records := make([]nativeMatch, len(indices)) for index, match := range indices { records[index] = materializeMatch(match, groupCount) } result := bridgeSuccess{ ABI: bridgeABIVersion, OK: true, GroupCount: groupCount, GroupNames: names, Matches: records, ResultsTruncated: resultsTruncated, } if request.Operation == "replace" { outputBase64, outputBytes, outputTruncated := replaceBounded( compiled, request.Subject, *request.Replacement, indices, *request.MaximumOutputBytes, ) result.OutputBase64 = &outputBase64 result.OutputBytes = &outputBytes result.OutputTruncated = &outputTruncated } return marshal(result) } func selfTest() bool { if runtime.Version() != expectedGoVersion { return false } compiled, err := regexp.Compile(`(?P😀+)`) if err != nil { return false } match := compiled.FindStringSubmatchIndex("x😀😀") if len(match) != 4 || match[0] != 1 || match[1] != 9 || match[2] != 1 || match[3] != 9 || compiled.SubexpNames()[1] != "word" { return false } for _, template := range []string{ `${word}-$1-$$`, `$1x|${1}x`, `${missing}|$|${word`, `$01|${01}|$1234567890`, } { expected := string(compiled.ExpandString(nil, template, "x😀😀", match)) bounded := boundedOutput{maximum: 4 * 1024} expandReplacementBounded(&bounded, compiled, template, "x😀😀", match) if bounded.truncated || bounded.buffer.String() != expected { return false } } adversarial := boundedOutput{maximum: 5} expandReplacementBounded( &adversarial, compiled, strings.Repeat("$word", 32*1024), "x😀😀", match, ) if !adversarial.truncated || adversarial.buffer.String() != "😀" { return false } empty := regexp.MustCompile("a*").FindAllStringIndex("baaab", -1) return len(empty) == 3 && empty[0][0] == 0 && empty[0][1] == 0 && empty[1][0] == 1 && empty[1][1] == 4 && empty[2][0] == 5 && empty[2][1] == 5 } func identity() string { return marshal(identityEnvelope{ ABI: bridgeABIVersion, OK: true, Identity: map[string]any{ "bridge": "regex-tools-go-json", "bridgeVersion": bridgeABIVersion, "engine": "Go standard-library regexp", "engineVersion": "1.26.5", "goVersion": runtime.Version(), "offsetUnit": "utf8-byte", "package": "regexp", "selfTest": selfTest(), }, }) } var retainedCallbacks []js.Func func main() { identityCallback := js.FuncOf(func(_ js.Value, arguments []js.Value) any { if len(arguments) != 0 { return failure("bridge", "invalid-request", "The identity callback does not accept arguments.") } return identity() }) runCallback := js.FuncOf(func(_ js.Value, arguments []js.Value) (result any) { defer func() { if recovered := recover(); recovered != nil { result = failure("bridge", "panic", "The Go bridge stopped an unexpected internal panic.") } }() if len(arguments) != 1 || arguments[0].Type() != js.TypeString { return failure("bridge", "invalid-request", "The run callback requires one JSON string.") } return run(arguments[0].String()) }) retainedCallbacks = []js.Func{identityCallback, runCallback} installer := js.Global().Get(bridgeInstallerGlobalName) if installer.Type() != js.TypeFunction { panic("Regex Tools Go bridge installer is unavailable") } installer.Invoke(identityCallback, runCallback) select {} }