diff --git a/go.mod b/go.mod index 7c79ff10e..7c253116d 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/hashicorp/raft-wal v0.4.2 github.com/jackc/pglogrepl v0.0.0-20260401131349-e37c41485510 github.com/jackc/pgx/v5 v5.10.0 - github.com/kaptinlin/jsonschema v0.9.3 + github.com/kaptinlin/jsonschema v0.9.6 github.com/klauspost/compress v1.19.1 github.com/lib/pq v1.12.3 github.com/mattn/go-sqlite3 v1.14.49 @@ -173,7 +173,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jsimonetti/rtnetlink v1.4.1 // indirect - github.com/kaptinlin/jsonpointer v0.4.27 // indirect + github.com/kaptinlin/jsonpointer v0.4.28 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect diff --git a/go.sum b/go.sum index ad19ef58c..45ccb52b7 100644 --- a/go.sum +++ b/go.sum @@ -315,10 +315,10 @@ github.com/jsimonetti/rtnetlink v1.4.1/go.mod h1:xJjT7t59UIZ62GLZbv6PLLo8VFrostJ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kaptinlin/jsonpointer v0.4.27 h1:5FOnhlkqQ4/lvHudaAWS8HJCXjN4yAHSIGl7aPKHI0Q= -github.com/kaptinlin/jsonpointer v0.4.27/go.mod h1:dfub/n58cWS32Dyf3AZsnKblSAgrz9PyOU76GDPpx8Q= -github.com/kaptinlin/jsonschema v0.9.3 h1:uDVd3w4aXwO0tbycblKYvFofhl3hVuE31vOl5JkDXI0= -github.com/kaptinlin/jsonschema v0.9.3/go.mod h1:LvtQ/mO0E1e/3c3DiOWTj05LTeot8cTv7DyQq7eJMQg= +github.com/kaptinlin/jsonpointer v0.4.28 h1:PyxOfdml9PGpxQz+JnNLGsyzQ+Rnm+OVNTGcgbVLqgg= +github.com/kaptinlin/jsonpointer v0.4.28/go.mod h1:JzeIDw1+60bryzOFea9P3zjKg8pfJHd+UIA/zfZ70x8= +github.com/kaptinlin/jsonschema v0.9.6 h1:RNqurZUqduQm+mINsCFbRzBQR0uKbAcRPBj3BqRA/I8= +github.com/kaptinlin/jsonschema v0.9.6/go.mod h1:TP7RFmFBNExjBE0b3fsM6RpA/9PeOzfg2g4weXqOrDg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= diff --git a/runtime/lua/modules/json/json.go b/runtime/lua/modules/json/json.go index 1629b494c..74bf33b87 100644 --- a/runtime/lua/modules/json/json.go +++ b/runtime/lua/modules/json/json.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "math" "sort" "strconv" @@ -638,7 +639,7 @@ func compileSchema(schemaJSON []byte) (*jsonschema.Schema, error) { return schema, nil } - compiler := jsonschema.NewCompiler() + compiler := jsonschema.NewCompiler().WithDecoderJSON(decodeSchemaInstance) schema, err := compiler.Compile(schemaJSON) if err != nil { return nil, err @@ -648,6 +649,23 @@ func compileSchema(schemaJSON []byte) (*jsonschema.Schema, error) { return schema, nil } +func decodeSchemaInstance(data []byte, value any) error { + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + if err := decoder.Decode(value); err != nil { + return err + } + + var trailing any + if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { + if err == nil { + return errors.New("multiple JSON values") + } + return err + } + return nil +} + func hashSchemaJSON(data []byte) string { hash := sha256.Sum256(data) return hex.EncodeToString(hash[:]) diff --git a/runtime/lua/modules/json/module_test.go b/runtime/lua/modules/json/module_test.go index d909948a8..23ae5aaeb 100644 --- a/runtime/lua/modules/json/module_test.go +++ b/runtime/lua/modules/json/module_test.go @@ -280,6 +280,41 @@ func TestValidateStringSuccess(t *testing.T) { } } +func TestValidateStringPreservesExactNumbers(t *testing.T) { + l := lua.NewState() + defer l.Close() + bindJSON(l) + + err := l.DoString(` + local schema = '{"type":"integer","const":9007199254740993}' + local valid, validation_err = json.validate_string(schema, '9007199254740993') + if not valid then error("expected exact integer to validate: " .. tostring(validation_err)) end + + valid, validation_err = json.validate_string(schema, '9007199254740992') + if valid then error("adjacent integer must not match exact const") end + if validation_err == nil then error("expected validation error") end + `) + if err != nil { + t.Errorf("validate_string exact number test failed: %v", err) + } +} + +func TestValidateStringRejectsTrailingJSONValue(t *testing.T) { + l := lua.NewState() + defer l.Close() + bindJSON(l) + + err := l.DoString(` + local schema = {type = "object"} + local valid, validation_err = json.validate_string(schema, '{} {}') + if valid then error("expected trailing JSON value to be rejected") end + if validation_err == nil then error("expected validation error") end + `) + if err != nil { + t.Errorf("validate_string trailing value test failed: %v", err) + } +} + func TestValidateMissingSchema(t *testing.T) { l := lua.NewState() defer l.Close()