Skip to content

Commit 554cbc4

Browse files
committed
Extract common uri validations. Add uri-reference format
1 parent 7acf38b commit 554cbc4

File tree

7 files changed

+382
-268
lines changed

7 files changed

+382
-268
lines changed

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/general/FormatAssertionFactory.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.github.optimumcode.json.schema.internal.formats.JsonPointerFormatValid
2424
import io.github.optimumcode.json.schema.internal.formats.RelativeJsonPointerFormatValidator
2525
import io.github.optimumcode.json.schema.internal.formats.TimeFormatValidator
2626
import io.github.optimumcode.json.schema.internal.formats.UriFormatValidator
27+
import io.github.optimumcode.json.schema.internal.formats.UriReferenceFormatValidator
2728
import io.github.optimumcode.json.schema.internal.formats.UuidFormatValidator
2829
import kotlinx.serialization.json.JsonElement
2930
import kotlinx.serialization.json.JsonPrimitive
@@ -74,6 +75,7 @@ internal sealed class FormatAssertionFactory(
7475
"hostname" to HostnameFormatValidator,
7576
"idn-hostname" to IdnHostnameFormatValidator,
7677
"uri" to UriFormatValidator,
78+
"uri-reference" to UriReferenceFormatValidator,
7779
)
7880
}
7981
}

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/formats/UriFormatValidator.kt

Lines changed: 11 additions & 266 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ package io.github.optimumcode.json.schema.internal.formats
22

33
import io.github.optimumcode.json.schema.FormatValidationResult
44
import io.github.optimumcode.json.schema.FormatValidator
5-
6-
private const val SCHEMA_DELIMITER = ':'
7-
private const val QUERY_DELIMITER = '?'
8-
private const val FRAGMENT_DELIMITER = '#'
5+
import io.github.optimumcode.json.schema.internal.formats.UriSpec.FRAGMENT_DELIMITER
6+
import io.github.optimumcode.json.schema.internal.formats.UriSpec.QUERY_DELIMITER
7+
import io.github.optimumcode.json.schema.internal.formats.UriSpec.SCHEMA_DELIMITER
98

