From 2a0cad9ebfd6a2e57c4dc7557a6c344e28809b23 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Mon, 26 Aug 2024 13:40:55 +0200 Subject: [PATCH] Introduce an option to override the regex implementation (#1006) --- .github/docs/openapi3.txt | 14 ++++++++++++++ .github/docs/openapi3filter.txt | 3 +++ openapi3/schema.go | 7 +++---- openapi3/schema_pattern.go | 10 ++++++++-- openapi3/schema_validation_settings.go | 13 +++++++++++++ openapi3/validation_options.go | 9 +++++++++ openapi3filter/options.go | 3 +++ openapi3filter/validate_request.go | 3 +++ 8 files changed, 56 insertions(+), 6 deletions(-) diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 09f1192ab..cfccb97aa 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -1240,6 +1240,12 @@ type RefNameResolver func(*T, ComponentRef) string The function should avoid name collisions (i.e. be a injective mapping). It must only contain characters valid for fixed field names: IdentifierRegExp. +type RegexCompilerFunc func(expr string) (RegexMatcher, error) + +type RegexMatcher interface { + MatchString(s string) bool +} + type RequestBodies map[string]*RequestBodyRef func (m RequestBodies) JSONLookup(token string) (any, error) @@ -1764,6 +1770,10 @@ func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaVali If the passed function returns an empty string, it returns to the previous Error() implementation. +func SetSchemaRegexCompiler(c RegexCompilerFunc) SchemaValidationOption + SetSchemaRegexCompiler allows to override the regex implementation used to + validate field "pattern". + func VisitAsRequest() SchemaValidationOption func VisitAsResponse() SchemaValidationOption @@ -2128,6 +2138,10 @@ func ProhibitExtensionsWithRef() ValidationOption fields. Non-extension fields are prohibited unless allowed explicitly with the AllowExtraSiblingFields option. +func SetRegexCompiler(c RegexCompilerFunc) ValidationOption + SetRegexCompiler allows to override the regex implementation used to + validate field "pattern". + type ValidationOptions struct { // Has unexported fields. } diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt index 626cd24a7..d44ba5ab8 100644 --- a/.github/docs/openapi3filter.txt +++ b/.github/docs/openapi3filter.txt @@ -223,6 +223,9 @@ type Options struct { MultiError bool + // Set RegexCompiler to override the regex implementation + RegexCompiler openapi3.RegexCompilerFunc + // A document with security schemes defined will not pass validation // unless an AuthenticationFunc is defined. // See NoopAuthenticationFunc diff --git a/openapi3/schema.go b/openapi3/schema.go index 7be6bd38e..f81196066 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -9,7 +9,6 @@ import ( "math" "math/big" "reflect" - "regexp" "sort" "strconv" "strings" @@ -1019,7 +1018,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, } } if !validationOpts.schemaPatternValidationDisabled && schema.Pattern != "" { - if _, err := schema.compilePattern(); err != nil { + if _, err := schema.compilePattern(validationOpts.regexCompilerFunc); err != nil { return stack, err } } @@ -1729,10 +1728,10 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value // "pattern" if !settings.patternValidationDisabled && schema.Pattern != "" { cpiface, _ := compiledPatterns.Load(schema.Pattern) - cp, _ := cpiface.(*regexp.Regexp) + cp, _ := cpiface.(RegexMatcher) if cp == nil { var err error - if cp, err = schema.compilePattern(); err != nil { + if cp, err = schema.compilePattern(settings.regexCompiler); err != nil { if !settings.multiError { return err } diff --git a/openapi3/schema_pattern.go b/openapi3/schema_pattern.go index 4794b6a0d..581971378 100644 --- a/openapi3/schema_pattern.go +++ b/openapi3/schema_pattern.go @@ -13,9 +13,14 @@ func intoGoRegexp(re string) string { } // NOTE: racey WRT [writes to schema.Pattern] vs [reads schema.Pattern then writes to compiledPatterns] -func (schema *Schema) compilePattern() (cp *regexp.Regexp, err error) { +func (schema *Schema) compilePattern(c RegexCompilerFunc) (cp RegexMatcher, err error) { pattern := schema.Pattern - if cp, err = regexp.Compile(intoGoRegexp(pattern)); err != nil { + if c != nil { + cp, err = c(pattern) + } else { + cp, err = regexp.Compile(intoGoRegexp(pattern)) + } + if err != nil { err = &SchemaError{ Schema: schema, SchemaField: "pattern", @@ -24,6 +29,7 @@ func (schema *Schema) compilePattern() (cp *regexp.Regexp, err error) { } return } + var _ bool = compiledPatterns.CompareAndSwap(pattern, nil, cp) return } diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index 17aad2fa7..e9c1422bd 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -7,6 +7,12 @@ import ( // SchemaValidationOption describes options a user has when validating request / response bodies. type SchemaValidationOption func(*schemaValidationSettings) +type RegexCompilerFunc func(expr string) (RegexMatcher, error) + +type RegexMatcher interface { + MatchString(s string) bool +} + type schemaValidationSettings struct { failfast bool multiError bool @@ -16,6 +22,8 @@ type schemaValidationSettings struct { readOnlyValidationDisabled bool writeOnlyValidationDisabled bool + regexCompiler RegexCompilerFunc + onceSettingDefaults sync.Once defaultsSet func() @@ -70,6 +78,11 @@ func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaVali return func(s *schemaValidationSettings) { s.customizeMessageError = f } } +// SetSchemaRegexCompiler allows to override the regex implementation used to validate field "pattern". +func SetSchemaRegexCompiler(c RegexCompilerFunc) SchemaValidationOption { + return func(s *schemaValidationSettings) { s.regexCompiler = c } +} + func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidationSettings { settings := &schemaValidationSettings{} for _, opt := range opts { diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 45563256a..1d141d40a 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -13,6 +13,7 @@ type ValidationOptions struct { schemaFormatValidationEnabled bool schemaPatternValidationDisabled bool schemaExtensionsInRefProhibited bool + regexCompilerFunc RegexCompilerFunc extraSiblingFieldsAllowed map[string]struct{} } @@ -113,6 +114,14 @@ func ProhibitExtensionsWithRef() ValidationOption { } } +// SetRegexCompiler allows to override the regex implementation used to validate +// field "pattern". +func SetRegexCompiler(c RegexCompilerFunc) ValidationOption { + return func(options *ValidationOptions) { + options.regexCompilerFunc = c + } +} + // WithValidationOptions allows adding validation options to a context object that can be used when validating any OpenAPI type. func WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context { if len(opts) == 0 { diff --git a/openapi3filter/options.go b/openapi3filter/options.go index 9b915c50b..e7fad8321 100644 --- a/openapi3filter/options.go +++ b/openapi3filter/options.go @@ -25,6 +25,9 @@ type Options struct { MultiError bool + // Set RegexCompiler to override the regex implementation + RegexCompiler openapi3.RegexCompilerFunc + // A document with security schemes defined will not pass validation // unless an AuthenticationFunc is defined. // See NoopAuthenticationFunc diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 3892a39ae..bf4771a98 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -316,6 +316,9 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req if options.ExcludeReadOnlyValidations { opts = append(opts, openapi3.DisableReadOnlyValidation()) } + if options.RegexCompiler != nil { + opts = append(opts, openapi3.SetSchemaRegexCompiler(options.RegexCompiler)) + } // Validate JSON with the schema if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil {