diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 87a9233..8a90dc0 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -46,4 +46,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew sonar --info + run: ./gradlew sonar --info -Dsonar.verbose=true diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..53beb10 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,113 @@ +/* + * Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com) + * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * + * 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. + */ + +import org.jetbrains.dokka.gradle.DokkaTask + +plugins { + buildsrc.conventions.lang.`kotlin-jvm` + buildsrc.conventions.publishing + buildsrc.conventions.sonarqube + id("application") + id("com.github.ben-manes.versions") +} + +description = "A simple defensive application to encode/decode URL components" + +val deployDir = project.layout.projectDirectory.dir("deploy") +val urlEncoderMainClass = "net.thauvin.erik.urlencoder.UrlEncoder" + +dependencies { + implementation(projects.lib) + kover(projects.lib) + +// testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25") + testImplementation("org.junit.jupiter:junit-jupiter:5.9.1") +} + +base { + archivesName.set(rootProject.name) +} + +application { + mainClass.set(urlEncoderMainClass) +} + +tasks { + jar { + manifest { + attributes["Main-Class"] = urlEncoderMainClass + } + } + + val fatJar by registering(Jar::class) { + group = LifecycleBasePlugin.BUILD_GROUP + dependsOn.addAll(listOf("compileJava", "compileKotlin", "processResources")) + archiveClassifier.set("all") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + manifest { attributes(mapOf("Main-Class" to application.mainClass)) } + from(sourceSets.main.get().output) + dependsOn(configurations.runtimeClasspath) + from(configurations.runtimeClasspath.map { classpath -> + classpath.incoming.artifacts.artifactFiles.files.filter { it.name.endsWith("jar") }.map { zipTree(it) } + }) + } + + build { + dependsOn(fatJar) + } + + withType().configureEach { + destination = file("$projectDir/pom.xml") + } + + clean { + delete(deployDir) + } + + withType().configureEach { + dokkaSourceSets { + named("main") { + moduleName.set("UrlEncoder Application") + } + } + } + + val copyToDeploy by registering(Sync::class) { + group = PublishingPlugin.PUBLISH_TASK_GROUP + from(configurations.runtimeClasspath) { + exclude("annotations-*.jar") + } + from(jar) + into(deployDir) + } + + register("deploy") { + description = "Copies all needed files to the 'deploy' directory." + group = PublishingPlugin.PUBLISH_TASK_GROUP + dependsOn(build, copyToDeploy) + } +} + +publishing { + publications { + create("mavenJava") { + from(components["java"]) + artifactId = rootProject.name + artifact(tasks.javadocJar) + } + } +} diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml new file mode 100644 index 0000000..fd4e62e --- /dev/null +++ b/app/detekt-baseline.xml @@ -0,0 +1,8 @@ + + + + + ComplexCondition:UrlEncoder.kt$UrlEncoder$hasOption && args.size == 2 || !hasOption && args.size == 1 + MaxLineLength:UrlEncoder.kt$UrlEncoder$* + + diff --git a/app/pom.xml b/app/pom.xml new file mode 100644 index 0000000..e610169 --- /dev/null +++ b/app/pom.xml @@ -0,0 +1,58 @@ + + + + + + + + 4.0.0 + net.thauvin.erik + urlencoder + 1.3.1-SNAPSHOT + UrlEncoder for Kotlin + A simple defensive application to encode/decode URL components + https://github.com/ethauvin/urlencoder + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + gbevin + Geert Bevin + gbevin@uwyn.com + https://github.com/gbevin + + + ethauvin + Erik C. Thauvin + erik@thauvin.net + https://erik.thauvin.net/ + + + + scm:git://github.com/ethauvin/urlencoder.git + scm:git@github.com:ethauvin/urlencoder.git + https://github.com/ethauvin/urlencoder + + + GitHub + https://github.com/ethauvin/urlencoder/issues + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.8.0 + compile + + + net.thauvin.erik + urlencoder-lib + 1.3.1-SNAPSHOT + runtime + + + diff --git a/app/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoder.kt b/app/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoder.kt new file mode 100644 index 0000000..902bc45 --- /dev/null +++ b/app/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoder.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com) + * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * + * 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 net.thauvin.erik.urlencoder + +import kotlin.system.exitProcess + +/** + * Most defensive approach to URL encoding and decoding. + * + * - Rules determined by combining the unreserved character set from + * [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#page-13) with the percent-encode set from + * [application/x-www-form-urlencoded](https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set). + * + * - Both specs above support percent decoding of two hexadecimal digits to a binary octet, however their unreserved + * set of characters differs and `application/x-www-form-urlencoded` adds conversion of space to `+`, which has the + * potential to be misunderstood. + * + * - This library 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) + **/ +object UrlEncoder { + + internal val usage = + "Usage : java -jar urlencoder-*all.jar [-ed] text" + System.lineSeparator() + + "Encode and decode URL components defensively." + System.lineSeparator() + + " -e encode (default) " + System.lineSeparator() + + " -d decode" + + /** + * Encodes and decodes URLs from the command line. + * + * - `java -jar urlencoder-*all.jar [-ed] text` + */ + @JvmStatic + fun main(args: Array) { + try { + val result = processMain(args) + if (result.status == 1) { + System.err.println(result.output) + } else { + println(result.output) + } + exitProcess(result.status) + } catch (e: IllegalArgumentException) { + System.err.println("${UrlEncoder::class.java.simpleName}: ${e.message}") + exitProcess(1) + } + } + + internal data class MainResult(var output: String = usage, var status: Int = 1) + + internal fun processMain(args: Array): MainResult { + val result = MainResult() + if (args.isNotEmpty() && args[0].isNotEmpty()) { + val hasDecode = (args[0] == "-d") + val hasOption = (hasDecode || args[0] == "-e") + if (hasOption && args.size == 2 || !hasOption && args.size == 1) { + val arg = if (hasOption) args[1] else args[0] + if (hasDecode) { + result.output = decode(arg) + } else { + result.output = UrlEncoderUtil.encode(arg) + } + result.status = 0 + } + } + return result + } + + /** + * Transforms a provided [String] into a new string, containing decoded URL characters in the UTF-8 + * encoding. + */ + @JvmStatic + @JvmOverloads + fun decode(source: String, plusToSpace: Boolean = false): String = + // delegate to UrlEncoderFunctions for backwards compatibility + UrlEncoderUtil.decode(source, plusToSpace) + + /** + * Transforms a provided [String] object into a new string, containing only valid URL characters in the UTF-8 + * encoding. + * + * - Letters, numbers, unreserved (`_-!.'()*`) and allowed characters are left intact. + */ + @JvmStatic + @JvmOverloads + fun encode(source: String, allow: String = "", spaceToPlus: Boolean = false): String = + UrlEncoderUtil.encode(source, allow, spaceToPlus) +} diff --git a/lib/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderTest.kt b/app/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderTest.kt similarity index 64% rename from lib/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderTest.kt rename to app/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderTest.kt index 8439010..08e78b7 100644 --- a/lib/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderTest.kt +++ b/app/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderTest.kt @@ -17,14 +17,10 @@ package net.thauvin.erik.urlencoder -import net.thauvin.erik.urlencoder.UrlEncoder.decode -import net.thauvin.erik.urlencoder.UrlEncoder.encode import net.thauvin.erik.urlencoder.UrlEncoder.processMain import net.thauvin.erik.urlencoder.UrlEncoder.usage import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertSame import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -34,7 +30,6 @@ import org.junit.jupiter.params.provider.ValueSource import java.util.stream.Stream class UrlEncoderTest { - private val same = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_." companion object { @JvmStatic @@ -54,67 +49,6 @@ class UrlEncoderTest { ) } - @ParameterizedTest(name = "decode({0}) should be {1}") - @MethodSource("validMap") - fun `Decode URL`(expected: String, source: String) { - assertEquals(expected, decode(source)) - } - - @ParameterizedTest(name = "decode({0})") - @MethodSource("invalid") - fun `Decode with Exception`(source: String) { - assertThrows(IllegalArgumentException::class.java, { decode(source) }, "decode($source)") - } - - @Test - fun `Decode when None needed`() { - assertSame(same, decode(same)) - assertEquals("", decode(""), "decode('')") - assertEquals(" ", decode(" "), "decode(' ')") - } - - @Test - fun `Decode with Plus to Space`() { - assertEquals("foo bar", decode("foo+bar", true)) - assertEquals("foo bar foo", decode("foo+bar++foo", true)) - assertEquals("foo bar foo", decode("foo+%20bar%20+foo", true)) - assertEquals("foo + bar", decode("foo+%2B+bar", plusToSpace = true)) - assertEquals("foo+bar", decode("foo%2Bbar", plusToSpace = true)) - } - - @ParameterizedTest(name = "encode({0}) should be {1}") - @MethodSource("validMap") - fun `Encode URL`(source: String, expected: String) { - assertEquals(expected, encode(source)) - } - - @Test - fun `Encode Empty or Blank`() { - assertTrue(encode("", allow = "").isEmpty(), "encode('','')") - assertEquals("", encode(""), "encode('')") - assertEquals("%20", encode(" "), "encode(' ')") - } - - @Test - fun `Encode when None needed`() { - assertSame(same, encode(same)) - assertSame(same, encode(same, allow = ""), "with empty allow") - } - - @Test - fun `Encode with Allow Arg`() { - assertEquals("?test=a%20test", encode("?test=a test", allow = "=?"), "encode(x, =?)") - assertEquals("aaa", encode("aaa", "a"), "encode(aaa, a)") - assertEquals(" ", encode(" ", " "), "encode(' ', ' ')") - } - - @Test - fun `Encode with Space to Plus`() { - assertEquals("foo+bar", encode("foo bar", spaceToPlus = true)) - assertEquals("foo+bar++foo", encode("foo bar foo", spaceToPlus = true)) - assertEquals("foo bar", encode("foo bar", " ", true)) - } - @ParameterizedTest(name = "processMain(-d {1}) should be {0}") @MethodSource("validMap") fun `Main Decode`(expected: String, source: String) { diff --git a/build.gradle.kts b/build.gradle.kts index 9eae0de..3636913 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,7 @@ plugins { buildsrc.conventions.base + buildsrc.conventions.sonarqube } group = "net.thauvin.erik" diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index e862322..0b4b926 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -9,4 +9,4 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21") implementation("org.jetbrains.kotlinx:kover-gradle-plugin:0.7.0") implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.0.3129") -} \ No newline at end of file +} diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-jvm.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-jvm.gradle.kts index 790e694..eb302e8 100644 --- a/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-jvm.gradle.kts +++ b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-jvm.gradle.kts @@ -1,15 +1,10 @@ package buildsrc.conventions.lang import buildsrc.utils.Rife2TestListener -import org.gradle.api.JavaVersion -import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.sonarqube.gradle.SonarTask /** * Common configuration for Kotlin/JVM projects @@ -20,7 +15,8 @@ import org.sonarqube.gradle.SonarTask plugins { id("buildsrc.conventions.base") kotlin("jvm") - id("buildsrc.conventions.code-quality") + id("io.gitlab.arturbosch.detekt") + id("org.jetbrains.kotlinx.kover") } java { @@ -39,4 +35,11 @@ tasks.withType().configureEach { tasks.withType().configureEach { useJUnitPlatform() + + val testsBadgeApiKey = providers.gradleProperty("testsBadgeApiKey") + addTestListener(Rife2TestListener(testsBadgeApiKey)) + testLogging { + exceptionFormat = TestExceptionFormat.FULL + events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) + } } diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-base.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-base.gradle.kts index 4af978b..3a98de0 100644 --- a/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-base.gradle.kts +++ b/buildSrc/src/main/kotlin/buildsrc/conventions/lang/kotlin-multiplatform-base.gradle.kts @@ -1,5 +1,8 @@ package buildsrc.conventions.lang +import buildsrc.utils.Rife2TestListener +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget @@ -13,6 +16,8 @@ import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget plugins { id("buildsrc.conventions.base") kotlin("multiplatform") + id("io.gitlab.arturbosch.detekt") + id("org.jetbrains.kotlinx.kover") } @@ -43,3 +48,12 @@ kotlin { } } } + +tasks.withType().configureEach { + val testsBadgeApiKey = providers.gradleProperty("testsBadgeApiKey") + addTestListener(Rife2TestListener(testsBadgeApiKey)) + testLogging { + exceptionFormat = TestExceptionFormat.FULL + events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) + } +} diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/publishing.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/publishing.gradle.kts index 2390479..9155ad4 100644 --- a/buildSrc/src/main/kotlin/buildsrc/conventions/publishing.gradle.kts +++ b/buildSrc/src/main/kotlin/buildsrc/conventions/publishing.gradle.kts @@ -36,7 +36,7 @@ publishing { licenses { license { name.set("The Apache License, Version 2.0") - url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") } } developers { @@ -84,6 +84,8 @@ signing { sign(publishing.publications) setRequired({ + // only enable signing for non-snapshot versions, or when publishing to a non-local repo, otherwise + // publishing to Maven Local requires signing for users without access to the signing key. !isSnapshotVersion() || gradle.taskGraph.hasTask("publish") }) } @@ -99,6 +101,7 @@ tasks.withType().configureEach { } val javadocJar by tasks.registering(Jar::class) { + description = "Generate Javadoc using Dokka" dependsOn(tasks.dokkaJavadoc) from(tasks.dokkaJavadoc) archiveClassifier.set("javadoc") diff --git a/buildSrc/src/main/kotlin/buildsrc/conventions/code-quality.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/conventions/sonarqube.gradle.kts similarity index 73% rename from buildSrc/src/main/kotlin/buildsrc/conventions/code-quality.gradle.kts rename to buildSrc/src/main/kotlin/buildsrc/conventions/sonarqube.gradle.kts index f78c963..06ca6af 100644 --- a/buildSrc/src/main/kotlin/buildsrc/conventions/code-quality.gradle.kts +++ b/buildSrc/src/main/kotlin/buildsrc/conventions/sonarqube.gradle.kts @@ -17,18 +17,23 @@ package buildsrc.conventions -import buildsrc.utils.Rife2TestListener -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent import org.sonarqube.gradle.SonarTask +/** + * Convention plugin for SonarQube analysis. + * + * SonarQube depends on an aggregated XML coverage report from + * [Kotlinx Kover](https://github.com/Kotlin/kotlinx-kover). + * See the Kover docs for + * [how to aggregate coverage reports](https://kotlin.github.io/kotlinx-kover/gradle-plugin/#multiproject-build). + */ + plugins { id("org.sonarqube") - id("io.gitlab.arturbosch.detekt") id("org.jetbrains.kotlinx.kover") } -sonarqube { +sonar { properties { property("sonar.projectName", rootProject.name) property("sonar.projectKey", "ethauvin_${rootProject.name}") @@ -43,14 +48,6 @@ sonarqube { } tasks.withType().configureEach { + // workaround for https://github.com/Kotlin/kotlinx-kover/issues/394 dependsOn(tasks.matching { it.name == "koverXmlReport" }) } - -tasks.withType().configureEach { - val testsBadgeApiKey = providers.gradleProperty("testsBadgeApiKey") - addTestListener(Rife2TestListener(testsBadgeApiKey)) - testLogging { - exceptionFormat = TestExceptionFormat.FULL - events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) - } -} diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index eb0edae..aeb5eb3 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -1,17 +1,32 @@ -import org.jetbrains.dokka.gradle.DokkaTask +/* + * Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com) + * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * + * 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. + */ +import org.jetbrains.dokka.gradle.DokkaTask plugins { buildsrc.conventions.lang.`kotlin-jvm` buildsrc.conventions.publishing - id("application") + buildsrc.conventions.sonarqube id("com.github.ben-manes.versions") } description = "A simple defensive library to encode/decode URL components" val deployDir = project.layout.projectDirectory.dir("deploy") -val urlEncoderMainClass = "net.thauvin.erik.urlencoder.UrlEncoder" dependencies { // testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25") @@ -19,50 +34,27 @@ dependencies { } base { - archivesName.set(rootProject.name) -} - -application { - mainClass.set(urlEncoderMainClass) + archivesName.set("${rootProject.name}-lib") } tasks { - jar { - manifest { - attributes["Main-Class"] = urlEncoderMainClass - } - } - - val fatJar by registering(Jar::class) { - group = LifecycleBasePlugin.BUILD_GROUP - dependsOn.addAll(listOf("compileJava", "compileKotlin", "processResources")) - archiveClassifier.set("all") - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - manifest { attributes(mapOf("Main-Class" to application.mainClass)) } - val sourcesMain = sourceSets.main.get() - val contents = configurations.runtimeClasspath.get() - .map { if (it.isDirectory) it else zipTree(it) } + sourcesMain.output - from(contents) - } - - build { - dependsOn(fatJar) - } - withType().configureEach { destination = file("$projectDir/pom.xml") } + clean { + delete(deployDir) + } + withType().configureEach { dokkaSourceSets { named("main") { - moduleName.set("UrlEncoder API") + moduleName.set("UrlEncoder Library") } } } val copyToDeploy by registering(Sync::class) { - description = "Copies all needed files to the 'deploy' directory." group = PublishingPlugin.PUBLISH_TASK_GROUP from(configurations.runtimeClasspath) { exclude("annotations-*.jar") @@ -76,17 +68,13 @@ tasks { group = PublishingPlugin.PUBLISH_TASK_GROUP dependsOn(build, copyToDeploy) } - - clean { - delete(deployDir) - } } publishing { publications { create("mavenJava") { from(components["java"]) - artifactId = rootProject.name + artifactId = "${rootProject.name}-lib" artifact(tasks.javadocJar) } } diff --git a/lib/detekt-baseline.xml b/lib/detekt-baseline.xml index 700d787..2562c74 100644 --- a/lib/detekt-baseline.xml +++ b/lib/detekt-baseline.xml @@ -2,14 +2,13 @@ - ComplexCondition:UrlEncoder.kt$UrlEncoder$hasOption && args.size == 2 || !hasOption && args.size == 1 - MagicNumber:UrlEncoder.kt$UrlEncoder$0x80 - MagicNumber:UrlEncoder.kt$UrlEncoder$0xFF - MagicNumber:UrlEncoder.kt$UrlEncoder$16 - MagicNumber:UrlEncoder.kt$UrlEncoder$3 - MagicNumber:UrlEncoder.kt$UrlEncoder$4 - MaxLineLength:UrlEncoder.kt$UrlEncoder$* - NestedBlockDepth:UrlEncoder.kt$UrlEncoder$@JvmStatic @JvmOverloads fun decode(source: String, plusToSpace: Boolean = false): String - NestedBlockDepth:UrlEncoder.kt$UrlEncoder$@JvmStatic @JvmOverloads fun encode(source: String, allow: String = "", spaceToPlus: Boolean = false): String + MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$0x80 + MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$0xFF + MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$16 + MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$3 + MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$4 + MaxLineLength:UrlEncoderUtil.kt$UrlEncoderUtil$* + NestedBlockDepth:UrlEncoderUtil.kt$UrlEncoderUtil$@JvmStatic @JvmOverloads fun decode(source: String, plusToSpace: Boolean = false): String + NestedBlockDepth:UrlEncoderUtil.kt$UrlEncoderUtil$@JvmStatic @JvmOverloads fun encode(source: String, allow: String = "", spaceToPlus: Boolean = false): String diff --git a/lib/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoder.kt b/lib/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoderUtil.kt similarity index 80% rename from lib/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoder.kt rename to lib/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoderUtil.kt index d03145e..7994cb1 100644 --- a/lib/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoder.kt +++ b/lib/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoderUtil.kt @@ -19,7 +19,6 @@ package net.thauvin.erik.urlencoder import java.nio.charset.StandardCharsets import java.util.BitSet -import kotlin.system.exitProcess /** * Most defensive approach to URL encoding and decoding. @@ -37,13 +36,8 @@ import kotlin.system.exitProcess * @author Geert Bevin (gbevin(remove) at uwyn dot com) * @author Erik C. Thauvin (erik@thauvin.net) **/ -object UrlEncoder { +object UrlEncoderUtil { private val hexDigits = "0123456789ABCDEF".toCharArray() - internal val usage = - "Usage : java -jar urlencoder-*all.jar [-ed] text" + System.lineSeparator() + - "Encode and decode URL components defensively." + System.lineSeparator() + - " -e encode (default) " + System.lineSeparator() + - " -d decode" // see https://www.rfc-editor.org/rfc/rfc3986#page-13 // and https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set @@ -200,45 +194,4 @@ object UrlEncoder { return out?.toString() ?: source } - - /** - * Encodes and decodes URLs from the command line. - * - * - `java -jar urlencoder-*all.jar [-ed] text` - */ - @JvmStatic - fun main(args: Array) { - try { - val result = processMain(args) - if (result.status == 1) { - System.err.println(result.output) - } else { - println(result.output) - } - exitProcess(result.status) - } catch (e: IllegalArgumentException) { - System.err.println("${UrlEncoder::class.java.simpleName}: ${e.message}") - exitProcess(1) - } - } - - internal data class MainResult(var output: String = usage, var status: Int = 1) - - internal fun processMain(args: Array): MainResult { - val result = MainResult() - if (args.isNotEmpty() && args[0].isNotEmpty()) { - val hasDecode = (args[0] == "-d") - val hasOption = (hasDecode || args[0] == "-e") - if (hasOption && args.size == 2 || !hasOption && args.size == 1) { - val arg = if (hasOption) args[1] else args[0] - if (hasDecode) { - result.output = decode(arg) - } else { - result.output = encode(arg) - } - result.status = 0 - } - } - return result - } } diff --git a/lib/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderUtilTest.kt b/lib/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderUtilTest.kt new file mode 100644 index 0000000..20d8e4d --- /dev/null +++ b/lib/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderUtilTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com) + * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) + * + * 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 net.thauvin.erik.urlencoder + +import net.thauvin.erik.urlencoder.UrlEncoderUtil.decode +import net.thauvin.erik.urlencoder.UrlEncoderUtil.encode +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +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.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class UrlEncoderUtilTest { + private val same = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_." + + companion object { + @JvmStatic + fun invalid() = arrayOf("sdkjfh%", "sdkjfh%6", "sdkjfh%xx", "sdfjfh%-1") + + @JvmStatic + fun validMap(): Stream = 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" + ) + ) + } + + @ParameterizedTest(name = "decode({0}) should be {1}") + @MethodSource("validMap") + fun `Decode URL`(expected: String, source: String) { + assertEquals(expected, decode(source)) + } + + @ParameterizedTest(name = "decode({0})") + @MethodSource("invalid") + fun `Decode with Exception`(source: String) { + assertThrows(IllegalArgumentException::class.java, { decode(source) }, "decode($source)") + } + + @Test + fun `Decode when None needed`() { + assertSame(same, decode(same)) + assertEquals("", decode(""), "decode('')") + assertEquals(" ", decode(" "), "decode(' ')") + } + + @Test + fun `Decode with Plus to Space`() { + assertEquals("foo bar", decode("foo+bar", true)) + assertEquals("foo bar foo", decode("foo+bar++foo", true)) + assertEquals("foo bar foo", decode("foo+%20bar%20+foo", true)) + assertEquals("foo + bar", decode("foo+%2B+bar", plusToSpace = true)) + assertEquals("foo+bar", decode("foo%2Bbar", plusToSpace = true)) + } + + @ParameterizedTest(name = "encode({0}) should be {1}") + @MethodSource("validMap") + fun `Encode URL`(source: String, expected: String) { + assertEquals(expected, encode(source)) + } + + @Test + fun `Encode Empty or Blank`() { + assertTrue(encode("", allow = "").isEmpty(), "encode('','')") + assertEquals("", encode(""), "encode('')") + assertEquals("%20", encode(" "), "encode(' ')") + } + + @Test + fun `Encode when None needed`() { + assertSame(same, encode(same)) + assertSame(same, encode(same, allow = ""), "with empty allow") + } + + @Test + fun `Encode with Allow Arg`() { + assertEquals("?test=a%20test", encode("?test=a test", allow = "=?"), "encode(x, =?)") + assertEquals("aaa", encode("aaa", "a"), "encode(aaa, a)") + assertEquals(" ", encode(" ", " "), "encode(' ', ' ')") + } + + @Test + fun `Encode with Space to Plus`() { + assertEquals("foo+bar", encode("foo bar", spaceToPlus = true)) + assertEquals("foo+bar++foo", encode("foo bar foo", spaceToPlus = true)) + assertEquals("foo bar", encode("foo bar", " ", true)) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e75db1d..86af703 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,6 +20,9 @@ dependencyResolutionManagement { } } +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + include( + ":app", ":lib", )