Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes uri, uri-reference, iri, iri-reference formats and does iri to uri conversion #983

Merged
merged 3 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/main/java/com/networknt/schema/format/IriFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ protected boolean validate(URI uri) {
return false;
}
}

String query = uri.getQuery();
if (query != null) {
// [ and ] must be percent encoded
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
return false;
}
}
}
return result;
}
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/networknt/schema/format/IriReferenceFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
public class IriReferenceFormat extends AbstractRFC3986Format {
@Override
protected boolean validate(URI uri) {
String authority = uri.getAuthority();
if (authority != null) {
if (IPv6Format.PATTERN.matcher(authority).matches() ) {
return false;
}
}
String query = uri.getQuery();
if (query != null) {
// [ and ] must be percent encoded
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
return false;
}
}
return true;
}

Expand Down
16 changes: 15 additions & 1 deletion src/main/java/com/networknt/schema/format/UriFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,21 @@
public class UriFormat extends AbstractRFC3986Format {
@Override
protected boolean validate(URI uri) {
return uri.isAbsolute();
boolean result = uri.isAbsolute();
if (result) {
// Java URI accepts non ASCII characters and this is not a valid in RFC3986
result = uri.toString().codePoints().allMatch(ch -> ch < 0x7F);
if (result) {
String query = uri.getQuery();
if (query != null) {
// [ and ] must be percent encoded
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
return false;
}
}
}
}
return result;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@
public class UriReferenceFormat extends AbstractRFC3986Format {
@Override
protected boolean validate(URI uri) {
return true;
// Java URI accepts non ASCII characters and this is not a valid in RFC3986
boolean result = uri.toString().codePoints().allMatch(ch -> ch < 0x7F);
if (result) {
String query = uri.getQuery();
if (query != null) {
// [ and ] must be percent encoded
if (query.indexOf('[') != -1 || query.indexOf(']') != -1) {
return false;
}
}
}
return result;
}

@Override
Expand Down
36 changes: 34 additions & 2 deletions src/main/java/com/networknt/schema/resource/UriSchemaLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,57 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;

import com.networknt.schema.AbsoluteIri;
import com.networknt.schema.utils.AbsoluteIris;

/**
* Loads from uri.
*/
public class UriSchemaLoader implements SchemaLoader {
@Override
public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
URI uri = URI.create(absoluteIri.toString());
URI uri = toURI(absoluteIri);
URL url = toURL(uri);
return () -> {
URLConnection conn = uri.toURL().openConnection();
URLConnection conn = url.openConnection();
return this.openConnectionCheckRedirects(conn);
};
}

/**
* Converts an AbsoluteIRI to a URI.
* <p>
* Internationalized domain names will be converted using java.net.IDN.toASCII.
*
* @param absoluteIri the absolute IRI
* @return the URI
*/
protected URI toURI(AbsoluteIri absoluteIri) {
return URI.create(AbsoluteIris.toUri(absoluteIri));
}

/**
* Converts a URI to a URL.
* <p>
* This will throw if the URI is not a valid URL. For instance if the URI is not
* absolute.
*
* @param uri the URL
* @return the URL
*/
protected URL toURL(URI uri) {
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}

// https://www.cs.mun.ca/java-api-1.5/guide/deployment/deployment-guide/upgrade-guide/article-17.html
protected InputStream openConnectionCheckRedirects(URLConnection c) throws IOException {
boolean redir;
Expand Down
172 changes: 172 additions & 0 deletions src/main/java/com/networknt/schema/utils/AbsoluteIris.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright (c) 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.networknt.schema.utils;

import java.io.UnsupportedEncodingException;
import java.net.IDN;
import java.net.URI;
import java.net.URLEncoder;

import com.networknt.schema.AbsoluteIri;

/**
* Utility functions for AbsoluteIri.
*/
public class AbsoluteIris {
/**
* Converts an IRI to a URI.
*
* @param iri the IRI to convert
* @return the URI string
*/
public static String toUri(AbsoluteIri iri) {
String iriString = iri.toString();
boolean ascii = isAscii(iriString);
if (ascii) {
int index = iriString.indexOf('?');
if (index == -1) {
return iriString;
}
String rest = iriString.substring(0, index + 1);
String query = iriString.substring(index + 1);
StringBuilder result = new StringBuilder(rest);
handleQuery(result, query);
return result.toString();
}
String[] parts = iriString.split(":"); // scheme + rest
if (parts.length == 2) {
StringBuilder result = new StringBuilder(parts[0]);
result.append(":");

String rest = parts[1];
if (rest.startsWith("//")) {
rest = rest.substring(2);
result.append("//");
} else if (rest.startsWith("/")) {
rest = rest.substring(1);
result.append("/");
}
String[] query = rest.split("\\?"); // rest ? query
String[] restParts = query[0].split("/");
for (int x = 0; x < restParts.length; x++) {
String p = restParts[x];
if (x == 0) {
// Domain
if (isAscii(p)) {
result.append(p);
} else {
result.append(unicodeToASCII(p));
}
} else {
result.append(p);
}
if (x != restParts.length - 1) {
result.append("/");
}
}
if (query[0].endsWith("/")) {
result.append("/");
}
if (query.length == 2) {
// handle query string
result.append("?");
handleQuery(result, query[1]);
}

return URI.create(result.toString()).toASCIIString();
}
return iriString;
}

/**
* Determine if a string is US ASCII.
*
* @param value to test
* @return true if ASCII
*/
static boolean isAscii(String value) {
return value.codePoints().allMatch(ch -> ch < 0x7F);
}

/**
* Ensures that the query parameters are properly URL encoded.
*
* @param result the string builder to add to
* @param query the query string
*/
static void handleQuery(StringBuilder result, String query) {
String[] queryParts = query.split("&");
for (int y = 0; y < queryParts.length; y++) {
String queryPart = queryParts[y];

String[] nameValue = queryPart.split("=");
try {
result.append(URLEncoder.encode(nameValue[0], "UTF-8"));
if (nameValue.length == 2) {
result.append("=");
result.append(URLEncoder.encode(nameValue[1], "UTF-8"));
}
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
if (y != queryParts.length - 1) {
result.append("&");
}
}
}

// The following routines are from apache commons validator routines
// DomainValidator
static String unicodeToASCII(final String input) {
try {
final String ascii = IDN.toASCII(input);
if (IDNBUGHOLDER.IDN_TOASCII_PRESERVES_TRAILING_DOTS) {
return ascii;
}
final int length = input.length();
if (length == 0) { // check there is a last character
return input;
}
// RFC3490 3.1. 1)
// Whenever dots are used as label separators, the following
// characters MUST be recognized as dots: U+002E (full stop), U+3002
// (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61
// (halfwidth ideographic full stop).
final char lastChar = input.charAt(length - 1);// fetch original last char
switch (lastChar) {
case '\u002E': // "." full stop
case '\u3002': // ideographic full stop
case '\uFF0E': // fullwidth full stop
case '\uFF61': // halfwidth ideographic full stop
return ascii + "."; // restore the missing stop
default:
return ascii;
}
} catch (final IllegalArgumentException e) { // input is not valid
return input;
}
}

private static class IDNBUGHOLDER {
private static final boolean IDN_TOASCII_PRESERVES_TRAILING_DOTS = keepsTrailingDot();

private static boolean keepsTrailingDot() {
final String input = "a."; // must be a valid name
return input.equals(IDN.toASCII(input));
}
}

}
74 changes: 74 additions & 0 deletions src/test/java/com/networknt/schema/format/IriFormatTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.networknt.schema.format;

import static org.junit.jupiter.api.Assertions.*;

import java.util.Set;

import org.junit.jupiter.api.Test;

import com.networknt.schema.InputFormat;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SchemaValidatorsConfig;
import com.networknt.schema.SpecVersion.VersionFlag;
import com.networknt.schema.ValidationMessage;

class IriFormatTest {
@Test
void uriShouldPass() {
String schemaData = "{\r\n"
+ " \"format\": \"iri\"\r\n"
+ "}";

SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setFormatAssertionsEnabled(true);
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
Set<ValidationMessage> messages = schema.validate("\"https://test.com/assets/product.pdf\"",
InputFormat.JSON);
assertTrue(messages.isEmpty());
}

@Test
void queryWithBracketsShouldFail() {
String schemaData = "{\r\n"
+ " \"format\": \"iri\"\r\n"
+ "}";

SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setFormatAssertionsEnabled(true);
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
Set<ValidationMessage> messages = schema.validate("\"https://test.com/assets/product.pdf?filter[test]=1\"",
InputFormat.JSON);
assertFalse(messages.isEmpty());
}

@Test
void iriShouldPass() {
String schemaData = "{\r\n"
+ " \"format\": \"iri\"\r\n"
+ "}";

SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setFormatAssertionsEnabled(true);
JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
Set<ValidationMessage> messages = schema.validate("\"https://test.com/assets/produktdatenblätter.pdf\"",
InputFormat.JSON);
assertTrue(messages.isEmpty());
}

}
Loading
Loading