File tree

5 files changed

+203
-9
lines changed

5 files changed

+203
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import io..optimumcode.json.schema.internal.formats.IpV6FormatValidator
2525
import io..optimumcode.json.schema.internal.formats.IriFormatValidator
2626
import io..optimumcode.json.schema.internal.formats.IriReferenceFormatValidator
2727
import io..optimumcode.json.schema.internal.formats.JsonPointerFormatValidator
28+
import io..optimumcode.json.schema.internal.formats.RegexFormatValidator
2829
import io..optimumcode.json.schema.internal.formats.RelativeJsonPointerFormatValidator
2930
import io..optimumcode.json.schema.internal.formats.TimeFormatValidator
3031
import io..optimumcode.json.schema.internal.formats.UriFormatValidator
@@ -86,6 +87,7 @@ internal sealed class FormatAssertionFactory(
8687
"uri-template" to UriTemplateFormatValidator,
8788
"email" to EmailFormatValidator,
8889
"idn-email" to IdnEmailFormatValidator,
90+
"regex" to RegexFormatValidator,
8991
)
9092
}
9193
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package io..optimumcode.json.schema.internal.formats
2+
3+
import de.cketti.codepoints.CodePoints
4+
import de.cketti.codepoints.codePointAt
5+
import io..optimumcode.json.schema.FormatValidationResult
6+
import io..optimumcode.json.schema.FormatValidator
7+
8+
internal object RegexFormatValidator : AbstractStringFormatValidator() {
9+
private const val OPENING_CURLY_BRACKET = '{'.code
10+
private const val CLOSING_CURLY_BRACKET = '}'.code
11+
private const val OPENING_SQUARE_BRACKET = '['.code
12+
private const val CLOSING_SQUARE_BRACKET = ']'.code
13+
private const val OPENING_BRACKET = '('.code
14+
private const val CLOSING_BRACKET = ')'.code
15+
private const val ESCAPE = '\''.code
16+
17+
override fun validate(value: String): FormatValidationResult {
18+
if (value.isEmpty()) {
19+
return FormatValidator.Valid()
20+
}
21+
return if (isValidEcma262Regex(value)) {
22+
FormatValidator.Valid()
23+
} else {
24+
FormatValidator.Invalid()
25+
}
26+
}
27+
28+
private fun isValidEcma262Regex(value: String): Boolean {
29+
val brackets = ArrayDeque<Int>()
30+
var escaped = false
31+
var index = 0
32+
while (index < value.length) {
33+
val codePoint = value.codePointAt(index)
34+
index += CodePoints.charCount(codePoint)
35+
36+
if (!escaped) {
37+
// check brackets
38+
when (codePoint) {
39+
OPENING_CURLY_BRACKET,
40+
OPENING_SQUARE_BRACKET,
41+
OPENING_BRACKET,
42+
-> brackets.add(codePoint)
43+
44+
CLOSING_CURLY_BRACKET,
45+
CLOSING_SQUARE_BRACKET,
46+
CLOSING_BRACKET,
47+
-> {
48+
val prev = brackets.removeLastOrNull() ?: return false
49+
if (prev != oppositeBracket(codePoint)) {
50+
return false
51+
}
52+
}
53+
}
54+
}
55+
56+
if (codePoint == ESCAPE) {
57+
escaped = true
58+
continue
59+
}
60+
61+
val updatedIndex = checkGroupStart(index, value, codePoint, escaped)
62+
if (updatedIndex > 0) {
63+
index = updatedIndex
64+
} else {
65+
val nextIndex = checkValidPattern(index, value, codePoint, escaped)
66+
if (nextIndex < 0) {
67+
// invalid pattern
68+
return false
69+
}
70+
index = nextIndex
71+
}
72+
73+
escaped = false
74+
}
75+
return brackets.isEmpty() && !escaped
76+
}
77+
78+
private fun checkValidPattern(
79+
index: Int,
80+
value: String,
81+
codePoint: Int,
82+
escaped: Boolean,
83+
): Int {
84+
return if (escaped) {
85+
when (codePoint) {
86+
'x'.code -> checkHexEscape(value, index)
87+
'u'.code -> checkUnicodeEscape(value, index)
88+
'c'.code -> checkControlLetter(value, index)
89+
// control escape
90+
'f'.code, 'n'.code, 'r'.code, 't'.code, 'v'.code -> index
91+
// character class escape
92+
'd'.code, 'D'.code, 's'.code, 'S'.code, 'w'.code, 'W'.code -> index
93+
// assertion
94+
'b'.code, 'B'.code -> index
95+
else -> checkDecimalEscape(value, index)
96+
}
97+
} else {
98+
index
99+
}
100+
}
101+
102+
private fun checkDecimalEscape(
103+
value: String,
104+
index: Int,
105+
): Int {
106+
if (!Validation.isDigit(value[index])) {
107+
return -1
108+
}
109+
if (value[index] == '0') {
110+
return if (index + 1 >= value.length || !Validation.isDigit(value[index + 1])) {
111+
index
112+
} else {
113+
-1
114+
}
115+
}
116+
var lastDigitIndex = index
117+
for (i in index..<value.length) {
118+
if (!Validation.isDigit(value[i])) {
119+
break
120+
}
121+
lastDigitIndex = i
122+
}
123+
return lastDigitIndex
124+
}
125+
126+
private fun checkControlLetter(
127+
value: String,
128+
index: Int,
129+
): Int {
130+
return if (Validation.isAlpha(value[index])) {
131+
index
132+
} else {
133+
-1
134+
}
135+
}
136+
137+
private fun checkUnicodeEscape(
138+
value: String,
139+
index: Int,
140+
): Int {
141+
val lastIndex = index + 3
142+
if (lastIndex >= value.length) {
143+
return -1
144+
}
145+
for (i in index..lastIndex) {
146+
if (!Validation.isHexDigit(value[i])) {
147+
return -1
148+
}
149+
}
150+
return lastIndex
151+
}
152+
153+
private fun checkHexEscape(
154+
value: String,
155+
index: Int,
156+
): Int {
157+
val lastIndex = index + 1
158+
if (lastIndex >= value.length) {
159+
return -1
160+
}
161+
return if (Validation.isHexDigit(value[index]) && Validation.isHexDigit(value[lastIndex])) {
162+
lastIndex
163+
} else {
164+
-1
165+
}
166+
}
167+
168+
private fun checkGroupStart(
169+
nextIndex: Int,
170+
value: String,
171+
codePoint: Int,
172+
escaped: Boolean,
173+
): Int {
174+
return if (!escaped && codePoint == OPENING_BRACKET) {
175+
if (
176+
value.regionMatches(nextIndex, "?=", 0, 2) ||
177+
value.regionMatches(nextIndex, "?!", 0, 2) ||
178+
value.regionMatches(nextIndex, "?:", 0, 2)
179+
) {
180+
nextIndex + 1
181+
} else {
182+
nextIndex
183+
}
184+
} else {
185+
-1
186+
}
187+
}
188+
189+
private fun oppositeBracket(bracket: Int): Int {
190+
return when (bracket) {
191+
CLOSING_BRACKET -> OPENING_BRACKET
192+
CLOSING_CURLY_BRACKET -> OPENING_CURLY_BRACKET
193+
CLOSING_SQUARE_BRACKET -> OPENING_SQUARE_BRACKET
194+
else -> error("no pair for bracket with code ${bracket.toString(16)}")
195+
}
196+
}
197+
}
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io..optimumcode.json.schema.internal.formats
22

