Migrated to bld

This commit is contained in:
Geert Bevin 2023-04-04 19:49:20 -04:00
parent baa1784ca1
commit 6f8fc8df36
30 changed files with 349 additions and 573 deletions

BIN
lib/bld/bld-wrapper.jar Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
bld.downloadExtensionJavadoc=false
bld.downloadExtensionSources=true
bld.extensions=com.uwyn.rife2:bld-tests-badge:0.9.3
bld.repositories=MAVEN_CENTRAL,RIFE2
rife2.downloadLocation=
rife2.version=1.5.17

View file

@ -1,160 +0,0 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
import java.net.*
import java.net.http.*
plugins {
application
`java-library`
`maven-publish`
signing
jacoco
id("org.sonarqube") version "4.0.0.2929"
}
group = "com.uwyn"
version = "1.3.1-SNAPSHOT"
val mavenName = "UrlEncoder"
val javaMainClass = "$group.${rootProject.name}.$mavenName"
base {
archivesName.set(rootProject.name)
}
java {
withJavadocJar()
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
application {
mainClass.set(javaMainClass)
}
sonarqube {
properties {
property("sonar.projectName", rootProject.name)
property("sonar.projectKey", "gbevin_${rootProject.name}")
property("sonar.organization", "gbevin")
property("sonar.host.url", "https://sonarcloud.io")
property("sonar.sourceEncoding", "UTF-8")
}
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
}
tasks {
jar {
manifest {
attributes["Main-Class"] = javaMainClass
}
}
test {
useJUnitPlatform()
testLogging {
exceptionFormat = TestExceptionFormat.FULL
events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
addTestListener(object : TestListener {
override fun beforeTest(p0: TestDescriptor?) = Unit
override fun beforeSuite(p0: TestDescriptor?) = Unit
override fun afterTest(desc: TestDescriptor, result: TestResult) = Unit
override fun afterSuite(desc: TestDescriptor, result: TestResult) {
if (desc.parent == null) {
val passed = result.successfulTestCount
val failed = result.failedTestCount
val skipped = result.skippedTestCount
if (project.properties["testsBadgeApiKey"] != null) {
val apiKey = project.properties["testsBadgeApiKey"]
val response: HttpResponse<String> = HttpClient.newHttpClient()
.send(
HttpRequest.newBuilder()
.uri(
URI(
"https://rife2.com/tests-badge/update/com.uwyn/urlencoder?" +
"apiKey=$apiKey&" +
"passed=$passed&" +
"failed=$failed&" +
"skipped=$skipped"
)
)
.POST(HttpRequest.BodyPublishers.noBody())
.build(), HttpResponse.BodyHandlers.ofString()
)
println("RESPONSE: " + response.statusCode())
println(response.body())
}
}
}
})
}
jacocoTestReport {
reports {
xml.required.set(true)
}
}
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
artifactId = rootProject.name
from(components["java"])
pom {
name.set(mavenName)
description.set("A simple defensive library to encode/decode URL components")
url.set("https://github.com/gbevin/urlencoder")
licenses {
license {
name.set("The Apache License, Version 2.0")
url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
}
}
developers {
developer {
id.set("gbevin")
name.set("Geert Bevin")
email.set("gbevin@uwyn.com")
url.set("https://github.com/gbevin")
}
developer {
id.set("ethauvin")
name.set("Erik C. Thauvin")
email.set("erik@thauvin.net")
url.set("https://erik.thauvin.net/")
}
}
scm {
connection.set("scm:git:https://github.com/gbevin/urlencoder.git")
developerConnection.set("scm:git:git@github.com:gbevin/urlencoder.git")
url.set("https://github.com/gbevin/urlencoder")
}
}
repositories {
maven {
credentials {
username = project.properties["ossrhUsername"].toString()
password = project.properties["ossrhPassword"].toString()
}
val releasesRepoUrl = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
val snapshotsRepoUrl = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
}
}
}
}
}
signing {
sign(publishing.publications["mavenJava"])
}

