From 9544ae2a982aaa1c7e8a5742099777ee9a051ce2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 12:17:07 +0000 Subject: [PATCH 1/4] Bump test-suites/schema-test-suite from `e99b24c` to `be58fa9` Bumps [test-suites/schema-test-suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) from `e99b24c` to `be58fa9`. - [Release notes](https://github.com/json-schema-org/JSON-Schema-Test-Suite/releases) - [Commits](https://github.com/json-schema-org/JSON-Schema-Test-Suite/compare/e99b24c92006fd83803be511d77278910986d6aa...be58fa98d5f79bb8d20f45a15e540332669ad947) --- updated-dependencies: - dependency-name: test-suites/schema-test-suite dependency-version: be58fa98d5f79bb8d20f45a15e540332669ad947 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- test-suites/schema-test-suite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suites/schema-test-suite b/test-suites/schema-test-suite index e99b24c9..be58fa98 160000 --- a/test-suites/schema-test-suite +++ b/test-suites/schema-test-suite @@ -1 +1 @@ -Subproject commit e99b24c92006fd83803be511d77278910986d6aa +Subproject commit be58fa98d5f79bb8d20f45a15e540332669ad947 From dfb1ce785bca6096f3dedc4afb91ef1d35d45fdf Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 8 Nov 2025 12:59:24 +0100 Subject: [PATCH 2/4] fix idn-hostname validation --- .../schema/internal/formats/IdnHostnameFormatValidator.kt | 7 ++++++- .../format/JsonSchemaIdnHostnameFormatValidationTest.kt | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/formats/IdnHostnameFormatValidator.kt b/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/formats/IdnHostnameFormatValidator.kt index 29e622e6..c119708f 100644 --- a/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/formats/IdnHostnameFormatValidator.kt +++ b/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/formats/IdnHostnameFormatValidator.kt @@ -48,7 +48,7 @@ internal object IdnHostnameFormatValidator : AbstractStringFormatValidator() { if (value.isEmpty()) { return FormatValidator.Invalid() } - if (value.length == 1 && isLabelSeparator(value[0])) { + if (isLabelSeparator(value[0]) || isLabelSeparator(value[value.lastIndex])) { return FormatValidator.Invalid() } @@ -113,6 +113,11 @@ internal object IdnHostnameFormatValidator : AbstractStringFormatValidator() { return false } + if (unicode.isEmpty()) { + // empty labels are not valid + return false + } + // https://datatracker.ietf.org/doc/html/rfc5891#section-4.2.3.1 if (unicode[0] == '-' || unicode.codePointBefore(unicode.length) == '-'.code) { // cannot start or end with hyphen diff --git a/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/assertions/general/format/JsonSchemaIdnHostnameFormatValidationTest.kt b/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/assertions/general/format/JsonSchemaIdnHostnameFormatValidationTest.kt index 0b955f6d..ef2821d3 100644 --- a/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/assertions/general/format/JsonSchemaIdnHostnameFormatValidationTest.kt +++ b/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/assertions/general/format/JsonSchemaIdnHostnameFormatValidationTest.kt @@ -33,6 +33,9 @@ class JsonSchemaIdnHostnameFormatValidationTest : FunSpec() { listOf( TestCase("", "empty value"), TestCase(".", "single separator"), + TestCase(".example", "leading separator"), + TestCase("example.", "trailing separator"), + TestCase("example..com", "two separators in a row"), TestCase("\u3002", "single separator U+3002"), TestCase("\uFF0E", "single separator U+FF0E"), TestCase("\uFF61", "single separator U+FF61"), From e04189d5fbee1f19de26950c5f2d7f069671260c Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 8 Nov 2025 15:42:32 +0100 Subject: [PATCH 3/4] add URI normalization when building reference URI --- .../json/schema/internal/SchemaLoader.kt | 38 ++++++++ .../json/schema/base/JsonSchemaTest.kt | 86 ++++++++++--------- 2 files changed, 82 insertions(+), 42 deletions(-) diff --git a/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt b/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt index 13638f19..e1a82751 100644 --- a/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt +++ b/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt @@ -712,8 +712,46 @@ private fun Uri.appendPathToParent(path: String): Uri { } }.appendEncodedPath(path) .build() + .normalizeUri() } +private fun Uri.normalizeUri(): Uri { + if (pathSegments.none { it in RELATIVE_PATH_SEGMENTS }) { + // Nothing to normalize + return this + } + + val newPathSegments = ArrayDeque() + for (segment in pathSegments) { + when (segment) { + SAME_LEVEL_SEGMENT -> { // skip + } + + PARENT_LEVEL_SEGMENT -> + if (newPathSegments.isEmpty()) { + error("cannot normalize URI '$this'. Path goes beyond root") + } else { + newPathSegments.removeLast() + } + + else -> newPathSegments.addLast(segment) + } + } + + return buildUpon() + .encodedPath(null) + .apply { + for (segment in newPathSegments) { + appendEncodedPath(segment) + } + }.build() +} + +private const val SAME_LEVEL_SEGMENT = "." +private const val PARENT_LEVEL_SEGMENT = ".." + +private val RELATIVE_PATH_SEGMENTS = setOf(SAME_LEVEL_SEGMENT, PARENT_LEVEL_SEGMENT) + private val ANCHOR_REGEX: Regex = "^[A-Za-z][A-Za-z0-9-_:.]*$".toRegex() private fun Uri.buildRefId(): RefId = RefId(this) diff --git a/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/base/JsonSchemaTest.kt b/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/base/JsonSchemaTest.kt index bd503fa5..c6704dbd 100644 --- a/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/base/JsonSchemaTest.kt +++ b/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/base/JsonSchemaTest.kt @@ -21,9 +21,9 @@ class JsonSchemaTest : FunSpec() { test("loads schema object from string description") { shouldNotThrowAny { JsonSchema.fromDefinition( - """ + $$""" { - "${KEY}schema": "http://json-schema.org/draft-07/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "type": "string" } """.trimIndent(), @@ -46,9 +46,9 @@ class JsonSchemaTest : FunSpec() { test("loads schema with definitions") { shouldNotThrowAny { JsonSchema.fromDefinition( - """ + $$""" { - "${KEY}schema": "http://json-schema.org/draft-07/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "positiveInteger": { "type": "integer", @@ -64,11 +64,11 @@ class JsonSchemaTest : FunSpec() { test("loads schema with self reference") { shouldNotThrowAny { JsonSchema.fromDefinition( - """ + $$""" { - "${KEY}schema": "http://json-schema.org/draft-07/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "properties": { - "other": { "${KEY}ref": "#" } + "other": { "$ref": "#" } } } """.trimIndent(), @@ -79,9 +79,9 @@ class JsonSchemaTest : FunSpec() { test("reports missing reference") { shouldThrow { JsonSchema.fromDefinition( - """ + $$""" { - "${KEY}schema": "http://json-schema.org/draft-07/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "positiveInteger": { "type": "integer", @@ -90,7 +90,7 @@ class JsonSchemaTest : FunSpec() { }, "properties": { "size": { - "${KEY}ref": "#/definitions/positiveIntege" + "$ref": "#/definitions/positiveIntege" } } } @@ -115,6 +115,7 @@ class JsonSchemaTest : FunSpec() { "http://example.com/other.json", "http://example.com/other.json#", "http://example.com/root.json#/definitions/B", + "./other.json", ), "definition X" to listOf( @@ -136,7 +137,8 @@ class JsonSchemaTest : FunSpec() { "http://example.com/root.json#/definitions/C", ), ).forEach { (refDestination, possibleRefs) -> - possibleRefs.asSequence() + possibleRefs + .asSequence() .flatMapIndexed { index, ref -> val uri = Uri.parse(ref) val caseNumber = index + 1 @@ -150,24 +152,24 @@ class JsonSchemaTest : FunSpec() { withClue(ref) { shouldNotThrowAny { JsonSchema.fromDefinition( - """ + $$""" { - "${KEY}id": "http://example.com/root.json", + "$id": "http://example.com/root.json", "definitions": { - "A": { "${KEY}id": "#foo" }, + "A": { "$id": "#foo" }, "B": { - "${KEY}id": "other.json", + "$id": "other.json", "definitions": { - "X": { "${KEY}id": "#bar" }, - "Y": { "${KEY}id": "t/inner.json" } + "X": { "$id": "#bar" }, + "Y": { "$id": "t/inner.json" } } }, "C": { - "${KEY}id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" + "$id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" } }, "properties": { - "test": { "${KEY}ref": "$ref" } + "test": { "$ref": "$$ref" } } } """.trimIndent(), @@ -187,9 +189,9 @@ class JsonSchemaTest : FunSpec() { test("loads schema with supported '$it' \$schema property") { shouldNotThrowAny { JsonSchema.fromDefinition( - """ + $$""" { - "${KEY}schema": "$it", + "$schema": "$$it", "type": "string" } """.trimIndent(), @@ -205,9 +207,9 @@ class JsonSchemaTest : FunSpec() { test("reports unsupported '$it' \$schema property") { shouldThrow { JsonSchema.fromDefinition( - """ + $$""" { - "${KEY}schema": "$it", + "$schema": "$$it", "type": "string" } """.trimIndent(), @@ -219,53 +221,53 @@ class JsonSchemaTest : FunSpec() { test("\$dynamicRef is resolved every time") { val schema = JsonSchema.fromDefinition( - """ + $$""" { - "${KEY}schema": "https://json-schema.org/draft/2020-12/schema", - "${KEY}id": "https://test.json-schema.org/dynamic-ref-with-multiple-paths/main", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://test.json-schema.org/dynamic-ref-with-multiple-paths/main", "if": { "properties": { "kindOfList": { "const": "numbers" } }, "required": ["kindOfList"] }, - "then": { "${KEY}ref": "numberList" }, - "else": { "${KEY}ref": "stringList" }, + "then": { "$ref": "numberList" }, + "else": { "$ref": "stringList" }, - "${KEY}defs": { + "$defs": { "genericList": { - "${KEY}id": "genericList", + "$id": "genericList", "properties": { "list": { - "items": { "${KEY}dynamicRef": "#itemType" } + "items": { "$dynamicRef": "#itemType" } } }, - "${KEY}defs": { + "$defs": { "defaultItemType": { - "${KEY}comment": "Only needed to satisfy bookending requirement", - "${KEY}dynamicAnchor": "itemType" + "$comment": "Only needed to satisfy bookending requirement", + "$dynamicAnchor": "itemType" } } }, "numberList": { - "${KEY}id": "numberList", - "${KEY}defs": { + "$id": "numberList", + "$defs": { "itemType": { - "${KEY}dynamicAnchor": "itemType", + "$dynamicAnchor": "itemType", "type": "number" } }, - "${KEY}ref": "genericList" + "$ref": "genericList" }, "stringList": { - "${KEY}id": "stringList", - "${KEY}defs": { + "$id": "stringList", + "$defs": { "itemType": { - "${KEY}dynamicAnchor": "itemType", + "$dynamicAnchor": "itemType", "type": "string" } }, - "${KEY}ref": "genericList" + "$ref": "genericList" } } } From 309a5556d017637d94143b34660cd4108a99262e Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 8 Nov 2025 15:58:02 +0100 Subject: [PATCH 4/4] rollback $$ usage --- .../json/schema/base/JsonSchemaTest.kt | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/base/JsonSchemaTest.kt b/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/base/JsonSchemaTest.kt index c6704dbd..00930631 100644 --- a/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/base/JsonSchemaTest.kt +++ b/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/base/JsonSchemaTest.kt @@ -21,9 +21,9 @@ class JsonSchemaTest : FunSpec() { test("loads schema object from string description") { shouldNotThrowAny { JsonSchema.fromDefinition( - $$""" + """ { - "$schema": "http://json-schema.org/draft-07/schema#", + "${KEY}schema": "http://json-schema.org/draft-07/schema#", "type": "string" } """.trimIndent(), @@ -46,9 +46,9 @@ class JsonSchemaTest : FunSpec() { test("loads schema with definitions") { shouldNotThrowAny { JsonSchema.fromDefinition( - $$""" + """ { - "$schema": "http://json-schema.org/draft-07/schema#", + "${KEY}schema": "http://json-schema.org/draft-07/schema#", "definitions": { "positiveInteger": { "type": "integer", @@ -64,11 +64,11 @@ class JsonSchemaTest : FunSpec() { test("loads schema with self reference") { shouldNotThrowAny { JsonSchema.fromDefinition( - $$""" + """ { - "$schema": "http://json-schema.org/draft-07/schema#", + "${KEY}schema": "http://json-schema.org/draft-07/schema#", "properties": { - "other": { "$ref": "#" } + "other": { "${KEY}ref": "#" } } } """.trimIndent(), @@ -79,9 +79,9 @@ class JsonSchemaTest : FunSpec() { test("reports missing reference") { shouldThrow { JsonSchema.fromDefinition( - $$""" + """ { - "$schema": "http://json-schema.org/draft-07/schema#", + "${KEY}schema": "http://json-schema.org/draft-07/schema#", "definitions": { "positiveInteger": { "type": "integer", @@ -90,7 +90,7 @@ class JsonSchemaTest : FunSpec() { }, "properties": { "size": { - "$ref": "#/definitions/positiveIntege" + "${KEY}ref": "#/definitions/positiveIntege" } } } @@ -152,24 +152,24 @@ class JsonSchemaTest : FunSpec() { withClue(ref) { shouldNotThrowAny { JsonSchema.fromDefinition( - $$""" + """ { - "$id": "http://example.com/root.json", + "${KEY}id": "http://example.com/root.json", "definitions": { - "A": { "$id": "#foo" }, + "A": { "${KEY}id": "#foo" }, "B": { - "$id": "other.json", + "${KEY}id": "other.json", "definitions": { - "X": { "$id": "#bar" }, - "Y": { "$id": "t/inner.json" } + "X": { "${KEY}id": "#bar" }, + "Y": { "${KEY}id": "t/inner.json" } } }, "C": { - "$id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" + "${KEY}id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" } }, "properties": { - "test": { "$ref": "$$ref" } + "test": { "${KEY}ref": "$ref" } } } """.trimIndent(), @@ -189,9 +189,9 @@ class JsonSchemaTest : FunSpec() { test("loads schema with supported '$it' \$schema property") { shouldNotThrowAny { JsonSchema.fromDefinition( - $$""" + """ { - "$schema": "$$it", + "${KEY}schema": "$it", "type": "string" } """.trimIndent(), @@ -207,9 +207,9 @@ class JsonSchemaTest : FunSpec() { test("reports unsupported '$it' \$schema property") { shouldThrow { JsonSchema.fromDefinition( - $$""" + """ { - "$schema": "$$it", + "${KEY}schema": "$it", "type": "string" } """.trimIndent(), @@ -221,53 +221,53 @@ class JsonSchemaTest : FunSpec() { test("\$dynamicRef is resolved every time") { val schema = JsonSchema.fromDefinition( - $$""" + """ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/dynamic-ref-with-multiple-paths/main", + "${KEY}schema": "https://json-schema.org/draft/2020-12/schema", + "${KEY}id": "https://test.json-schema.org/dynamic-ref-with-multiple-paths/main", "if": { "properties": { "kindOfList": { "const": "numbers" } }, "required": ["kindOfList"] }, - "then": { "$ref": "numberList" }, - "else": { "$ref": "stringList" }, + "then": { "${KEY}ref": "numberList" }, + "else": { "${KEY}ref": "stringList" }, - "$defs": { + "${KEY}defs": { "genericList": { - "$id": "genericList", + "${KEY}id": "genericList", "properties": { "list": { - "items": { "$dynamicRef": "#itemType" } + "items": { "${KEY}dynamicRef": "#itemType" } } }, - "$defs": { + "${KEY}defs": { "defaultItemType": { - "$comment": "Only needed to satisfy bookending requirement", - "$dynamicAnchor": "itemType" + "${KEY}comment": "Only needed to satisfy bookending requirement", + "${KEY}dynamicAnchor": "itemType" } } }, "numberList": { - "$id": "numberList", - "$defs": { + "${KEY}id": "numberList", + "${KEY}defs": { "itemType": { - "$dynamicAnchor": "itemType", + "${KEY}dynamicAnchor": "itemType", "type": "number" } }, - "$ref": "genericList" + "${KEY}ref": "genericList" }, "stringList": { - "$id": "stringList", - "$defs": { + "${KEY}id": "stringList", + "${KEY}defs": { "itemType": { - "$dynamicAnchor": "itemType", + "${KEY}dynamicAnchor": "itemType", "type": "string" } }, - "$ref": "genericList" + "${KEY}ref": "genericList" } } }