33
import io..optimumcode.json.schema.internal.formats.Validation.isAlpha
44
import io..optimumcode.json.schema.internal.formats.Validation.isDigit
5+
import io..optimumcode.json.schema.internal.formats.Validation.isHexDigit
56

67
internal object UriSpec {
78
const val SCHEMA_DELIMITER = ':'
@@ -278,6 +279,4 @@ internal object UriSpec {
278279
private fun isSubDelimiter(c: Char): Boolean =
279280
c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' ||
280281
c == '*' || c == '+' || c == ',' || c == ';' || c == '='
281-
282-
private fun isHexDigit(c: Char): Boolean = c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F'
283282
}
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ internal object Validation {
55

66
fun isDigit(c: Char): Boolean = c in '0'..'9'
77

8+
fun isHexDigit(c: Char): Boolean = c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F'
9+
810
inline fun eachSeparatedPart(
911
value: String,
1012
separator: Char,
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,7 @@ internal class TestFilter(
4343
val excludeTests: Map<String, Set<String>> = emptyMap(),
4444
)
4545

46-
internal val COMMON_FORMAT_FILTER =
47-
TestFilter(
48-
excludeSuites =
49-
mapOf(
50-
"regex" to emptySet(),
51-
),
52-
)
46+
internal val COMMON_FORMAT_FILTER = TestFilter()
5347

5448
/**
5549
* This class is a base for creating a test suite run from https://.com/json-schema-org/JSON-Schema-Test-Suite.

0 commit comments

Comments
 (0)