View file

@ -1,339 +0,0 @@
/*
* Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package com.uwyn.urlencoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
/**
* Most defensive approach to URL encoding and decoding.
* <p>
* Rules determined by combining the unreserved character set from
* <a href="https://www.rfc-editor.org/rfc/rfc3986#page-13">RFC 3986</a> with
* the percent-encode set from
* <a href="https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set">application/x-www-form-urlencoded</a>.
* <p>
* Both specs above support percent decoding of two hexadecimal digits to a
* binary octet, however their unreserved set of characters differs and
* {@code application/x-www-form-urlencoded} adds conversion of space to +,
* which has the potential to be misunderstood.
* <p>
* This class encodes with rules that will be decoded correctly in either case.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @author Erik C. Thauvin (erik@thauvin.net)
* @since 1.0
*/
public final class UrlEncoder {
static final BitSet UNRESERVED_URI_CHARS;
private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
static {
// see https://www.rfc-editor.org/rfc/rfc3986#page-13
// and https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
var unreserved = new BitSet('z' + 1);
unreserved.set('-');
unreserved.set('.');
for (int c = '0'; c <= '9'; ++c) unreserved.set(c);
for (int c = 'A'; c <= 'Z'; ++c) unreserved.set(c);
unreserved.set('_');
for (int c = 'a'; c <= 'z'; ++c) unreserved.set(c);
UNRESERVED_URI_CHARS = unreserved;
}
private UrlEncoder() {
// no-op
}
private static void appendUrlEncodedByte(StringBuilder out, int ch) {
out.append("%");
appendUrlEncodedDigit(out, ch >> 4);
appendUrlEncodedDigit(out, ch);
}
private static void appendUrlEncodedDigit(StringBuilder out, int digit) {
out.append(HEX_DIGITS[digit & 0x0F]);
}
/**
* Transforms a provided <code>String</code> URL into a new string,
* containing decoded URL characters in the UTF-8 encoding.
*
* @param source The string URL that has to be decoded
* @return The decoded <code>String</code> object.
* @see #encode(String, String)
* @since 1.0
*/
public static String decode(String source) {
return decode(source, false);
}
/**
* Transforms a provided <code>String</code> URL into a new string,
* containing decoded URL characters in the UTF-8 encoding.
*
* @param source The string URL that has to be decoded
* @param plusToSpace Convert any {@code +} to space.
* @return The decoded <code>String</code> object.
* @see #encode(String, String)
* @since 1.0
*/
public static String decode(String source, boolean plusToSpace) {
if (source == null || source.isEmpty()) {
return source;
}
var length = source.length();
StringBuilder out = null;
char ch;
byte[] bytes_buffer = null;
var bytes_pos = 0;
var i = 0;
while (i < length) {
ch = source.charAt(i);
if (ch == '%') {
out = startConstructingIfNeeded(out, source, i);
if (bytes_buffer == null) {
// the remaining characters divided by the length
// of the encoding format %xx, is the maximum number of
// bytes that can be extracted
bytes_buffer = new byte[(length - i) / 3];
}
i += 1;
if (length < i + 2) {
throw new IllegalArgumentException("Illegal escape sequence");
}
try {
var v = Integer.parseInt(source, i, i + 2, 16);
if (v < 0 || v > 0xFF) {
throw new IllegalArgumentException("Illegal escape value");
}
bytes_buffer[bytes_pos++] = (byte) v;
i += 2;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Illegal characters in escape sequence: " + e.getMessage(), e);
}
} else {
if (bytes_buffer != null) {
out.append(new String(bytes_buffer, 0, bytes_pos, StandardCharsets.UTF_8));
bytes_buffer = null;
bytes_pos = 0;
}
if (plusToSpace && ch == '+') {
out = startConstructingIfNeeded(out, source, i);
out.append(" ");
} else if (out != null) {
out.append(ch);
}
i += 1;
}
}
if (out == null) {
return source;
}
if (bytes_buffer != null) {
out.append(new String(bytes_buffer, 0, bytes_pos, StandardCharsets.UTF_8));
}
return out.toString();
}
private static StringBuilder startConstructingIfNeeded(StringBuilder out, String source, int currentSourcePosition) {
if (out == null) {
out = new StringBuilder(source.length());
out.append(source, 0, currentSourcePosition);
}
return out;
}
/**
* Transforms a provided <code>String</code> object into a new string,
* containing only valid URL characters in the UTF-8 encoding.
*
* @param source The string that has to be transformed into a valid URL
* string.
* @return The encoded <code>String</code> object.
* @see #decode(String)
* @since 1.0
*/
public static String encode(String source) {
return encode(source, null, false);
}
/**
* Transforms a provided <code>String</code> object into a new string,
* containing only valid URL characters in the UTF-8 encoding.
*
* @param source The string that has to be transformed into a valid URL
* string.
* @param allow Additional characters to allow.
* @return The encoded <code>String</code> object.
* @see #decode(String)
* @since 1.0
*/
public static String encode(String source, String allow) {
return encode(source, allow, false);
}
/**
* Transforms a provided <code>String</code> object into a new string,
* containing only valid URL characters in the UTF-8 encoding.
*
* @param source The string that has to be transformed into a valid URL
* string.
* @param spaceToPlus Convert any space to {@code +}.
* @return The encoded <code>String</code> object.
* @see #decode(String)
* @since 1.0
*/
public static String encode(String source, boolean spaceToPlus) {
return encode(source, null, spaceToPlus);
}
/**
* Transforms a provided <code>String</code> object into a new string,
* containing only valid URL characters in the UTF-8 encoding.
*
* @param source The string that has to be transformed into a valid URL
* string.
* @param allow Additional characters to allow.
* @param spaceToPlus Convert any space to {@code +}.
* @return The encoded <code>String</code> object.
* @see #decode(String)
* @since 1.0
*/
public static String encode(String source, String allow, boolean spaceToPlus) {
if (source == null || source.isEmpty()) {
return source;
}
StringBuilder out = null;
char ch;
var i = 0;
while (i < source.length()) {
ch = source.charAt(i);
if (isUnreservedUriChar(ch) || (allow != null && allow.indexOf(ch) != -1)) {
if (out != null) {
out.append(ch);
}
i += 1;
} else {
out = startConstructingIfNeeded(out, source, i);
var cp = source.codePointAt(i);
if (cp < 0x80) {
if (spaceToPlus && ch == ' ') {
out.append('+');
} else {
appendUrlEncodedByte(out, cp);
}
i += 1;
} else if (Character.isBmpCodePoint(cp)) {
for (var b : Character.toString(ch).getBytes(StandardCharsets.UTF_8)) {
appendUrlEncodedByte(out, b);
}
i += 1;
} else if (Character.isSupplementaryCodePoint(cp)) {
var high = Character.highSurrogate(cp);
var low = Character.lowSurrogate(cp);
for (var b : new String(new char[]{high, low}).getBytes(StandardCharsets.UTF_8)) {
appendUrlEncodedByte(out, b);
}
i += 2;
}
}
}
if (out == null) {
return source;
}
return out.toString();
}
// see https://www.rfc-editor.org/rfc/rfc3986#page-13
// and https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
private static boolean isUnreservedUriChar(char ch) {
return ch <= 'z' && UNRESERVED_URI_CHARS.get(ch);
}
/**
* Main method to encode/decode URLs on the command line
*
* @param arguments the command line arguments
* @since 1.1
*/
public static void main(String[] arguments) {
try {
var result = processMain(arguments);
if (result.status == 0) {
System.out.println(result.output);
} else {
System.err.println(result.output);
}
System.exit(result.status);
} catch (IllegalArgumentException e) {
System.err.println(UrlEncoder.class.getSimpleName() + ": " + e.getMessage());
System.exit(1);
}
}
static MainResult processMain(String... arguments) {
var valid_arguments = false;
var perform_decode = false;
var args = new ArrayList<>(List.of(arguments));
if (!args.isEmpty() && args.get(0).startsWith("-")) {
var option = args.remove(0);
if (("-d").equals(option)) {
perform_decode = true;
valid_arguments = (args.size() == 1);
} else if (("-e").equals(option)) {
valid_arguments = (args.size() == 1);
} else {
args.clear();
}
}
var text = "";
if (args.size() == 1 && !args.get(0).isEmpty()) {
text = args.remove(0);
valid_arguments = true;
}
if (!valid_arguments) {
return new MainResult("Usage : java -jar urlencoder-*.jar [-ed] text" + System.lineSeparator() +
"Encode and decode URL components defensively." + System.lineSeparator() +
" -e encode (default)" + System.lineSeparator() +
" -d decode", 1);
}
if (perform_decode) {
return new MainResult(UrlEncoder.decode(text), 0);
} else {
return new MainResult(UrlEncoder.encode(text), 0);
}
}
static class MainResult {
final String output;
final int status;
public MainResult(String output, int status) {
this.output = output;
this.status = status;
}
}
}

