From fad3714ae0fc1ccbe79fe7e2132fae3ddfa9bd7b Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Sun, 1 Jan 2023 06:26:47 -0800 Subject: [PATCH] Added main method for command line usage --- README.md | 11 +++- lib/build.gradle.kts | 25 ++++++--- lib/pom.xml | 4 +- .../net/thauvin/erik/urlencoder/UrlEncoder.kt | 55 ++++++++++++++++++- .../thauvin/erik/urlencoder/UrlEncoderTest.kt | 50 ++++++++++++++++- 5 files changed, 127 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 446b66b..d9d421b 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,20 @@ This library was adapted from the [RIFE2 Web Application Framework](https://rife A pure Java version can also be found at [https://github.com/gbevin/urlencoder](https://github.com/gbevin/urlencoder). -For decades we've been using [java.net.URLEncoder](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URLEncoder.html) because of its improper naming. It is actually intended to encode HTML form parameters, not URLs. +For decades we've been using [java.net.URLEncoder](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URLEncoder.html) because of its improper naming. It is actually intended to encode HTML form parameters, not URLs, causing the wrong escape sequences to be used. -Android's [Uri.encode](https://developer.android.com/reference/android/net/Uri#encode(java.lang.String,%20java.lang.String)) also addresses this issue. +Additionally, `java.net.URLEncoder` allocates memory even when no encoding is necessary, significantly impacting performance. This library has a negligible performance impact when the specified string doesn't need to be encoded. + + +Android's [Uri.encode](https://developer.android.com/reference/android/net/Uri#encode(java.lang.String,%20java.lang.String)) also addresses these issues, but does not currently support [unicode surrogate pairs](https://learn.microsoft.com/en-us/globalization/encoding/surrogate-pairs). ## Examples (TL;DR) ```kotlin +UrlEncoder.encode("a test &") // -> a%20test%20%26 UrlEncoder.encode("%#okékÉȢ smile!😁") // -> %25%23ok%C3%A9k%C3%89%C8%A2%20smile%21%F0%9F%98%81 -UrlEncoder.encode("?test=a test", '=', '?') // -> ?test=a%20test +UrlEncoder.encode("?test=a test", allow = "?=") // -> ?test=a%20test +UrlEncoder.decode("a%20test%20%26") // -> a test & UrlEncoder.decode("%25%23ok%C3%A9k%C3%89%C8%A2%20smile%21%F0%9F%98%81") // -> %#okékÉȢ smile!😁 ``` diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index be86be5..cf9a02f 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -4,9 +4,9 @@ import org.jetbrains.dokka.gradle.DokkaTask import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { + id("application") id("com.github.ben-manes.versions") version "0.44.0" id("io.gitlab.arturbosch.detekt") version "1.22.0" - id("java") id("java-library") id("maven-publish") id("org.jetbrains.dokka") version "1.7.20" @@ -16,10 +16,11 @@ plugins { id("signing") } -description = "Encode and decode URL parameters" +description = "A simple library to encode/decode URL parameters" group = "net.thauvin.erik" version = "0.9-SNAPSHOT" +val mavenName = "UrlEncoder" val deployDir = "deploy" val gitHub = "ethauvin/${rootProject.name}" val mavenUrl = "https://github.com/$gitHub" @@ -34,12 +35,20 @@ dependencies { testImplementation(kotlin("test")) } +base { + archivesName.set(rootProject.name) +} + java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 withSourcesJar() } +application { + mainClass.set("$group.${rootProject.name}.$mavenName") +} + sonarqube { properties { property("sonar.projectName", rootProject.name) @@ -58,6 +67,12 @@ val javadocJar by tasks.creating(Jar::class) { } tasks { + jar { + manifest { + attributes["Main-Class"] = "$group.${rootProject.name}.$mavenName" + } + } + withType().configureEach { kotlinOptions.jvmTarget = java.targetCompatibility.toString() } @@ -104,10 +119,6 @@ tasks { mustRunAfter(clean) } - jar { - archiveBaseName.set(rootProject.name) - } - "sonar" { dependsOn(koverReport) } @@ -120,7 +131,7 @@ publishing { artifactId = rootProject.name artifact(javadocJar) pom { - name.set(rootProject.name) + name.set(mavenName) description.set(project.description) url.set(mavenUrl) licenses { diff --git a/lib/pom.xml b/lib/pom.xml index ebd9adc..cf4b75a 100644 --- a/lib/pom.xml +++ b/lib/pom.xml @@ -9,8 +9,8 @@ net.thauvin.erik urlencoder 0.9-SNAPSHOT - urlencoder - Encode and decode URL parameters + UrlEncoder + A simple library to encode/decode URL parameters https://github.com/ethauvin/urlencoder diff --git a/lib/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoder.kt b/lib/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoder.kt index 5801d90..81f1872 100644 --- a/lib/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoder.kt +++ b/lib/src/main/kotlin/net/thauvin/erik/urlencoder/UrlEncoder.kt @@ -19,15 +19,22 @@ package net.thauvin.erik.urlencoder import java.nio.charset.StandardCharsets import java.util.BitSet +import kotlin.system.exitProcess /** * URL parameters encoding and decoding. * + * - Rules determined by [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#page-13), + * * @author Geert Bevin (gbevin[remove] at uwyn dot com) * @author Erik C. Thauvin (erik@thauvin.net) */ object UrlEncoder { private val hexDigits = "0123456789ABCDEF".toCharArray() + internal val usage = """Usage : kotlin -cp urlencoder-*.jar ${UrlEncoder::class.java.name} [-ed] text +Encode and decode URL parameters. + -e encode (default) + -d decode""" // see https://www.rfc-editor.org/rfc/rfc3986#page-13 private val unreservedChars = BitSet('~'.code + 1).apply { @@ -103,7 +110,7 @@ object UrlEncoder { } } else { if (bytesBuffer != null) { - out?.append(String(bytesBuffer, 0, bytesPos, StandardCharsets.UTF_8)) + out!!.append(String(bytesBuffer, 0, bytesPos, StandardCharsets.UTF_8)) bytesBuffer = null bytesPos = 0 } @@ -121,7 +128,9 @@ object UrlEncoder { /** * 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. + * encoding. + * + * - Letters, numbers, unreserved (`_-!.~'()*`) and allowed characters are left intact. */ @JvmStatic fun encode(source: String, allow: String): String { @@ -167,10 +176,50 @@ object UrlEncoder { /** * 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. + * encoding. + * + * - Letters, numbers, unreserved (`_-!.~'()*`) and allowed characters are left intact. */ @JvmStatic fun encode(source: String, vararg allow: Char): String { return encode(source, String(allow)) } + + /** + * Encodes and decodes URLs from the command line. + * + * - `kotlin -cp urlencoder-*.jar net.thauvin.erik.urlencoder.UrlEncoder` + */ + @JvmStatic + fun main(args: Array) { + val result = processMain(args) + if (result.status == 1) { + System.err.println(result.output) + } else { + println(result.output) + } + exitProcess(result.status) + } + + 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].isNotBlank()) { + val hasDecode = args[0] == "-d" + val hasOption = hasDecode || args[0] == "-e" + if (!hasOption || args.size >= 2) { + val argsList = mutableListOf() + argsList.addAll(args) + if (hasOption) argsList.removeAt(0) + if (hasDecode) { + result.output = decode(argsList.joinToString(" ")) + } else { + result.output = encode(argsList.joinToString(" ")) + } + result.status = 0 + } + } + return result + } } diff --git a/lib/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderTest.kt b/lib/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderTest.kt index 223e8e8..7756ae9 100644 --- a/lib/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderTest.kt +++ b/lib/src/test/kotlin/net/thauvin/erik/urlencoder/UrlEncoderTest.kt @@ -19,6 +19,8 @@ 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.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -44,9 +46,10 @@ class UrlEncoderTest { assertEquals(it.key, decode(it.value)) } invalid.forEach { - assertFailsWith(IllegalArgumentException::class) { - decode(it) - } + assertFailsWith( + message = it, + block = { decode(it) } + ) } } @@ -63,4 +66,45 @@ class UrlEncoderTest { assertEquals("?test=a%20test", encode("?test=a test", "=?")) assertEquals("aaa", encode("aaa", 'a')) } + + @Test + fun testMainDecode() { + var result: UrlEncoder.MainResult + validMap.forEach { + result = processMain(arrayOf("-d", it.value)) + assertEquals(result.output, it.key, it.key) + assertEquals(result.status, 0, it.key) + } + } + + @Test + fun testMainEncode() { + var result: UrlEncoder.MainResult + validMap.forEach { + result = processMain(arrayOf("-e", it.key)) + assertEquals(it.value, result.output, "-e ${it.key}") + assertEquals(0, result.status, "-e ${it.key}") + + result = processMain(arrayOf(it.key)) + assertEquals(it.value, result.output, it.value) + assertEquals(0, result.status, it.value) + } + + invalid.forEach { + assertFailsWith( + message = it, + block = { processMain(arrayOf("-d", it)) } + ) + } + } + + @Test + fun testMainUsage() { + var result: UrlEncoder.MainResult + for (arg in arrayOf("", " ", "-d", "-e")) { + result = processMain(arrayOf(arg)) + assertEquals(usage, result.output, "processMain('$arg')") + assertEquals(1, result.status, "processMain('$arg')") + } + } }