Getting started¶
This tutorial walks you through compiling a regular expression that came from a
config file — the canonical untrusted source — using CompileBounded, and
handling each way it can fail. By the end you will know how to branch on the
three sentinel errors and, just as importantly, when not to use this package
at all.
You will build one small, self-contained program so you can see every moving part.
Prerequisites¶
- Go 1.26 or newer.
- A new module to experiment in:
mkdir regexutil-tutorial && cd regexutil-tutorial
go mod init example.test/regexutil-tutorial
go get gitlab.com/phpboyscout/go/regexutil
Step 1 — Compile a config-supplied pattern¶
Imagine your tool lets an operator define which branches to act on via a regex
in a config file. That value is untrusted: it can be arbitrarily long, or
pathological, or simply mistyped. Route it through CompileBounded instead of
regexp.Compile.
CompileBounded takes a context.Context and the pattern, and returns the
usual *regexp.Regexp plus an error:
ctx := context.Background()
re, err := regexutil.CompileBounded(ctx, patternFromConfig)
if err != nil {
// handled in the next step
}
Under the hood it applies two bounds before you get a compiled regex back: it
rejects the pattern outright if it exceeds MaxPatternLength (1024 bytes), and
it aborts the compile if it runs longer than DefaultCompileTimeout (100 ms).
On success you get an ordinary *regexp.Regexp you can use exactly as if you
had called regexp.Compile.
Step 2 — Handle each failure mode¶
The error returned on failure wraps one of three sentinels, so you can tell
the failure modes apart with errors.Is. Use cockroachdb/errors (the same
package regexutil wraps with); the standard library's errors.Is works too,
since the sentinels are ordinary error values.
switch {
case errors.Is(err, regexutil.ErrPatternTooLong):
// The pattern exceeded MaxPatternLength and was rejected without being
// compiled. Ask the operator to shorten it.
case errors.Is(err, regexutil.ErrPatternCompileTimeout):
// Compilation did not finish within the timeout — a sign of a
// pathological pattern. Ask for a simpler expression.
case errors.Is(err, regexutil.ErrPatternInvalid):
// The pattern is not valid regex syntax (regexp.Compile rejected it).
case err != nil:
// Defensive: no other error kind is expected, but never assume.
default:
// err == nil — re is ready to use.
}
Why
errors.Is, not==. The returned error is a wrapped value with an attached hint, not the bare sentinel. Comparing with==would miss it;errors.Isunwraps the chain and matches the sentinel underneath.
Note the distinction between the first two branches. ErrPatternTooLong is
cheap — the length is checked and the pattern is rejected before any compile
work starts. ErrPatternCompileTimeout means a short-enough pattern still spent
too long compiling, which is the pathological case the timeout exists to catch.
Step 3 — The complete program¶
package main
import (
"context"
"fmt"
"github.com/cockroachdb/errors"
"gitlab.com/phpboyscout/go/regexutil"
)
func main() {
// Pretend this came from a config file.
patternFromConfig := `^release/v\d+\.\d+\.\d+$`
re, err := regexutil.CompileBounded(context.Background(), patternFromConfig)
switch {
case errors.Is(err, regexutil.ErrPatternTooLong):
fmt.Println("pattern rejected: too long — shorten it")
return
case errors.Is(err, regexutil.ErrPatternCompileTimeout):
fmt.Println("pattern rejected: too complex to compile safely")
return
case errors.Is(err, regexutil.ErrPatternInvalid):
fmt.Println("pattern rejected: invalid regex syntax")
return
case err != nil:
fmt.Println("unexpected error:", err)
return
}
fmt.Println("release/v1.2.3 matches:", re.MatchString("release/v1.2.3"))
fmt.Println("main matches:", re.MatchString("main"))
}
Run it:
You should see release/v1.2.3 matches: true and main matches: false. Try
editing patternFromConfig to broken syntax such as (unclosed and rerun — the
program prints the invalid-syntax branch.
When not to use regexutil¶
regexutil exists to bound patterns you do not control. If the pattern is a
literal known at build time, there is nothing to defend against — you wrote
it — and the bounds are pure overhead. Keep using regexp.MustCompile for those:
// Build-time literal: you own it, so this is correct and idiomatic.
var semverRE = regexp.MustCompile(`^v\d+\.\d+\.\d+$`)
Reach for regexutil only when the pattern crosses a trust boundary into your
process — config file, CLI flag, TUI input, HTTP payload, message queue.
Next steps¶
- Choose between the two entry points and pick a timeout: Compile an untrusted pattern.
- Understand why compilation is the risk and RE2's match-time safety does not cover it: Threat model & the two defences.