View file

@ -1,169 +0,0 @@
/*
* Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package com.uwyn.urlencoder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.params.provider.Arguments.arguments;
class UrlEncoderTest {
private final String same = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.";
private static Stream<String> invalid() {
return Stream.of("sdkjfh%", "sdkjfh%6", "sdkjfh%xx", "sdfjfh%-1");
}
private static Stream<Arguments> validMap() {
return Stream.of(
arguments("a test &", "a%20test%20%26"),
arguments(
"!abcdefghijklmnopqrstuvwxyz%%ABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.~=",
"%21abcdefghijklmnopqrstuvwxyz%25%25ABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.%7E%3D"
),
arguments("%#okékÉȢ smile!😁", "%25%23ok%C3%A9k%C3%89%C8%A2%20smile%21%F0%9F%98%81"),
arguments(
"\uD808\uDC00\uD809\uDD00\uD808\uDF00\uD808\uDD00",
"%F0%92%80%80%F0%92%94%80%F0%92%8C%80%F0%92%84%80"
)
);
}
@Test
void testDecodeNotNeeded() {
assertSame(same, UrlEncoder.decode(same));
assertEquals("", UrlEncoder.decode(""), "decode('')");
assertEquals(" ", UrlEncoder.decode(" "), "decode(' ')");
}
@ParameterizedTest(name = "decode({0}) should be {1}")
@MethodSource("validMap")
void testDecodeUrl(String expected, String source) {
assertEquals(expected, UrlEncoder.decode(source));
}
@ParameterizedTest(name = "decode({0})")
@MethodSource("invalid")
void testDecodeWithException(String source) {
assertThrows(IllegalArgumentException.class, () -> UrlEncoder.decode(source), "decode(" + source + ")");
}
@Test
void testDecodeWithNull() {
assertNull(UrlEncoder.decode(null), "decode(null)");
}
@ParameterizedTest(name = "encode({0}) should be {1}")
@MethodSource("validMap")
void testEncodeUrl(String source, String expected) {
assertEquals(expected, UrlEncoder.encode(source));
}
@Test
void testEncodeWhenNoneNeeded() {
assertSame(same, UrlEncoder.encode(same));
assertSame(same, UrlEncoder.encode(same, ""), "with empty allow");
}
@Test
void testEncodeWithAllowArg() {
assertEquals("?test=a%20test", UrlEncoder.encode("?test=a test", "=?"), "encode(x, =?)");
assertEquals("aaa", UrlEncoder.encode("aaa", "a"), "encode(aaa, a)");
assertEquals(" ", UrlEncoder.encode(" ", " "), "encode(' ', ' ')");
}
@Test
void testEncodeWithEmptyOrBlank() {
assertTrue(UrlEncoder.encode("", "").isEmpty(), "encode('','')");
assertEquals("", UrlEncoder.encode(""), "encode('')");
assertEquals("%20", UrlEncoder.encode(" "), "encode('')");
}
@Test
void testEncodeWithNulls() {
assertNull(UrlEncoder.encode(null), "encode(null)");
assertNull(UrlEncoder.encode(null, null), "encode(null, null)");
assertEquals("foo", UrlEncoder.encode("foo", null), "encode(foo, null");
}
@Test
void testEncodeSpaceToPlus() {
assertEquals("foo+bar", UrlEncoder.encode("foo bar", true));
assertEquals("foo+bar++foo", UrlEncoder.encode("foo bar foo", true));
assertEquals("foo bar", UrlEncoder.encode("foo bar", " ", true));
}
@Test
void testDecodePlusToSpace() {
assertEquals("foo bar", UrlEncoder.decode("foo+bar", true));
assertEquals("foo bar foo", UrlEncoder.decode("foo+bar++foo", true));
assertEquals("foo bar foo", UrlEncoder.decode("foo+%20bar%20+foo", true));
}
@ParameterizedTest(name = "processMain(-d {1}) should be {0}")
@MethodSource("validMap")
void testMainDecode(String expected, String source) {
var result = UrlEncoder.processMain("-d", source);
assertEquals(expected, result.output);
assertEquals(0, result.status, "processMain(-d " + source + ").status");
}
@ParameterizedTest(name = "processMain(-e {0})")
@MethodSource("validMap")
void testMainEncode(String source, String expected) {
var result = UrlEncoder.processMain(source);
assertEquals(expected, result.output);
assertEquals(0, result.status, "processMain(-e " + source + ").status");
}
@ParameterizedTest(name = "processMain(-d {0})")
@MethodSource("invalid")
void testMainEncodeWithExceptions(String source) {
assertThrows(IllegalArgumentException.class, () -> UrlEncoder.processMain("-d", source), source);
}
@Test
void testMainTooManyArgs() {
assertTrue(UrlEncoder.processMain("foo", "bar", "test").output.contains("Usage :"), "too many args");
}
@Test
void testMainWithEmptyArgs() {
assertTrue(UrlEncoder.processMain(" ", " ").output.contains("Usage :"), "processMain(' ', ' ')");
assertTrue(UrlEncoder.processMain("foo", " ").output.contains("Usage :"), "processMain('foo', ' ')");
assertTrue(UrlEncoder.processMain(" ", "foo").output.contains("Usage :"), "processMain(' ', 'foo')");
assertTrue(UrlEncoder.processMain("-d ", "").output.contains("Usage :"), "processMain('-d', '')");
assertEquals("%20", UrlEncoder.processMain(new String[]{"-e", " "}).output, "processMain('-e', ' ')");
assertEquals(" ", UrlEncoder.processMain(new String[]{"-d", " "}).output, "processMain('-d', ' ')");
}
@ParameterizedTest
@ValueSource(strings = {"", "-d", "-e"})
void testMainWithInvalidArgs(String arg) {
var result = UrlEncoder.processMain(arg);
assertTrue(result.output.contains("Usage :"), "processMain('" + arg + "')");
assertEquals(1, result.status, "processMain('" + arg + "').status");
}
@ParameterizedTest(name = "processMain(-e {0})")
@MethodSource("validMap")
void testMainWithOption(String source, String expected) {
var result = UrlEncoder.processMain("-e", source);
assertEquals(expected, result.output);
assertEquals(0, result.status, "processMain(-e " + source + ").status");
}
@Test
void testMainWithUnknownOptions() {
assertTrue(UrlEncoder.processMain("-p").output.contains("Usage :"), "processMain(-p)");
assertTrue(UrlEncoder.processMain("-").output.contains("Usage :"), "processMain(-)");
}
}