109
internal object UriFormatValidator : AbstractStringFormatValidator() {
1110
@Suppress("detekt:ReturnCount")
@@ -20,16 +19,15 @@ internal object UriFormatValidator : AbstractStringFormatValidator() {
2019
}
2120

2221
val schema = value.substring(0, schemaEndIndex)
23-
if (!isValidSchema(schema)) {
22+
if (!UriSpec.isValidSchema(schema)) {
2423
return FormatValidator.Invalid()
2524
}
2625

27-
val queryDelimiterIndex = value.indexOf(QUERY_DELIMITER)
2826
val fragmentDelimiterIndex = value.indexOf(FRAGMENT_DELIMITER)
29-
if (queryDelimiterIndex > 0 && fragmentDelimiterIndex > 0 && queryDelimiterIndex > fragmentDelimiterIndex) {
30-
// fragment is before query
31-
return FormatValidator.Invalid()
32-
}
27+
val queryDelimiterIndex =
28+
value.indexOf(QUERY_DELIMITER)
29+
.takeUnless { fragmentDelimiterIndex in 0..<it }
30+
?: -1
3331
val hierPart =
3432
when {
3533
queryDelimiterIndex > 0 ->
@@ -39,7 +37,7 @@ internal object UriFormatValidator : AbstractStringFormatValidator() {
3937
else ->
4038
value.substring(schemaEndIndex + 1)
4139
}
42-
if (!isValidHierPart(hierPart)) {
40+
if (!UriSpec.isValidHierPart(hierPart)) {
4341
return FormatValidator.Invalid()
4442
}
4543

@@ -50,271 +48,18 @@ internal object UriFormatValidator : AbstractStringFormatValidator() {
5048
} else {
5149
value.substring(queryDelimiterIndex + 1)
5250
}
53-
if (!isValidQuery(query)) {
51+
if (!UriSpec.isValidQuery(query)) {
5452
return FormatValidator.Invalid()
5553
}
5654
}
5755

5856
if (fragmentDelimiterIndex > 0 && fragmentDelimiterIndex < value.lastIndex) {
5957
val fragment = value.substring(fragmentDelimiterIndex + 1)
60-
if (!isValidFragment(fragment)) {
58+
if (!UriSpec.isValidFragment(fragment)) {
6159
return FormatValidator.Invalid()
6260
}
6361
}
6462

6563
return FormatValidator.Valid()
6664
}
67-
68-
private fun isValidFragment(fragment: String): Boolean = isValidFragmentOrQuery(fragment)
69-
70-
private fun isValidQuery(query: String): Boolean = isValidFragmentOrQuery(query)
71-
72-
private fun isValidHierPart(hierPart: String): Boolean {
73-
if (hierPart.isEmpty()) {
74-
return true
75-
}
76-
return when {
77-
hierPart.startsWith("//") ->
78-
isValidAuthorityWithPath(hierPart.substring(2))
79-
hierPart.startsWith("/") ->
80-
isValidAbsolutePath(hierPart.substring(1))
81-
else ->
82-
isValidRootlessPath(hierPart)
83-
}
84-
}
85-
86-
private fun isValidRootlessPath(rootlessPath: String): Boolean {
87-
if (rootlessPath.isEmpty()) {
88-
return false
89-
}
90-
91-
return isValidSegments(rootlessPath)
92-
}
93-
94-
private fun isValidAbsolutePath(absolutePath: String): Boolean {
95-
if (absolutePath.isEmpty()) {
96-
return true
97-
}
98-
99-
return isValidSegments(absolutePath)
100-
}
101-
102-
private fun isValidSegments(segments: String): Boolean {
103-
var lastSep = -1
104-
for ((index, value) in segments.withIndex()) {
105-
if (value == '/') {
106-
if (!hasOnlyPChars(segments.substring(lastSep + 1, index))) {
107-
return false
108-
}
109-
lastSep = index
110-
}
111-
}
112-
113-
return hasOnlyPChars(segments.substring(lastSep + 1))
114-
}
115-
116-
@Suppress("detekt:ReturnCount")
117-
private fun isValidAuthorityWithPath(authorityWithPath: String): Boolean {
118-
if (authorityWithPath.isEmpty()) {
119-
return false
120-
}
121-
val userInfoSeparatorIndex = authorityWithPath.indexOf('@')
122-
if (userInfoSeparatorIndex >= 0) {
123-
if (!isValidUserInfo(authorityWithPath.substring(0, userInfoSeparatorIndex))) {
124-
return false
125-
}
126-
}
127-
val ipV6EndIndex = authorityWithPath.lastIndexOf(']')
128-
val portSeparatorIndex =
129-
authorityWithPath.indexOf(
130-
':',
131-
startIndex =
132-
when {
133-
ipV6EndIndex > 0 -> ipV6EndIndex
134-
userInfoSeparatorIndex > 0 -> userInfoSeparatorIndex
135-
else -> 0
136-
},
137-
)
138-
139-
val segmentSeparatorIndex = authorityWithPath.indexOf('/')
140-
val hostEndIndex =
141-
when {
142-
portSeparatorIndex > 0 -> portSeparatorIndex
143-
segmentSeparatorIndex > 0 -> segmentSeparatorIndex
144-
else -> authorityWithPath.length
145-
}
146-
val hostStartIndex =
147-
if (userInfoSeparatorIndex >= 0) {
148-
userInfoSeparatorIndex + 1
149-
} else {
150-
0
151-
}
152-
val host = authorityWithPath.substring(hostStartIndex, hostEndIndex)
153-
if (!isValidHost(host)) {
154-
return false
155-
}
156-
if (portSeparatorIndex > 0 && portSeparatorIndex < authorityWithPath.lastIndex) {
157-
val portEndIndex =
158-
if (segmentSeparatorIndex > 0) {
159-
segmentSeparatorIndex
160-
} else {
161-
authorityWithPath.length
162-
}
163-
// empty port part
164-
return isValidPort(authorityWithPath.substring(portSeparatorIndex + 1, portEndIndex))
165-
}
166-
return segmentSeparatorIndex < 0 || isValidSegments(authorityWithPath.substring(segmentSeparatorIndex))
167-
}
168-
169-
private fun isValidPort(port: String): Boolean {
170-
if (port.isEmpty()) {
171-
return true
172-
}
173-
174-
for (ch in port) {
175-
if (!isDigit(ch)) {
176-
return false
177-
}
178-
}
179-
180-
return true
181-
}
182-
183-
private fun isValidHost(host: String): Boolean {
184-
if (host.isEmpty()) {
185-
return false
186-
}
187-
if (IpV4FormatValidator.validate(host).isValid()) {
188-
return true
189-
}
190-
if (host.startsWith('[') && host.endsWith(']')) {
191-
val substr = host.substring(1, host.lastIndex)
192-
return IpV6FormatValidator.validate(substr).isValid() || isValidIPvFuture(substr)
193-
}
194-
return isRegName(host)
195-
}
196-
197-
@Suppress("detekt:ReturnCount")
198-
private fun isValidIPvFuture(ipVFuture: String): Boolean {
199-
if (ipVFuture.isEmpty()) {
200-
return false
201-
}
202-
if (ipVFuture[0] != 'v') {
203-
return false
204-
}
205-
val dotIndex = ipVFuture.indexOf('.')
206-
if (dotIndex < 0) {
207-
return false
208-
}
209-
val firstPart = ipVFuture.substring(1, dotIndex)
210-
val secondPart = ipVFuture.substring(dotIndex + 1)
211-
if (firstPart.isEmpty() || secondPart.isEmpty()) {
212-
return false
213-
}
214-
for (ch in firstPart) {
215-
if (isHexDigit(ch)) {
216-
continue
217-
}
218-
return false
219-
}
220-
for (ch in secondPart) {
221-
if (isUnreserved(ch) || isSubDelimiter(ch) || ch == ':') {
222-
continue
223-
}
224-
return false
225-
}
226-
return true
227-
}
228-
229-
private fun isRegName(host: String): Boolean =
230-
hasValidCharsOrPctEncoded(host) {
231-
isSubDelimiter(it) || isUnreserved(it)
232-
}
233-
234-
private fun isValidUserInfo(userInfo: String): Boolean =
235-
hasValidCharsOrPctEncoded(userInfo) {
236-
it == ':' || isSubDelimiter(it) || isUnreserved(it)
237-
}
238-
239-
private fun isValidSchema(schema: String): Boolean {
240-
if (schema.isEmpty()) {
241-
return false
242-
}
243-
244-
if (!isAlpha(schema[0])) {
245-
return false
246-
}
247-
248-
for (i in 1..schema.lastIndex) {
249-
val char = schema[i]
250-
@Suppress("detekt:ComplexCondition")
251-
if (isAlpha(char) || isDigit(char) || char == '+' || char == '-' || char == '.') {
252-
continue
253-
}
254-
return false
255-
}
256-
257-
return true
258-
}
259-
260-
private fun isValidFragmentOrQuery(part: String): Boolean {
261-
if (part.isEmpty()) {
262-
return true
263-
}
264-
265-
return hasValidCharsOrPctEncoded(part) {
266-
it == '/' || it == '?' || isPChar(it)
267-
}
268-
}
269-
270-
private fun hasOnlyPChars(part: String): Boolean = hasValidCharsOrPctEncoded(part, ::isPChar)
271-
272-
private inline fun hasValidCharsOrPctEncoded(
273-
part: String,
274-
isValidChar: (Char) -> Boolean,
275-
): Boolean {
276-
var i = 0
277-
var valid = true
278-
while (i < part.length) {
279-
val char = part[i]
280-
if (char != '%' && !isValidChar(char)) {
281-
valid = false
282-
break
283-
}
284-
if (char == '%') {
285-
if (!isPctEncoded(i, part)) {
286-
valid = false
287-
break
288-
}
289-
i += 2
290-
}
291-
i += 1
292-
}
293-
294-
return valid
295-
}
296-
297-
private fun isPctEncoded(
298-
index: Int,
299-
str: String,
300-
): Boolean {
301-
if (index + 2 >= str.length) {
302-
return false
303-
}
304-
return str[index] == '%' && isHexDigit(str[index + 1]) && isHexDigit(str[index + 2])
305-
}
306-
307-
private fun isAlpha(c: Char): Boolean = c in 'a'..'z' || c in 'A'..'Z'
308-
309-
private fun isDigit(c: Char): Boolean = c in '0'..'9'
310-
311-
private fun isPChar(c: Char): Boolean = isUnreserved(c) || isSubDelimiter(c) || c == ':' || c == '@'
312-
313-
private fun isUnreserved(c: Char): Boolean = isAlpha(c) || isDigit(c) || c == '_' || c == '-' || c == '.' || c == '~'
314-
315-
private fun isSubDelimiter(c: Char): Boolean =
316-
c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' ||
317-
c == '*' || c == '+' || c == ',' || c == ';' || c == '='
318-
319-
private fun isHexDigit(c: Char): Boolean = c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F'
32065
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.github.optimumcode.json.schema.internal.formats
2+
3+
import io.github.optimumcode.json.schema.FormatValidationResult
4+
import io.github.optimumcode.json.schema.FormatValidator
5+
import io.github.optimumcode.json.schema.internal.formats.UriSpec.FRAGMENT_DELIMITER
6+
import io.github.optimumcode.json.schema.internal.formats.UriSpec.QUERY_DELIMITER
7+
8+
internal object UriReferenceFormatValidator : AbstractStringFormatValidator() {
9+
@Suppress("detekt:ReturnCount")
10+
override fun validate(value: String): FormatValidationResult {
11+
if (UriFormatValidator.validate(value).isValid()) {
12+
return FormatValidator.Valid()
13+
}
14+
15+
val fragmentDelimiterIndex = value.indexOf(FRAGMENT_DELIMITER)
16+
val queryDelimiterIndex =
17+
value.indexOf(QUERY_DELIMITER)
18+
.takeUnless { fragmentDelimiterIndex in 0..<it }
19+
?: -1
20+
val relativePart =
21+
when {
22+
queryDelimiterIndex >= 0 ->
23+
value.substring(0, queryDelimiterIndex)
24+
fragmentDelimiterIndex >= 0 ->
25+
value.substring(0, fragmentDelimiterIndex)
26+
else -> value
27+
}
28+
if (!UriSpec.isValidRelativePart(relativePart)) {
29+
return FormatValidator.Invalid()
30+
}
31+
32+
if (queryDelimiterIndex >= 0 && queryDelimiterIndex < value.lastIndex) {
33+
val query =
34+
if (fragmentDelimiterIndex > 0) {
35+
value.substring(queryDelimiterIndex + 1, fragmentDelimiterIndex)
36+
} else {
37+
value.substring(queryDelimiterIndex + 1)
38+
}
39+
if (!UriSpec.isValidQuery(query)) {
40+
return FormatValidator.Invalid()
41+
}
42+
}
43+
44+
if (fragmentDelimiterIndex >= 0 && fragmentDelimiterIndex < value.lastIndex) {
45+
val fragment = value.substring(fragmentDelimiterIndex + 1)
46+
if (!UriSpec.isValidFragment(fragment)) {
47+
return FormatValidator.Invalid()
48+
}
49+
}
50+
51+
return FormatValidator.Valid()
52+
}
53+
}

0 commit comments

Comments
 (0)