Compare commits
153 commits
Author | SHA1 | Date | |
---|---|---|---|
b882d68d4c | |||
9e60b31f9e | |||
c69d7b7076 | |||
742c221a20 | |||
0b6e55b338 | |||
09b44d53e8 | |||
df7482c3a0 | |||
3e36e389fb | |||
d66fdb12e6 | |||
|
6f33f9c2a9 | ||
dd3d500496 | |||
|
c801a9703f | ||
1ef2045b32 | |||
|
d03c61cb92 | ||
a689c56e1e | |||
b59d01ec6d | |||
|
8890fef665 | ||
|
171570159e | ||
61137e0878 | |||
f3fc266c6f | |||
7817198be1 | |||
b9791dc507 | |||
ee35984130 | |||
05d4a52240 | |||
8ebc178474 | |||
404ca93137 | |||
2523b31073 | |||
6acf3417e5 | |||
f711cf719f | |||
ebdc9ceef8 | |||
9f3803bdcd | |||
5b46ff448c | |||
d790a02365 | |||
7ea75e30e7 | |||
e54ec3d98d | |||
0f369ca080 | |||
efcfe5e56f | |||
0fa364b47e | |||
392af12f50 | |||
0235444814 | |||
938bd54f2b | |||
f3f70f1f93 | |||
b6b7dabb50 | |||
080b7a971f | |||
61a93bd523 | |||
53f8fbab21 | |||
1ad7dc46dd | |||
825bcd7355 | |||
e7ab107f9c | |||
a0e0cff0cd | |||
|
8fcd629bce | ||
ae060f5bd2 | |||
dd2dc9e380 | |||
32e21491c7 | |||
5a6edfbfa0 | |||
e5cb0bd903 | |||
57eb20a160 | |||
|
cdb0176560 | ||
38dee4d236 | |||
fb19749795 | |||
2eb1c45f95 | |||
|
3f049993b9 | ||
|
dcb4dfb735 | ||
|
5f8533f9aa | ||
|
2d01ef821a | ||
|
757e651642 | ||
|
54ab7ff75f | ||
9988c003b4 | |||
d55080773b | |||
641ffad173 | |||
|
6d9023c8a6 | ||
|
1d6fa1c0ad | ||
c177d49934 | |||
de16f451fa | |||
bcf59ee0e7 | |||
|
05ee976d13 | ||
|
b6fbea813b | ||
|
1b0a5aa208 | ||
|
60feafa3f9 | ||
|
a4e919a75f | ||
|
a493d4e00b | ||
|
dce203845e | ||
4df6d3f599 | |||
7f8d044d04 | |||
221156e1fd | |||
6f1e6fd203 | |||
c946b9d6d9 | |||
3a19b5e895 | |||
00eef331d8 | |||
|
17ea49aba6 | ||
e45fc71ab9 | |||
090ccbff18 | |||
|
9b34b5684c | ||
|
9be2d3897e | ||
|
231bca79fb | ||
01c46bbdc9 | |||
|
6dbd5f7e15 | ||
cb460faf2b | |||
|
1986a8e56d | ||
|
f1622d0c0a | ||
|
c1efd8a955 | ||
|
97a23a195f | ||
|
76750cfd71 | ||
|
3b6fffdc52 | ||
|
09a58f4142 | ||
|
13c73903c3 | ||
|
86ae661788 | ||
|
bccfd0003f | ||
|
e53dc8c546 | ||
|
7ddfa061c6 | ||
|
78403becf2 | ||
228b3fb4e3 | |||
8bf5bb8349 | |||
f6b5a77eb5 | |||
|
12c1c4f12a | ||
|
62cc110651 | ||
|
fc3a5648ed | ||
|
1b40145970 | ||
bbfd09c3ba | |||
|
ae8ffb91bb | ||
|
b2316b0029 | ||
d90fe5a7c8 | |||
|
671e4c6810 | ||
|
acfaaec754 | ||
|
6670346890 | ||
|
b8a394c9ad | ||
|
b2d20e93ed | ||
|
61b51824fd | ||
|
4d294bfee8 | ||
|
93e113fa69 | ||
|
c9bbe70fbb | ||
|
425e79eb38 | ||
|
a3d973100a | ||
34b69a7d1f | |||
13c09989b2 | |||
f18f145ead | |||
67ecf7f069 | |||
ac278b6fef | |||
41912076b0 | |||
53a9f6a4ac | |||
9f6b5a0913 | |||
4b746ba75c | |||
7c8a4358b6 | |||
f8b9376f40 | |||
50ffe56ba8 | |||
a049175ed6 | |||
d4a6347310 | |||
1da595e08a | |||
aa3b40f307 | |||
a7b67c39af | |||
e20c096cfe | |||
3f44a59ff5 | |||
b9c1449d55 |
47 changed files with 3629 additions and 916 deletions
77
.github/workflows/gradle.yml
vendored
77
.github/workflows/gradle.yml
vendored
|
@ -1,61 +1,52 @@
|
||||||
name: gradle-ci
|
name: gradle-ci
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
GRADLE_OPTS: "-Dorg.gradle.jvmargs=-XX:MaxMetaspaceSize=512m"
|
|
||||||
SONAR_JDK: "11"
|
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
java-version: [ 11, 17, 19 ]
|
java-version: [8, 11, 17, 21]
|
||||||
|
os:
|
||||||
|
- macos-latest
|
||||||
|
- ubuntu-latest
|
||||||
|
- windows-latest
|
||||||
|
fail-fast: false
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
GRADLE_OPTS: "-Dorg.gradle.jvmargs=-XX:MaxMetaspaceSize=512m"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up JDK ${{ matrix.java-version }}
|
- name: Set up JDK ${{ matrix.java-version }}
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
|
distribution: "zulu"
|
||||||
java-version: ${{ matrix.java-version }}
|
java-version: ${{ matrix.java-version }}
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Validate Gradle wrapper
|
||||||
run: chmod +x gradlew
|
uses: gradle/wrapper-validation-action@v2
|
||||||
|
|
||||||
- name: Cache SonarCloud packages
|
- name: Cache Kotlin Konan
|
||||||
if: matrix.java-version == env.SONAR_JDK
|
uses: actions/cache@v4
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
with:
|
||||||
path: ~/.sonar/cache
|
path: ~/.konan/**/*
|
||||||
key: ${{ runner.os }}-sonar
|
key: kotlin-konan-${{ runner.os }}
|
||||||
restore-keys: ${{ runner.os }}-sonar
|
|
||||||
|
|
||||||
- name: Cache Gradle packages
|
- name: Set up Gradle
|
||||||
uses: actions/cache@v2
|
uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-gradle-${{ matrix.java-version }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-${{ matrix.java-version }}-
|
|
||||||
|
|
||||||
- name: Test with Gradle
|
- name: Publish
|
||||||
run: ./gradlew build check --stacktrace -PtestsBadgeApiKey=${{ secrets.TESTS_BADGE_API_KEY }}
|
run: ./gradlew check build --stacktrace -PtestsBadgeApiKey=${{ secrets.TESTS_BADGE_API_KEY }}
|
||||||
|
|
||||||
- name: SonarCloud
|
|
||||||
if: success() && matrix.java-version == env.SONAR_JDK
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
run: ./gradlew sonar
|
|
||||||
|
|
||||||
- name: Cleanup Gradle Cache
|
|
||||||
run: |
|
|
||||||
rm -f ~/.gradle/caches/modules-2/modules-2.lock
|
|
||||||
rm -f ~/.gradle/caches/modules-2/gc.properties
|
|
||||||
|
|
44
.github/workflows/publish.yml
vendored
Normal file
44
.github/workflows/publish.yml
vendored
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
name: publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.OSSRH_USERNAME}}
|
||||||
|
ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.OSSRH_PASSWORD}}
|
||||||
|
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGN_SECRET_KEY }}
|
||||||
|
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGN_SECRET_PWD }}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
|
||||||
|
# Don't cancel midway through publishing if another workflow is triggered, it might cause partial publications
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: macos-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: "21"
|
||||||
|
distribution: "zulu"
|
||||||
|
|
||||||
|
- name: Validate Gradle wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@v2
|
||||||
|
|
||||||
|
- name: Cache Kotlin Konan
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.konan/**/*
|
||||||
|
key: kotlin-konan-${{ runner.os }}
|
||||||
|
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: ./gradlew publish --no-parallel --stacktrace
|
149
.gitignore
vendored
149
.gitignore
vendored
|
@ -1,85 +1,72 @@
|
||||||
!.vscode/extensions.json
|
### Gradle ###
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
*.class
|
|
||||||
*.code-workspace
|
|
||||||
*.ctxt
|
|
||||||
*.iws
|
|
||||||
*.log
|
|
||||||
*.nar
|
|
||||||
*.rar
|
|
||||||
*.sublime-*
|
|
||||||
*.tar.gz
|
|
||||||
*.zip
|
|
||||||
.DS_Store
|
|
||||||
.classpath
|
|
||||||
.gradle
|
.gradle
|
||||||
.history
|
|
||||||
.kobalt
|
|
||||||
.mtj.tmp/
|
|
||||||
.mvn/timing.properties
|
|
||||||
.mvn/wrapper/maven-wrapper.jar
|
|
||||||
.nb-gradle
|
|
||||||
.project
|
|
||||||
.scannerwork
|
|
||||||
.settings
|
|
||||||
.vscode/*
|
|
||||||
/**/.idea/$CACHE_FILE$
|
|
||||||
/**/.idea/$PRODUCT_WORKSPACE_FILE$
|
|
||||||
/**/.idea/**/caches/build_file_checksums.ser
|
|
||||||
/**/.idea/**/contentModel.xml
|
|
||||||
/**/.idea/**/dataSources.ids
|
|
||||||
/**/.idea/**/dataSources.local.xml
|
|
||||||
/**/.idea/**/dataSources/
|
|
||||||
/**/.idea/**/dbnavigator.xml
|
|
||||||
/**/.idea/**/dictionaries
|
|
||||||
/**/.idea/**/dynamic.xml
|
|
||||||
/**/.idea/**/gradle.xml
|
|
||||||
/**/.idea/**/httpRequests
|
|
||||||
/**/.idea/**/libraries
|
|
||||||
/**/.idea/**/mongoSettings.xml
|
|
||||||
/**/.idea/**/replstate.xml
|
|
||||||
/**/.idea/**/shelf
|
|
||||||
/**/.idea/**/shelf/
|
|
||||||
/**/.idea/**/sqlDataSources.xml
|
|
||||||
/**/.idea/**/tasks.xml
|
|
||||||
/**/.idea/**/uiDesigner.xml
|
|
||||||
/**/.idea/**/usage.statistics.xml
|
|
||||||
/**/.idea/**/workspace.xml
|
|
||||||
/**/.idea/sonarlint*
|
|
||||||
/**/.idea_modules/
|
|
||||||
Thumbs.db
|
|
||||||
__pycache__
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
bin/
|
|
||||||
build/
|
build/
|
||||||
cmake-build-*/
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics-build.properties
|
|
||||||
crashlytics.properties
|
|
||||||
dependency-reduced-pom.xml
|
|
||||||
deploy/
|
deploy/
|
||||||
dist/
|
|
||||||
ehthumbs.db
|
### Kotlin/JVM ###
|
||||||
fabric.properties
|
*.class
|
||||||
gen/
|
*.log
|
||||||
gradle.properties
|
|
||||||
hs_err_pid*
|
hs_err_pid*
|
||||||
kobaltBuild
|
replay_pid*
|
||||||
kobaltw*-test
|
*.hprof
|
||||||
lib/kotlin*
|
|
||||||
libs/
|
*.jar
|
||||||
local.properties
|
*.war
|
||||||
out/
|
*.nar
|
||||||
pom.xml.asc
|
*.ear
|
||||||
pom.xml.next
|
*.zip
|
||||||
pom.xml.releaseBackup
|
*.tar.gz
|
||||||
pom.xml.tag
|
*.rar
|
||||||
pom.xml.versionsBackup
|
|
||||||
proguard-project.txt
|
|
||||||
project.properties
|
### IntelliJ ###
|
||||||
release.properties
|
.idea/**/*
|
||||||
target/
|
|
||||||
test-output
|
### Eclipse ###
|
||||||
venv
|
.metadata
|
||||||
|
bin/
|
||||||
|
tmp/
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*~.nib
|
||||||
|
.settings/
|
||||||
|
.loadpath
|
||||||
|
.recommenders
|
||||||
|
.classpath
|
||||||
|
|
||||||
|
.apt_generated/
|
||||||
|
.apt_generated_test/
|
||||||
|
.project
|
||||||
|
|
||||||
|
|
||||||
|
### Linux ###
|
||||||
|
*~
|
||||||
|
.fuse_hidden*
|
||||||
|
.Trash-*
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
|
||||||
|
### Windows ###
|
||||||
|
[Dd]esktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
|
||||||
|
### macOS ###
|
||||||
|
.DS_Store
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
|
||||||
|
###########################
|
||||||
|
|
||||||
|
# place overrides last, so they're not themselves overridden
|
||||||
|
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!gradle/wrapper/gradle-wrapper.properties
|
||||||
|
|
||||||
|
!.idea/copyright/**
|
||||||
|
|
3
.idea/.gitignore
generated
vendored
3
.idea/.gitignore
generated
vendored
|
@ -1,3 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<bytecodeTargetLevel target="11" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
2
.idea/copyright/Apache_License.xml
generated
2
.idea/copyright/Apache_License.xml
generated
|
@ -1,6 +1,6 @@
|
||||||
<component name="CopyrightManager">
|
<component name="CopyrightManager">
|
||||||
<copyright>
|
<copyright>
|
||||||
<option name="notice" value="Copyright 2001-&#36;today.year Geert Bevin (gbevin[remove] at uwyn dot com) Copyright &#36;today.year 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." />
|
<option name="notice" value="Copyright 2001-&#36;today.year 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 https://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." />
|
||||||
<option name="myName" value="Apache License" />
|
<option name="myName" value="Apache License" />
|
||||||
</copyright>
|
</copyright>
|
||||||
</component>
|
</component>
|
8
.idea/inspectionProfiles/Project_Default.xml
generated
8
.idea/inspectionProfiles/Project_Default.xml
generated
|
@ -1,8 +0,0 @@
|
||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="JavadocDeclaration" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="ADDITIONAL_TAGS" value="created" />
|
|
||||||
</inspection_tool>
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
25
.idea/jarRepositories.xml
generated
25
.idea/jarRepositories.xml
generated
|
@ -1,25 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RemoteRepositoriesConfiguration">
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="central" />
|
|
||||||
<option name="name" value="Maven Central repository" />
|
|
||||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="jboss.community" />
|
|
||||||
<option name="name" value="JBoss Community repository" />
|
|
||||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="MavenRepo" />
|
|
||||||
<option name="name" value="MavenRepo" />
|
|
||||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="maven" />
|
|
||||||
<option name="name" value="maven" />
|
|
||||||
<option name="url" value="https://oss.sonatype.org/content/repositories/snapshots" />
|
|
||||||
</remote-repository>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
6
.idea/kotlinc.xml
generated
6
.idea/kotlinc.xml
generated
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="KotlinJpsPluginSettings">
|
|
||||||
<option name="version" value="1.8.0" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
8
.idea/misc.xml
generated
8
.idea/misc.xml
generated
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
|
||||||
<component name="PDMPlugin">
|
|
||||||
<option name="skipTestSources" value="false" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="19" project-jdk-type="JavaSDK" />
|
|
||||||
</project>
|
|
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
36
CONTRIBUTING.md
Normal file
36
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# Contributing to UrlEncoder
|
||||||
|
|
||||||
|
First and foremost, thank you for your interest in contributing! Here's a brief guide on how to contribute to the
|
||||||
|
UrlEncoder project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Fork the repository.
|
||||||
|
2. Clone your fork locally.
|
||||||
|
3. Create a new branch for your feature or bugfix.
|
||||||
|
|
||||||
|
## Updating Dependencies
|
||||||
|
|
||||||
|
To support deterministic builds, and to help with dependency analysis tools like Snyk, UrlEncoder uses lockfiles
|
||||||
|
to ensure consistent dependencies. Whenever a dependency is updated the lockfiles must be updated.
|
||||||
|
|
||||||
|
### Gradle Lock Files
|
||||||
|
|
||||||
|
Gradle's [dependency lockfiles](https://docs.gradle.org/current/userguide/dependency_locking.html)
|
||||||
|
can be updated by running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew resolveAndLockAllDependencies --write-locks
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kotlin/JS Lockfile
|
||||||
|
|
||||||
|
The Kotlin/JS target
|
||||||
|
[also uses a lockfile](https://kotlinlang.org/docs/js-project-setup.html#version-locking-via-kotlin-js-store),
|
||||||
|
which is managed by Yarn.
|
||||||
|
|
||||||
|
To update the Kotlin/JS lockfile, run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew kotlinNpmInstall
|
||||||
|
```
|
138
README.md
138
README.md
|
@ -1,50 +1,154 @@
|
||||||
[](https://opensource.org/licenses/Apache-2.0)
|
[](https://opensource.org/licenses/Apache-2.0)
|
||||||
[](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/urlencoder/)
|
[](https://kotlinlang.org/)
|
||||||
|
[](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/urlencoder/)
|
||||||
[](https://github.com/ethauvin/urlencoder/releases/latest)
|
[](https://github.com/ethauvin/urlencoder/releases/latest)
|
||||||
[](https://maven-badges.herokuapp.com/maven-central/net.thauvin.erik/urlencoder)
|
[](https://central.sonatype.com/search?namespace=net.thauvin.erik.urlencoder)
|
||||||
|
|
||||||
[](https://sonarcloud.io/dashboard?id=ethauvin_urlencoder)
|
|
||||||
[](https://github.com/ethauvin/urlencoder/actions/workflows/gradle.yml)
|
[](https://github.com/ethauvin/urlencoder/actions/workflows/gradle.yml)
|
||||||
[](https://github.com/ethauvin/urlencoder/actions/workflows/gradle.yml)
|
[](https://github.com/ethauvin/urlencoder/actions/workflows/gradle.yml)
|
||||||
|
|
||||||
# URL Encoder for Kotlin and Java
|
# URL Encoder for Kotlin Multiplatform
|
||||||
|
|
||||||
A simple library to encode/decode URL parameters.
|
UrlEncoder is a simple defensive library to encode/decode URL components.
|
||||||
|
|
||||||
This library was adapted from the [RIFE2 Web Application Framework](https://rife2.com).
|
This library was adapted from the [RIFE2 Web Application Framework](https://rife2.com).
|
||||||
A pure Java version can also be found at [https://github.com/gbevin/urlencoder](https://github.com/gbevin/urlencoder).
|
A pure Java version can also be found at [https://github.com/gbevin/urlencoder](https://github.com/gbevin/urlencoder).
|
||||||
|
|
||||||
|
The rules are 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).
|
||||||
|
|
||||||
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.
|
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 `+`,
|
||||||
|
that has the potential to be misunderstood.
|
||||||
|
|
||||||
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.
|
This class encodes with rules that will be decoded correctly in either case.
|
||||||
|
|
||||||
|
Additionally, this library allocates no memory when encoding isn't needed and
|
||||||
|
does the work in a single pass without multiple loops. Both of these
|
||||||
|
optimizations have a significantly beneficial impact on performance of encoding
|
||||||
|
compared to other solutions like the standard `URLEncoder` in the JDK or
|
||||||
|
`UriUtils` in Spring.
|
||||||
|
|
||||||
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)
|
## Examples (TL;DR)
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
UrlEncoder.encode("a test &") // -> a%20test%20%26
|
UrlEncoderUtil.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
|
UrlEncoderUtil.encode("%#okékÉȢ smile!😁") // -> %25%23ok%C3%A9k%C3%89%C8%A2%20smile%21%F0%9F%98%81
|
||||||
UrlEncoder.encode("?test=a test", allow = "?=") // -> ?test=a%20test
|
UrlEncoderUtil.encode("?test=a test", allow = "?=") // -> ?test=a%20test
|
||||||
|
UrlEncoderUtil.endode("foo bar", spaceToPlus = true) // -> foo+bar
|
||||||
|
|
||||||
UrlEncoder.decode("a%20test%20%26") // -> a test &
|
UrlEncoderUtil.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!😁
|
UrlEncoderUtil.decode("%25%23ok%C3%A9k%C3%89%C8%A2%20smile%21%F0%9F%98%81") // -> %#okékÉȢ smile!😁
|
||||||
|
UrlEncoderUtil.decode("foo+bar", plusToSpace = true) // -> foo bar
|
||||||
```
|
```
|
||||||
|
|
||||||
## Gradle, Maven, etc.
|
## Gradle, Maven, etc.
|
||||||
|
|
||||||
To use with [Gradle](https://gradle.org/), include the following dependency in your build file:
|
To use with [Gradle](https://gradle.org/), include the following dependency in your build file:
|
||||||
|
|
||||||
```gradle
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } // only needed for SNAPSHOT
|
// only needed for SNAPSHOT
|
||||||
|
maven("https://oss.sonatype.org/content/repositories/snapshots") {
|
||||||
|
name = "SonatypeSnapshots"
|
||||||
|
mavenContent { snapshotsOnly() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("net.thauvin.erik:urlencoder:1.0.0")
|
implementation("net.thauvin.erik.urlencoder:urlencoder-lib:1.6.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Instructions for using with Maven, Ivy, etc. can be found on [Maven Central](https://maven-badges.herokuapp.com/maven-central/net.thauvin.erik/urlencoder).
|
Adding a dependency in [Maven](https://maven.apache.org/) requires specifying the JVM variant by adding a `-jvm` suffix
|
||||||
|
to the artifact URL.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.thauvin.erik.urlencoder</groupId>
|
||||||
|
<artifactId>urlencoder-lib-jvm</artifactId>
|
||||||
|
<version>1.6.0</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
Instructions for using with Ivy, etc. can be found on
|
||||||
|
[Maven Central](https://central.sonatype.com/search?namespace=net.thauvin.erik.urlencoder).
|
||||||
|
|
||||||
|
## Standalone usage
|
||||||
|
|
||||||
|
UrlEncoder can be used on the command line also, both for encoding and decoding.
|
||||||
|
|
||||||
|
You have two options:
|
||||||
|
|
||||||
|
* run it with Gradle
|
||||||
|
* build the jar and launch it with Java
|
||||||
|
|
||||||
|
The usage is as follows:
|
||||||
|
|
||||||
|
```console
|
||||||
|
Encode and decode URL components defensively.
|
||||||
|
-e encode (default)
|
||||||
|
-d decode
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running with Gradle
|
||||||
|
|
||||||
|
```console
|
||||||
|
./gradlew run --quiet --args="-e 'a test &'" # -> a%20test%20%26
|
||||||
|
./gradlew run --quiet --args="%#okékÉȢ" # -> %25%23ok%C3%A9k%C3%89%C8%A2
|
||||||
|
|
||||||
|
./gradlew run --quiet --args="-d 'a%20test%20%26'" # -> a test &
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running with Java
|
||||||
|
|
||||||
|
First build the jar file:
|
||||||
|
|
||||||
|
```console
|
||||||
|
./gradlew fatJar
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run it:
|
||||||
|
|
||||||
|
```console
|
||||||
|
java -jar urlencoder-app/build/libs/urlencoder-*all.jar -e "a test &" # -> a%20test%20%26
|
||||||
|
java -jar urlencoder-app/build/libs/urlencoder-*all.jar "%#okékÉȢ" # -> %25%23ok%C3%A9k%C3%89%C8%A2
|
||||||
|
|
||||||
|
java -jar urlencoder-app/build/libs/urlencoder-*all.jar -d "a%20test%20%26" # -> a test &
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why not simply use `java.net.URLEncoder`?
|
||||||
|
|
||||||
|
Apart for being quite inefficient, some URL components encoded with `URLEncoder.encode` might not be able to be properly decoded.
|
||||||
|
|
||||||
|
For example, a simple search query such as:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val u = URLEncoder.encode("foo +bar", StandardCharsets.UTF_8)
|
||||||
|
```
|
||||||
|
|
||||||
|
would be encoded as:
|
||||||
|
|
||||||
|
```
|
||||||
|
foo+%2Bbar
|
||||||
|
```
|
||||||
|
|
||||||
|
Trying to decode it with [Spring](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/UriUtils.html#decode(java.lang.String,java.lang.String)), for example:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
UriUtils.decode(u, StandardCharsets.UTF_8)
|
||||||
|
```
|
||||||
|
|
||||||
|
would return:
|
||||||
|
|
||||||
|
```
|
||||||
|
foo++bar
|
||||||
|
```
|
||||||
|
|
||||||
|
Unfortunately, decoding with [Uri.decode](https://developer.android.com/reference/android/net/Uri#decode(java.lang.String)) on Android, [decodeURI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI) in Javascript, etc. would yield the exact same result.
|
||||||
|
|
||||||
|

|
||||||
|
|
12
build.gradle.kts
Normal file
12
build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
plugins {
|
||||||
|
buildsrc.conventions.base
|
||||||
|
id("org.jetbrains.kotlinx.kover")
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "net.thauvin.erik.urlencoder"
|
||||||
|
version = "1.6.0"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
kover(projects.urlencoderLib)
|
||||||
|
kover(projects.urlencoderApp)
|
||||||
|
}
|
12
buildSrc/build.gradle.kts
Normal file
12
buildSrc/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("com.github.ben-manes:gradle-versions-plugin:0.51.0")
|
||||||
|
implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.7")
|
||||||
|
implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.9.20")
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24")
|
||||||
|
implementation("org.jetbrains.kotlinx:kover-gradle-plugin:0.8.3")
|
||||||
|
implementation("org.apache.httpcomponents:httpclient:4.5.13")
|
||||||
|
}
|
16
buildSrc/settings.gradle.kts
Normal file
16
buildSrc/settings.gradle.kts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
rootProject.name = "buildSrc"
|
||||||
|
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package buildsrc.conventions
|
||||||
|
|
||||||
|
/** common config for all subprojects */
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
base
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project != rootProject) {
|
||||||
|
project.version = rootProject.version
|
||||||
|
project.group = rootProject.group
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<AbstractArchiveTask>().configureEach {
|
||||||
|
// https://docs.gradle.org/current/userguide/working_with_files.html#sec:reproducible_archives
|
||||||
|
isPreserveFileTimestamps = false
|
||||||
|
isReproducibleFileOrder = true
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
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.ExperimentalKotlinGradlePluginApi
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base configuration for all Kotlin/Multiplatform conventions.
|
||||||
|
*
|
||||||
|
* This plugin does not enable any Kotlin target. To enable a target in a subproject, prefer applying specific Kotlin
|
||||||
|
* target convention plugins.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id("buildsrc.conventions.base")
|
||||||
|
kotlin("multiplatform")
|
||||||
|
id("io.gitlab.arturbosch.detekt")
|
||||||
|
id("org.jetbrains.kotlinx.kover")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
//jvmToolchain(11)
|
||||||
|
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||||
|
compilerOptions {
|
||||||
|
languageVersion = KotlinVersion.KOTLIN_1_6
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure all Kotlin/JVM Tests to use JUnit
|
||||||
|
targets.withType<KotlinJvmTarget>().configureEach {
|
||||||
|
testRuns.configureEach {
|
||||||
|
executionTask.configure {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
withType<JavaCompile>().configureEach {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8.toString()
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
withType<KotlinJvmCompile>().configureEach {
|
||||||
|
compilerOptions.jvmTarget.set(JvmTarget.JVM_1_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
withType<Test>().configureEach {
|
||||||
|
val testsBadgeApiKey = providers.gradleProperty("testsBadgeApiKey")
|
||||||
|
addTestListener(Rife2TestListener(testsBadgeApiKey))
|
||||||
|
testLogging {
|
||||||
|
exceptionFormat = TestExceptionFormat.FULL
|
||||||
|
events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package buildsrc.conventions.lang
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
|
||||||
|
|
||||||
|
/** conventions for a Kotlin/JS subproject */
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("buildsrc.conventions.lang.kotlin-multiplatform-base")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
js(IR) {
|
||||||
|
browser()
|
||||||
|
nodejs()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
nodejs()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmWasi {
|
||||||
|
nodejs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relocateKotlinJsStore()
|
||||||
|
|
||||||
|
|
||||||
|
//region FIXME: WORKAROUND https://youtrack.jetbrains.com/issue/KT-65864
|
||||||
|
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
|
||||||
|
rootProject.extensions.configure<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension> {
|
||||||
|
// Use a Node.js version current enough to support Kotlin/Wasm
|
||||||
|
nodeVersion = "22.0.0-nightly2024010568c8472ed9"
|
||||||
|
logger.lifecycle("Using Node.js $nodeVersion to support Kotlin/Wasm")
|
||||||
|
nodeDownloadBaseUrl = "https://nodejs.org/download/nightly"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.tasks.withType<org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask>().configureEach {
|
||||||
|
// Prevent Yarn from complaining about newer Node.js versions.
|
||||||
|
args.add("--ignore-engines")
|
||||||
|
}
|
||||||
|
//endregion
|
|
@ -0,0 +1,11 @@
|
||||||
|
package buildsrc.conventions.lang
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("buildsrc.conventions.lang.kotlin-multiplatform-base")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm {
|
||||||
|
withJava()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package buildsrc.conventions.lang
|
||||||
|
|
||||||
|
/** conventions for a Kotlin/Native subproject */
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("buildsrc.conventions.lang.kotlin-multiplatform-base")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
linuxX64()
|
||||||
|
|
||||||
|
mingwX64()
|
||||||
|
|
||||||
|
linuxArm64()
|
||||||
|
|
||||||
|
macosX64()
|
||||||
|
macosArm64()
|
||||||
|
|
||||||
|
iosArm64()
|
||||||
|
iosX64()
|
||||||
|
iosSimulatorArm64()
|
||||||
|
|
||||||
|
watchosArm32()
|
||||||
|
watchosArm64()
|
||||||
|
watchosX64()
|
||||||
|
watchosSimulatorArm64()
|
||||||
|
watchosDeviceArm64()
|
||||||
|
|
||||||
|
tvosArm64()
|
||||||
|
tvosX64()
|
||||||
|
tvosSimulatorArm64()
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package buildsrc.conventions.lang
|
||||||
|
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `kotlin-js` and `kotlin-multiplatform` plugins adds a directory in the root-dir for the Yarn
|
||||||
|
* lockfile. That's a bit annoying. It's a little neater if it's in the Gradle dir, next to the
|
||||||
|
* version catalog.
|
||||||
|
*/
|
||||||
|
internal fun Project.relocateKotlinJsStore() {
|
||||||
|
afterEvaluate {
|
||||||
|
rootProject.extensions.configure<YarnRootExtension> {
|
||||||
|
lockFileDirectory = project.rootDir.resolve("gradle/kotlin-js-store")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package buildsrc.conventions
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("maven-publish")
|
||||||
|
id("signing")
|
||||||
|
id("org.jetbrains.dokka")
|
||||||
|
}
|
||||||
|
|
||||||
|
val gitHub = "ethauvin/${rootProject.name}"
|
||||||
|
val mavenUrl = "https://github.com/$gitHub"
|
||||||
|
val isSnapshotVersion = { project.version.toString().contains("SNAPSHOT") }
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
withType<MavenPublication>().configureEach {
|
||||||
|
pom {
|
||||||
|
name.set("UrlEncoder for Kotlin Multiplatform")
|
||||||
|
description.set("A simple defensive library to encode/decode URL components")
|
||||||
|
url.set(mavenUrl)
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name.set("The Apache License, Version 2.0")
|
||||||
|
url.set("https://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/")
|
||||||
|
}
|
||||||
|
developer {
|
||||||
|
id.set("aSemy")
|
||||||
|
name.set("Adam")
|
||||||
|
url.set("https://github.com/aSemy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scm {
|
||||||
|
connection.set("scm:git://github.com/$gitHub.git")
|
||||||
|
developerConnection.set("scm:git@github.com:$gitHub.git")
|
||||||
|
url.set(mavenUrl)
|
||||||
|
}
|
||||||
|
issueManagement {
|
||||||
|
system.set("GitHub")
|
||||||
|
url.set("$mavenUrl/issues")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
maven(
|
||||||
|
if (isSnapshotVersion()) {
|
||||||
|
uri("https://oss.sonatype.org/content/repositories/snapshots/")
|
||||||
|
} else {
|
||||||
|
uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
name = "ossrh"
|
||||||
|
credentials(PasswordCredentials::class)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signing {
|
||||||
|
val signingKey: String? by project
|
||||||
|
val signingPassword: String? by project
|
||||||
|
useInMemoryPgpKeys(signingKey, signingPassword)
|
||||||
|
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
withType<Sign>().configureEach {
|
||||||
|
val signingRequiredPredicate = provider { signing.isRequired }
|
||||||
|
onlyIf { signingRequiredPredicate.get() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://youtrack.jetbrains.com/issue/KT-46466
|
||||||
|
val signingTasks = tasks.withType<Sign>()
|
||||||
|
tasks.withType<AbstractPublishToMaven>().configureEach {
|
||||||
|
dependsOn(signingTasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
val javadocJar by tasks.registering(Jar::class) {
|
||||||
|
description = "Generate Javadoc using Dokka"
|
||||||
|
dependsOn(tasks.dokkaJavadoc)
|
||||||
|
from(tasks.dokkaJavadoc)
|
||||||
|
archiveClassifier.set("javadoc")
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications.withType<MavenPublication>().configureEach {
|
||||||
|
artifact(javadocJar)
|
||||||
|
}
|
||||||
|
}
|
63
buildSrc/src/main/kotlin/buildsrc/utils/Rife2TestListener.kt
Normal file
63
buildSrc/src/main/kotlin/buildsrc/utils/Rife2TestListener.kt
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2001-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
|
||||||
|
*
|
||||||
|
* https://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 buildsrc.utils
|
||||||
|
|
||||||
|
import org.apache.http.client.methods.HttpPost
|
||||||
|
import org.apache.http.impl.client.HttpClients
|
||||||
|
import org.apache.http.util.EntityUtils
|
||||||
|
import org.gradle.api.provider.Provider
|
||||||
|
import org.gradle.api.tasks.testing.TestDescriptor
|
||||||
|
import org.gradle.api.tasks.testing.TestListener
|
||||||
|
import org.gradle.api.tasks.testing.TestResult
|
||||||
|
|
||||||
|
class Rife2TestListener(
|
||||||
|
private val testBadgeApiKey: Provider<String>
|
||||||
|
) : 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
|
||||||
|
|
||||||
|
val apiKey = testBadgeApiKey.orNull
|
||||||
|
|
||||||
|
if (apiKey != null) {
|
||||||
|
println(apiKey)
|
||||||
|
val url = "https://rife2.com/tests-badge/update/net.thauvin.erik/urlencoder?" +
|
||||||
|
"apiKey=$apiKey&" +
|
||||||
|
"passed=$passed&" +
|
||||||
|
"failed=$failed&" +
|
||||||
|
"skipped=$skipped"
|
||||||
|
|
||||||
|
val client = HttpClients.createDefault()
|
||||||
|
val post = HttpPost(url)
|
||||||
|
|
||||||
|
val response = client.execute(post)
|
||||||
|
val entity = response.entity
|
||||||
|
|
||||||
|
val statusCode = response.statusLine.statusCode
|
||||||
|
val responseBody = EntityUtils.toString(entity, "UTF-8")
|
||||||
|
|
||||||
|
println("RESPONSE: $statusCode")
|
||||||
|
println(responseBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
gradle.properties
Normal file
11
gradle.properties
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
org.gradle.jvmargs=-Dfile.encoding=UTF-8
|
||||||
|
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.welcome=never
|
||||||
|
|
||||||
|
# enableKgpDependencyResolution provides a smoother import experience in multiplatform projects
|
||||||
|
# https://kotlinlang.org/docs/whatsnew1820.html#preview-of-gradle-composite-builds-support-in-kotlin-multiplatform
|
||||||
|
kotlin.mpp.import.enableKgpDependencyResolution=true
|
||||||
|
# hide warning "Some Kotlin/Native targets cannot be built on this mingw_x64 machine and are disabled"
|
||||||
|
kotlin.native.ignoreDisabledTargets=true
|
1926
gradle/kotlin-js-store/yarn.lock
Normal file
1926
gradle/kotlin-js-store/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,7 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
34
gradlew
vendored
34
gradlew
vendored
|
@ -15,6 +15,8 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
|
@ -55,7 +57,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
@ -83,10 +85,9 @@ done
|
||||||
# This is normally unused
|
# This is normally unused
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
' "$PWD" ) || exit
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
@ -133,10 +134,13 @@ location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD=java
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
|
@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
|
@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
|
@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
# double quotes to make sure that they get re-expanded; and
|
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
|
22
gradlew.bat
vendored
22
gradlew.bat
vendored
|
@ -13,6 +13,8 @@
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|
|
@ -1,229 +0,0 @@
|
||||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
|
||||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
|
||||||
import org.jetbrains.dokka.gradle.DokkaTask
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
import java.net.URI
|
|
||||||
import java.net.http.HttpClient
|
|
||||||
import java.net.http.HttpRequest
|
|
||||||
import java.net.http.HttpResponse
|
|
||||||
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("application")
|
|
||||||
id("com.github.ben-manes.versions") version "0.44.0"
|
|
||||||
id("io.gitlab.arturbosch.detekt") version "1.22.0"
|
|
||||||
id("java-library")
|
|
||||||
id("maven-publish")
|
|
||||||
id("org.jetbrains.dokka") version "1.7.20"
|
|
||||||
id("org.jetbrains.kotlin.jvm") version "1.8.0"
|
|
||||||
id("org.jetbrains.kotlinx.kover") version "0.6.1"
|
|
||||||
id("org.sonarqube") version "3.5.0.2730"
|
|
||||||
id("signing")
|
|
||||||
}
|
|
||||||
|
|
||||||
description = "A simple library to encode/decode URL parameters"
|
|
||||||
group = "net.thauvin.erik"
|
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
val mavenName = "UrlEncoder"
|
|
||||||
val deployDir = "deploy"
|
|
||||||
val gitHub = "ethauvin/${rootProject.name}"
|
|
||||||
val mavenUrl = "https://github.com/$gitHub"
|
|
||||||
val publicationName = "mavenJava"
|
|
||||||
val myClassName = "$group.${rootProject.name}.$mavenName"
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25")
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
base {
|
|
||||||
archivesName.set(rootProject.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
withSourcesJar()
|
|
||||||
}
|
|
||||||
|
|
||||||
application {
|
|
||||||
mainClass.set(myClassName)
|
|
||||||
}
|
|
||||||
|
|
||||||
sonarqube {
|
|
||||||
properties {
|
|
||||||
property("sonar.projectName", rootProject.name)
|
|
||||||
property("sonar.projectKey", "ethauvin_${rootProject.name}")
|
|
||||||
property("sonar.organization", "ethauvin-github")
|
|
||||||
property("sonar.host.url", "https://sonarcloud.io")
|
|
||||||
property("sonar.sourceEncoding", "UTF-8")
|
|
||||||
property("sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/kover/xml/report.xml")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val javadocJar by tasks.creating(Jar::class) {
|
|
||||||
dependsOn(tasks.dokkaJavadoc)
|
|
||||||
from(tasks.dokkaJavadoc)
|
|
||||||
archiveClassifier.set("javadoc")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
|
||||||
jar {
|
|
||||||
manifest {
|
|
||||||
attributes["Main-Class"] = myClassName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
withType<KotlinCompile>().configureEach {
|
|
||||||
kotlinOptions.jvmTarget = java.targetCompatibility.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
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"]
|
|
||||||
println(apiKey)
|
|
||||||
val response: HttpResponse<String> = HttpClient.newHttpClient()
|
|
||||||
.send(
|
|
||||||
HttpRequest.newBuilder()
|
|
||||||
.uri(
|
|
||||||
URI(
|
|
||||||
"https://rife2.com/tests-badge/update/net.thauvin.erik/urlencoder?" +
|
|
||||||
"apiKey=$apiKey&" +
|
|
||||||
"passed=$passed&" +
|
|
||||||
"failed=$failed&" +
|
|
||||||
"skipped=$skipped"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.POST(HttpRequest.BodyPublishers.noBody())
|
|
||||||
.build(), HttpResponse.BodyHandlers.ofString()
|
|
||||||
)
|
|
||||||
println("RESPONSE: ${response.statusCode()}")
|
|
||||||
println(response.body())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
withType<Test> {
|
|
||||||
testLogging {
|
|
||||||
exceptionFormat = TestExceptionFormat.FULL
|
|
||||||
events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
withType<GenerateMavenPom> {
|
|
||||||
destination = file("$projectDir/pom.xml")
|
|
||||||
}
|
|
||||||
|
|
||||||
clean {
|
|
||||||
doLast {
|
|
||||||
project.delete(fileTree(deployDir))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
withType<DokkaTask>().configureEach {
|
|
||||||
dokkaSourceSets {
|
|
||||||
named("main") {
|
|
||||||
moduleName.set("UrlEncoder API")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val copyToDeploy by registering(Copy::class) {
|
|
||||||
from(configurations.runtimeClasspath) {
|
|
||||||
exclude("annotations-*.jar")
|
|
||||||
}
|
|
||||||
from(jar)
|
|
||||||
into(deployDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
register("deploy") {
|
|
||||||
description = "Copies all needed files to the $deployDir directory."
|
|
||||||
group = PublishingPlugin.PUBLISH_TASK_GROUP
|
|
||||||
dependsOn(clean, build, jar)
|
|
||||||
outputs.dir(deployDir)
|
|
||||||
inputs.files(copyToDeploy)
|
|
||||||
mustRunAfter(clean)
|
|
||||||
}
|
|
||||||
|
|
||||||
"sonar" {
|
|
||||||
dependsOn(koverReport)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
publishing {
|
|
||||||
publications {
|
|
||||||
create<MavenPublication>(publicationName) {
|
|
||||||
from(components["java"])
|
|
||||||
artifactId = rootProject.name
|
|
||||||
artifact(javadocJar)
|
|
||||||
pom {
|
|
||||||
name.set(mavenName)
|
|
||||||
description.set(project.description)
|
|
||||||
url.set(mavenUrl)
|
|
||||||
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://github.com/$gitHub.git")
|
|
||||||
developerConnection.set("scm:git@github.com:$gitHub.git")
|
|
||||||
url.set(mavenUrl)
|
|
||||||
}
|
|
||||||
issueManagement {
|
|
||||||
system.set("GitHub")
|
|
||||||
url.set("$mavenUrl/issues")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
name = "ossrh"
|
|
||||||
url = if (project.version.toString().contains("SNAPSHOT"))
|
|
||||||
uri("https://oss.sonatype.org/content/repositories/snapshots/") else
|
|
||||||
uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
|
|
||||||
credentials(PasswordCredentials::class)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signing {
|
|
||||||
useGpgCmd()
|
|
||||||
sign(publishing.publications[publicationName])
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
<?xml version='1.0' encoding='UTF-8'?>
|
|
||||||
<SmellBaseline>
|
|
||||||
<ManuallySuppressedIssues/>
|
|
||||||
<CurrentIssues>
|
|
||||||
<ID>ComplexCondition:UrlEncoder.kt$UrlEncoder$hasOption && args.size == 2 || !hasOption && args.size == 1</ID>
|
|
||||||
<ID>MagicNumber:UrlEncoder.kt$UrlEncoder$0x80</ID>
|
|
||||||
<ID>MagicNumber:UrlEncoder.kt$UrlEncoder$0xFF</ID>
|
|
||||||
<ID>MagicNumber:UrlEncoder.kt$UrlEncoder$16</ID>
|
|
||||||
<ID>MagicNumber:UrlEncoder.kt$UrlEncoder$3</ID>
|
|
||||||
<ID>MagicNumber:UrlEncoder.kt$UrlEncoder$4</ID>
|
|
||||||
<ID>NestedBlockDepth:UrlEncoder.kt$UrlEncoder$@JvmStatic fun encode(source: String, allow: String): String</ID>
|
|
||||||
</CurrentIssues>
|
|
||||||
</SmellBaseline>
|
|
52
lib/pom.xml
52
lib/pom.xml
|
@ -1,52 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
||||||
<!-- This module was also published with a richer model, Gradle metadata, -->
|
|
||||||
<!-- which should be used instead. Do not delete the following line which -->
|
|
||||||
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
|
|
||||||
<!-- that they should prefer consuming it instead. -->
|
|
||||||
<!-- do_not_remove: published-with-gradle-metadata -->
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<groupId>net.thauvin.erik</groupId>
|
|
||||||
<artifactId>urlencoder</artifactId>
|
|
||||||
<version>1.0.0</version>
|
|
||||||
<name>UrlEncoder</name>
|
|
||||||
<description>A simple library to encode/decode URL parameters</description>
|
|
||||||
<url>https://github.com/ethauvin/urlencoder</url>
|
|
||||||
<licenses>
|
|
||||||
<license>
|
|
||||||
<name>The Apache License, Version 2.0</name>
|
|
||||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
|
||||||
</license>
|
|
||||||
</licenses>
|
|
||||||
<developers>
|
|
||||||
<developer>
|
|
||||||
<id>gbevin</id>
|
|
||||||
<name>Geert Bevin</name>
|
|
||||||
<email>gbevin@uwyn.com</email>
|
|
||||||
<url>https://github.com/gbevin</url>
|
|
||||||
</developer>
|
|
||||||
<developer>
|
|
||||||
<id>ethauvin</id>
|
|
||||||
<name>Erik C. Thauvin</name>
|
|
||||||
<email>erik@thauvin.net</email>
|
|
||||||
<url>https://erik.thauvin.net/</url>
|
|
||||||
</developer>
|
|
||||||
</developers>
|
|
||||||
<scm>
|
|
||||||
<connection>scm:git://github.com/ethauvin/urlencoder.git</connection>
|
|
||||||
<developerConnection>scm:git@github.com:ethauvin/urlencoder.git</developerConnection>
|
|
||||||
<url>https://github.com/ethauvin/urlencoder</url>
|
|
||||||
</scm>
|
|
||||||
<issueManagement>
|
|
||||||
<system>GitHub</system>
|
|
||||||
<url>https://github.com/ethauvin/urlencoder/issues</url>
|
|
||||||
</issueManagement>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
|
||||||
<version>1.8.0</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
|
@ -1,227 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
|
|
||||||
* Copyright 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 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" + System.lineSeparator() +
|
|
||||||
"Encode and decode URL parameters." + System.lineSeparator() + " -e encode (default) " +
|
|
||||||
System.lineSeparator() + " -d decode"
|
|
||||||
|
|
||||||
// see https://www.rfc-editor.org/rfc/rfc3986#page-13
|
|
||||||
private val unreservedChars = BitSet('~'.code + 1).apply {
|
|
||||||
set('-')
|
|
||||||
set('.')
|
|
||||||
for (c in '0'..'9') {
|
|
||||||
set(c)
|
|
||||||
}
|
|
||||||
for (c in 'A'..'Z') {
|
|
||||||
set(c)
|
|
||||||
}
|
|
||||||
set('_'.code)
|
|
||||||
for (c in 'a'.code..'z'.code) {
|
|
||||||
set(c)
|
|
||||||
}
|
|
||||||
set('~')
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BitSet.set(c: Char) = this.set(c.code)
|
|
||||||
|
|
||||||
// see https://www.rfc-editor.org/rfc/rfc3986#page-13
|
|
||||||
private fun Char.isUnreserved(): Boolean {
|
|
||||||
return this <= '~' && unreservedChars.get(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun StringBuilder.appendEncodedDigit(digit: Int) {
|
|
||||||
this.append(hexDigits[digit and 0x0F])
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun StringBuilder.appendEncodedByte(ch: Int) {
|
|
||||||
this.append("%")
|
|
||||||
this.appendEncodedDigit(ch shr 4)
|
|
||||||
this.appendEncodedDigit(ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms a provided [String] into a new string, containing decoded URL characters in the UTF-8
|
|
||||||
* encoding.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun decode(source: String): String {
|
|
||||||
if (source.isBlank()) {
|
|
||||||
return source
|
|
||||||
}
|
|
||||||
|
|
||||||
val length = source.length
|
|
||||||
var out: StringBuilder? = null
|
|
||||||
var ch: Char
|
|
||||||
var bytesBuffer: ByteArray? = null
|
|
||||||
var bytesPos = 0
|
|
||||||
var i = 0
|
|
||||||
while (i < length) {
|
|
||||||
ch = source[i]
|
|
||||||
if (ch == '%') {
|
|
||||||
if (out == null) {
|
|
||||||
out = StringBuilder(length)
|
|
||||||
out.append(source, 0, i)
|
|
||||||
}
|
|
||||||
if (bytesBuffer == null) {
|
|
||||||
// the remaining characters divided by the length of the encoding format %xx, is the maximum number
|
|
||||||
// of bytes that can be extracted
|
|
||||||
bytesBuffer = ByteArray((length - i) / 3)
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
require(length >= i + 2) { "Illegal escape sequence" }
|
|
||||||
try {
|
|
||||||
val v: Int = source.substring(i, i + 2).toInt(16)
|
|
||||||
require(v in 0..0xFF) { "Illegal escape value" }
|
|
||||||
bytesBuffer[bytesPos++] = v.toByte()
|
|
||||||
i += 2
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
throw IllegalArgumentException("Illegal characters in escape sequence: $e.message")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (bytesBuffer != null) {
|
|
||||||
out!!.append(String(bytesBuffer, 0, bytesPos, StandardCharsets.UTF_8))
|
|
||||||
bytesBuffer = null
|
|
||||||
bytesPos = 0
|
|
||||||
}
|
|
||||||
out?.append(ch)
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytesBuffer != null) {
|
|
||||||
out!!.append(String(bytesBuffer, 0, bytesPos, StandardCharsets.UTF_8))
|
|
||||||
}
|
|
||||||
|
|
||||||
return out?.toString() ?: source
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
fun encode(source: String, allow: String): String {
|
|
||||||
if (source.isEmpty()) {
|
|
||||||
return source
|
|
||||||
}
|
|
||||||
var out: StringBuilder? = null
|
|
||||||
var ch: Char
|
|
||||||
var i = 0
|
|
||||||
while (i < source.length) {
|
|
||||||
ch = source[i]
|
|
||||||
if (ch.isUnreserved() || allow.indexOf(ch) != -1) {
|
|
||||||
out?.append(ch)
|
|
||||||
i++
|
|
||||||
} else {
|
|
||||||
if (out == null) {
|
|
||||||
out = StringBuilder(source.length)
|
|
||||||
out.append(source, 0, i)
|
|
||||||
}
|
|
||||||
val cp = source.codePointAt(i)
|
|
||||||
if (cp < 0x80) {
|
|
||||||
out.appendEncodedByte(cp)
|
|
||||||
i++
|
|
||||||
} else if (Character.isBmpCodePoint(cp)) {
|
|
||||||
for (b in ch.toString().toByteArray(StandardCharsets.UTF_8)) {
|
|
||||||
out.appendEncodedByte(b.toInt())
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
} else if (Character.isSupplementaryCodePoint(cp)) {
|
|
||||||
val high = Character.highSurrogate(cp)
|
|
||||||
val low = Character.lowSurrogate(cp)
|
|
||||||
for (b in charArrayOf(high, low).concatToString().toByteArray(StandardCharsets.UTF_8)) {
|
|
||||||
out.appendEncodedByte(b.toInt())
|
|
||||||
}
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out?.toString() ?: source
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
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<String>) {
|
|
||||||
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<String>): 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,156 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
|
|
||||||
* Copyright 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.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
|
|
||||||
import org.junit.jupiter.params.provider.Arguments.arguments
|
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
|
||||||
import org.junit.jupiter.params.provider.ValueSource
|
|
||||||
import java.util.stream.Stream
|
|
||||||
|
|
||||||
class UrlEncoderTest {
|
|
||||||
private val same = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.~"
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun invalid() = arrayOf("sdkjfh%", "sdkjfh%6", "sdkjfh%xx", "sdfjfh%-1")
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun validMap(): Stream<Arguments> = Stream.of(
|
|
||||||
arguments("a test &", "a%20test%20%26"),
|
|
||||||
arguments(
|
|
||||||
"!abcdefghijklmnopqrstuvwxyz%%ABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.~=",
|
|
||||||
"%21abcdefghijklmnopqrstuvwxyz%25%25ABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.~%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(' ')")
|
|
||||||
}
|
|
||||||
|
|
||||||
@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("", "").isEmpty(), "encode('','')")
|
|
||||||
assertEquals("", encode(""), "encode('')")
|
|
||||||
assertEquals("%20", encode(" "), "encode('')")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Encode when None needed`() {
|
|
||||||
assertSame(same, encode(same))
|
|
||||||
assertSame(same, encode(same, ""), "with empty allow")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Encode with Allow Arg`() {
|
|
||||||
assertEquals("?test=a%20test", encode("?test=a test", '=', '?'), "encode(x, =, ?)")
|
|
||||||
assertEquals("?test=a%20test", encode("?test=a test", "=?"), "encode(x, =?)")
|
|
||||||
assertEquals("aaa", encode("aaa", 'a'), "encode(aaa, a)")
|
|
||||||
assertEquals(" ", encode(" ", ' '), "encode(' ', ' ')")
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest(name = "processMain(-d {1}) should be {0}")
|
|
||||||
@MethodSource("validMap")
|
|
||||||
fun `Main Decode`(expected: String, source: String) {
|
|
||||||
val result: UrlEncoder.MainResult = processMain(arrayOf("-d", source))
|
|
||||||
assertEquals(expected, result.output)
|
|
||||||
assertEquals(0, result.status, "processMain(-d $source).status")
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest(name = "processMain(-d {0})")
|
|
||||||
@MethodSource("invalid")
|
|
||||||
fun `Main Decode with Exception`(source: String) {
|
|
||||||
assertThrows(IllegalArgumentException::class.java, { processMain(arrayOf("-d", source)) }, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest(name = "processMain(-e {0})")
|
|
||||||
@MethodSource("validMap")
|
|
||||||
fun `Main Encode`(source: String, expected: String) {
|
|
||||||
val result = processMain(arrayOf(source))
|
|
||||||
assertEquals(expected, result.output)
|
|
||||||
assertEquals(0, result.status, "processMain(-e $source).status")
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest(name = "processMain(-e {0})")
|
|
||||||
@MethodSource("validMap")
|
|
||||||
fun `Main Encode with Option`(source: String, expected: String) {
|
|
||||||
val result = processMain(arrayOf("-e", source))
|
|
||||||
assertEquals(expected, result.output)
|
|
||||||
assertEquals(0, result.status, "processMain(-e $source).status")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Main Usage with Empty Args`() {
|
|
||||||
assertEquals(usage, processMain(arrayOf(" ", " ")).output, "processMain(' ', ' ')")
|
|
||||||
assertEquals(usage, processMain(arrayOf("foo", " ")).output, "processMain('foo', ' ')")
|
|
||||||
assertEquals(usage, processMain(arrayOf(" ", "foo")).output, "processMain(' ', 'foo')")
|
|
||||||
assertEquals(usage, processMain(arrayOf("-d ", "")).output, "processMain('-d', '')")
|
|
||||||
assertEquals("%20", processMain(arrayOf("-e", " ")).output, "processMain('-e', ' ')")
|
|
||||||
assertEquals(" ", processMain(arrayOf("-d", " ")).output, "processMain('-d', ' ')")
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ValueSource(strings = ["", "-d", "-e"])
|
|
||||||
fun `Main Usage with Invalid arg`(arg: String) {
|
|
||||||
val result = processMain(arrayOf(arg))
|
|
||||||
assertEquals(usage, result.output, "processMain('$arg')")
|
|
||||||
assertEquals(1, result.status, "processMain('$arg').status")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Main Usage with too Many Args`() {
|
|
||||||
assertEquals(usage, processMain(arrayOf("foo", "bar", "test")).output, "too many args")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +1,90 @@
|
||||||
/*
|
|
||||||
* This file was generated by the Gradle 'init' task.
|
|
||||||
*
|
|
||||||
* The settings file is used to specify which projects to include in your build.
|
|
||||||
*
|
|
||||||
* Detailed information about configuring a multi-project build in Gradle can be found
|
|
||||||
* in the user manual at https://docs.gradle.org/7.6/userguide/multi_project_builds.html
|
|
||||||
* This project uses @Incubating APIs which are subject to change.
|
|
||||||
*/
|
|
||||||
|
|
||||||
rootProject.name = "urlencoder"
|
rootProject.name = "urlencoder"
|
||||||
include("lib")
|
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://oss.sonatype.org/content/repositories/snapshots") {
|
||||||
|
name = "Sonatype Snapshots"
|
||||||
|
mavenContent { snapshotsOnly() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare the Node.js & Yarn download repositories
|
||||||
|
exclusiveContent {
|
||||||
|
forRepositories(
|
||||||
|
ivy("https://nodejs.org/dist/") {
|
||||||
|
name = "Node Distributions at $url"
|
||||||
|
patternLayout { artifact("v[revision]/[artifact](-v[revision]-[classifier]).[ext]") }
|
||||||
|
metadataSources { artifact() }
|
||||||
|
},
|
||||||
|
ivy("https://nodejs.org/download/v8-canary/") {
|
||||||
|
name = "Node Canary Distributions at $url"
|
||||||
|
patternLayout { artifact("v[revision]/[artifact](-v[revision]-[classifier]).[ext]") }
|
||||||
|
metadataSources { artifact() }
|
||||||
|
},
|
||||||
|
ivy("https://nodejs.org/download/nightly/") {
|
||||||
|
name = "Node Nightly Distributions at $url"
|
||||||
|
patternLayout { artifact("v[revision]/[artifact](-v[revision]-[classifier]).[ext]") }
|
||||||
|
metadataSources { artifact() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
filter { includeGroup("org.nodejs") }
|
||||||
|
}
|
||||||
|
|
||||||
|
exclusiveContent {
|
||||||
|
forRepository {
|
||||||
|
ivy("https://github.com/yarnpkg/yarn/releases/download") {
|
||||||
|
name = "Yarn Distributions at $url"
|
||||||
|
patternLayout { artifact("v[revision]/[artifact](-v[revision]).[ext]") }
|
||||||
|
metadataSources { artifact() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter { includeGroup("com.yarnpkg") }
|
||||||
|
}
|
||||||
|
|
||||||
|
// workaround for https://youtrack.jetbrains.com/issue/KT-51379
|
||||||
|
exclusiveContent {
|
||||||
|
forRepository {
|
||||||
|
ivy("https://download.jetbrains.com/kotlin/native/builds") {
|
||||||
|
name = "Kotlin Native"
|
||||||
|
patternLayout {
|
||||||
|
// example download URLs:
|
||||||
|
// https://download.jetbrains.com/kotlin/native/builds/releases/1.7.20/linux-x86_64/kotlin-native-prebuilt-linux-x86_64-1.7.20.tar.gz
|
||||||
|
// https://download.jetbrains.com/kotlin/native/builds/releases/1.7.20/windows-x86_64/kotlin-native-prebuilt-windows-x86_64-1.7.20.zip
|
||||||
|
// https://download.jetbrains.com/kotlin/native/builds/releases/1.7.20/macos-x86_64/kotlin-native-prebuilt-macos-x86_64-1.7.20.tar.gz
|
||||||
|
listOf(
|
||||||
|
"macos-x86_64",
|
||||||
|
"macos-aarch64",
|
||||||
|
"osx-x86_64",
|
||||||
|
"osx-aarch64",
|
||||||
|
"linux-x86_64",
|
||||||
|
"windows-x86_64",
|
||||||
|
).forEach { os ->
|
||||||
|
listOf("dev", "releases").forEach { stage ->
|
||||||
|
artifact("$stage/[revision]/$os/[artifact]-[revision].[ext]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadataSources { artifact() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter { includeModuleByRegex(".*", ".*kotlin-native-prebuilt.*") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||||
|
|
||||||
|
include(
|
||||||
|
":urlencoder-app",
|
||||||
|
":urlencoder-lib",
|
||||||
|
)
|
||||||
|
|
67
urlencoder-app/build.gradle.kts
Normal file
67
urlencoder-app/build.gradle.kts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import org.jetbrains.dokka.gradle.DokkaTask
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
buildsrc.conventions.lang.`kotlin-multiplatform-jvm`
|
||||||
|
buildsrc.conventions.publishing
|
||||||
|
id("com.github.ben-manes.versions")
|
||||||
|
}
|
||||||
|
|
||||||
|
val urlEncoderMainClass = "net.thauvin.erik.urlencoder.UrlEncoder"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm {
|
||||||
|
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||||
|
mainRun {
|
||||||
|
mainClass.set(urlEncoderMainClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.urlencoderLib)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jvmTest {
|
||||||
|
dependencies {
|
||||||
|
//implementation("com.willowtreeapps.assertk:assertk-jvm:0.25")
|
||||||
|
//implementation("org.junit.jupiter:junit-jupiter:5.9.1")
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base {
|
||||||
|
archivesName.set(rootProject.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
jvmJar {
|
||||||
|
manifest {
|
||||||
|
attributes["Main-Class"] = urlEncoderMainClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fatJar by registering(Jar::class) {
|
||||||
|
group = LifecycleBasePlugin.BUILD_GROUP
|
||||||
|
archiveClassifier.set("all")
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
manifest { attributes(mapOf("Main-Class" to urlEncoderMainClass)) }
|
||||||
|
from(sourceSets.main.map { it.output })
|
||||||
|
dependsOn(configurations.jvmRuntimeClasspath)
|
||||||
|
from(configurations.jvmRuntimeClasspath.map { classpath ->
|
||||||
|
classpath.filter { it.name.endsWith(".jar") }.map { zipTree(it) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
build {
|
||||||
|
dependsOn(fatJar)
|
||||||
|
}
|
||||||
|
|
||||||
|
withType<DokkaTask>().configureEach {
|
||||||
|
dokkaSourceSets.configureEach {
|
||||||
|
moduleName.set("UrlEncoder Application")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
urlencoder-app/detekt-baseline.xml
Normal file
8
urlencoder-app/detekt-baseline.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<SmellBaseline>
|
||||||
|
<ManuallySuppressedIssues/>
|
||||||
|
<CurrentIssues>
|
||||||
|
<ID>ComplexCondition:UrlEncoder.kt$UrlEncoder$hasOption && args.size == 2 || !hasOption && args.size == 1</ID>
|
||||||
|
<ID>MaxLineLength:UrlEncoder.kt$UrlEncoder$*</ID>
|
||||||
|
</CurrentIssues>
|
||||||
|
</SmellBaseline>
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2001-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
|
||||||
|
*
|
||||||
|
* https://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<String>) {
|
||||||
|
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<String>): 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)
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2001-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
|
||||||
|
*
|
||||||
|
* https://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.UrlEncoder.processMain
|
||||||
|
import net.thauvin.erik.urlencoder.UrlEncoder.usage
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class UrlEncoderTest {
|
||||||
|
companion object {
|
||||||
|
val invalid = listOf("sdkjfh%", "sdkjfh%6", "sdkjfh%xx", "sdfjfh%-1")
|
||||||
|
|
||||||
|
val validMap = listOf(
|
||||||
|
"a test &" to "a%20test%20%26",
|
||||||
|
"!abcdefghijklmnopqrstuvwxyz%%ABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.~=" to
|
||||||
|
"%21abcdefghijklmnopqrstuvwxyz%25%25ABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.%7E%3D",
|
||||||
|
"%#okékÉȢ smile!😁" to "%25%23ok%C3%A9k%C3%89%C8%A2%20smile%21%F0%9F%98%81",
|
||||||
|
"\uD808\uDC00\uD809\uDD00\uD808\uDF00\uD808\uDD00" to "%F0%92%80%80%F0%92%94%80%F0%92%8C%80%F0%92%84%80",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Encode with SpaceToPlus`() {
|
||||||
|
assertEquals("this+is+a+test", UrlEncoder.encode("this is a test", spaceToPlus = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Encode with Allow`() {
|
||||||
|
assertEquals("this is a test", UrlEncoder.encode("this is a test", allow = " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Encode without Parameters`() {
|
||||||
|
for (m in validMap) {
|
||||||
|
assertEquals(m.second, UrlEncoder.encode(m.first), "encode(${m.first})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Main Decode`() {
|
||||||
|
for (m in validMap) {
|
||||||
|
val result: UrlEncoder.MainResult = processMain(arrayOf("-d", m.second))
|
||||||
|
assertEquals(m.first, result.output)
|
||||||
|
assertEquals(0, result.status, "processMain(-d ${m.second}).status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Main Decode with Exception`() {
|
||||||
|
for (source in invalid) {
|
||||||
|
assertFailsWith<IllegalArgumentException>(
|
||||||
|
message = source,
|
||||||
|
block = { processMain(arrayOf("-d", source)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Main Encode`() {
|
||||||
|
for (m in validMap) {
|
||||||
|
val result = processMain(arrayOf(m.first))
|
||||||
|
assertEquals(m.second, result.output)
|
||||||
|
assertEquals(0, result.status, "processMain(-e ${m.first}).status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Main Encode with Option`() {
|
||||||
|
for (m in validMap) {
|
||||||
|
val result = processMain(arrayOf("-e", m.first))
|
||||||
|
assertEquals(m.second, result.output)
|
||||||
|
assertEquals(0, result.status, "processMain(-e ${m.first}).status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Main Usage with Empty Args`() {
|
||||||
|
assertEquals(usage, processMain(arrayOf(" ", " ")).output, "processMain(' ', ' ')")
|
||||||
|
assertEquals(usage, processMain(arrayOf("foo", " ")).output, "processMain('foo', ' ')")
|
||||||
|
assertEquals(usage, processMain(arrayOf(" ", "foo")).output, "processMain(' ', 'foo')")
|
||||||
|
assertEquals(usage, processMain(arrayOf("-d ", "")).output, "processMain('-d', '')")
|
||||||
|
assertEquals("%20", processMain(arrayOf("-e", " ")).output, "processMain('-e', ' ')")
|
||||||
|
assertEquals(" ", processMain(arrayOf("-d", " ")).output, "processMain('-d', ' ')")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Main Usage with Invalid arg`() {
|
||||||
|
for (arg in arrayOf("", "-d", "-e")) {
|
||||||
|
val result = processMain(arrayOf(arg))
|
||||||
|
assertEquals(usage, result.output, "processMain('$arg')")
|
||||||
|
assertEquals(1, result.status, "processMain('$arg').status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Main Usage with too Many Args`() {
|
||||||
|
assertEquals(usage, processMain(arrayOf("foo", "bar", "test")).output, "too many args")
|
||||||
|
}
|
||||||
|
}
|
46
urlencoder-lib/build.gradle.kts
Normal file
46
urlencoder-lib/build.gradle.kts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import org.jetbrains.dokka.gradle.DokkaTask
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
buildsrc.conventions.lang.`kotlin-multiplatform-jvm`
|
||||||
|
buildsrc.conventions.lang.`kotlin-multiplatform-js`
|
||||||
|
buildsrc.conventions.lang.`kotlin-multiplatform-native`
|
||||||
|
buildsrc.conventions.publishing
|
||||||
|
id("com.github.ben-manes.versions")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonTest {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
base {
|
||||||
|
archivesName.set("${rootProject.name}-lib")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
dokkaJavadoc {
|
||||||
|
dokkaSourceSets {
|
||||||
|
configureEach {
|
||||||
|
suppress.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val commonMain by getting {
|
||||||
|
suppress.set(false)
|
||||||
|
platform.set(org.jetbrains.dokka.Platform.jvm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
withType<DokkaTask>().configureEach {
|
||||||
|
dokkaSourceSets.configureEach {
|
||||||
|
moduleName.set("UrlEncoder Library")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
urlencoder-lib/detekt-baseline.xml
Normal file
15
urlencoder-lib/detekt-baseline.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<SmellBaseline>
|
||||||
|
<ManuallySuppressedIssues/>
|
||||||
|
<CurrentIssues>
|
||||||
|
<ID>MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$0x80</ID>
|
||||||
|
<ID>MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$0xFF</ID>
|
||||||
|
<ID>MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$16</ID>
|
||||||
|
<ID>MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$3</ID>
|
||||||
|
<ID>MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$4</ID>
|
||||||
|
<ID>MaxLineLength:UrlEncoderUtil.kt$UrlEncoderUtil$*</ID>
|
||||||
|
<ID>NestedBlockDepth:UrlEncoderUtil.kt$UrlEncoderUtil$@JvmStatic @JvmOverloads fun decode(source: String, plusToSpace: Boolean = false): String</ID>
|
||||||
|
<ID>NestedBlockDepth:UrlEncoderUtil.kt$UrlEncoderUtil$@JvmStatic @JvmOverloads fun encode(source: String, allow: String = "", spaceToPlus: Boolean = false): String</ID>
|
||||||
|
<ID>WildcardImport:UrlEncoderUtilTest.kt$import kotlin.test.*</ID>
|
||||||
|
</CurrentIssues>
|
||||||
|
</SmellBaseline>
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2001-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
|
||||||
|
*
|
||||||
|
* https://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.Char.Companion.MIN_HIGH_SURROGATE
|
||||||
|
import kotlin.Char.Companion.MIN_LOW_SURROGATE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kotlin Multiplatform equivalent for `java.lang.Character`
|
||||||
|
*
|
||||||
|
* @author <a href="https://github.com/aSemy">aSemy</a>
|
||||||
|
*/
|
||||||
|
|
||||||
|
internal object Character {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://www.tutorialspoint.com/java/lang/character_issupplementarycodepoint.htm
|
||||||
|
*
|
||||||
|
* Determines whether the specified character (Unicode code point) is in the supplementary character range.
|
||||||
|
* The supplementary character range in the Unicode system falls in `U+10000` to `U+10FFFF`.
|
||||||
|
*
|
||||||
|
* The Unicode code points are divided into two categories:
|
||||||
|
* Basic Multilingual Plane (BMP) code points and Supplementary code points.
|
||||||
|
* BMP code points are present in the range U+0000 to U+FFFF.
|
||||||
|
*
|
||||||
|
* Whereas, supplementary characters are rare characters that are not represented using the original 16-bit Unicode.
|
||||||
|
* For example, these type of characters are used in Chinese or Japanese scripts and hence, are required by the
|
||||||
|
* applications used in these countries.
|
||||||
|
*
|
||||||
|
* @returns `true` if the specified code point falls in the range of supplementary code points
|
||||||
|
* ([MIN_SUPPLEMENTARY_CODE_POINT] to [MAX_CODE_POINT], inclusive), `false` otherwise.
|
||||||
|
*/
|
||||||
|
internal fun isSupplementaryCodePoint(codePoint: Int): Boolean =
|
||||||
|
codePoint in MIN_SUPPLEMENTARY_CODE_POINT..MAX_CODE_POINT
|
||||||
|
|
||||||
|
internal fun toCodePoint(highSurrogate: Char, lowSurrogate: Char): Int =
|
||||||
|
(highSurrogate.code shl 10) + lowSurrogate.code + SURROGATE_DECODE_OFFSET
|
||||||
|
|
||||||
|
/** Basic Multilingual Plane (BMP) */
|
||||||
|
internal fun isBmpCodePoint(codePoint: Int): Boolean = codePoint ushr 16 == 0
|
||||||
|
|
||||||
|
internal fun highSurrogateOf(codePoint: Int): Char =
|
||||||
|
((codePoint ushr 10) + HIGH_SURROGATE_ENCODE_OFFSET.code).toChar()
|
||||||
|
|
||||||
|
internal fun lowSurrogateOf(codePoint: Int): Char =
|
||||||
|
((codePoint and 0x3FF) + MIN_LOW_SURROGATE.code).toChar()
|
||||||
|
|
||||||
|
// private const val MIN_CODE_POINT: Int = 0x000000
|
||||||
|
private const val MAX_CODE_POINT: Int = 0x10FFFF
|
||||||
|
|
||||||
|
private const val MIN_SUPPLEMENTARY_CODE_POINT: Int = 0x10000
|
||||||
|
|
||||||
|
private const val SURROGATE_DECODE_OFFSET: Int =
|
||||||
|
MIN_SUPPLEMENTARY_CODE_POINT -
|
||||||
|
(MIN_HIGH_SURROGATE.code shl 10) -
|
||||||
|
MIN_LOW_SURROGATE.code
|
||||||
|
|
||||||
|
private const val HIGH_SURROGATE_ENCODE_OFFSET: Char = MIN_HIGH_SURROGATE - (MIN_SUPPLEMENTARY_CODE_POINT ushr 10)
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2001-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
|
||||||
|
*
|
||||||
|
* https://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.jvm.JvmOverloads
|
||||||
|
import kotlin.jvm.JvmStatic
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 UrlEncoderUtil {
|
||||||
|
private val hexDigits = "0123456789ABCDEF".toCharArray()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [BooleanArray] with entries for the [character codes][Char.code] of
|
||||||
|
*
|
||||||
|
* * `0-9`,
|
||||||
|
* * `A-Z`,
|
||||||
|
* * `a-z`
|
||||||
|
*
|
||||||
|
* set to `true`.
|
||||||
|
*/
|
||||||
|
private val unreservedChars = BooleanArray('z'.code + 1).apply {
|
||||||
|
set('-'.code, true)
|
||||||
|
set('.'.code, true)
|
||||||
|
set('_'.code, true)
|
||||||
|
for (c in '0'..'9') {
|
||||||
|
set(c.code, true)
|
||||||
|
}
|
||||||
|
for (c in 'A'..'Z') {
|
||||||
|
set(c.code, true)
|
||||||
|
}
|
||||||
|
for (c in 'a'..'z') {
|
||||||
|
set(c.code, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 fun Char.isUnreserved(): Boolean {
|
||||||
|
return this <= 'z' && unreservedChars[code]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.appendEncodedDigit(digit: Int) {
|
||||||
|
this.append(hexDigits[digit and 0x0F])
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.appendEncodedByte(ch: Int) {
|
||||||
|
this.append("%")
|
||||||
|
this.appendEncodedDigit(ch shr 4)
|
||||||
|
this.appendEncodedDigit(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
if (source.isEmpty()) {
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
val length = source.length
|
||||||
|
val out = StringBuilder(length)
|
||||||
|
var bytesBuffer: ByteArray? = null
|
||||||
|
var bytesPos = 0
|
||||||
|
var i = 0
|
||||||
|
var started = false
|
||||||
|
while (i < length) {
|
||||||
|
val ch = source[i]
|
||||||
|
if (ch == '%') {
|
||||||
|
if (!started) {
|
||||||
|
out.append(source, 0, i)
|
||||||
|
started = true
|
||||||
|
}
|
||||||
|
if (bytesBuffer == null) {
|
||||||
|
// the remaining characters divided by the length of the encoding format %xx, is the maximum number
|
||||||
|
// of bytes that can be extracted
|
||||||
|
bytesBuffer = ByteArray((length - i) / 3)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
require(length >= i + 2) { "Incomplete trailing escape ($ch) pattern" }
|
||||||
|
try {
|
||||||
|
val v = source.substring(i, i + 2).toInt(16)
|
||||||
|
require(v in 0..0xFF) { "Illegal escape value" }
|
||||||
|
bytesBuffer[bytesPos++] = v.toByte()
|
||||||
|
i += 2
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw IllegalArgumentException("Illegal characters in escape sequence: $e.message", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (bytesBuffer != null) {
|
||||||
|
out.append(bytesBuffer.decodeToString(0, bytesPos))
|
||||||
|
started = true
|
||||||
|
bytesBuffer = null
|
||||||
|
bytesPos = 0
|
||||||
|
}
|
||||||
|
if (plusToSpace && ch == '+') {
|
||||||
|
if (!started) {
|
||||||
|
out.append(source, 0, i)
|
||||||
|
started = true
|
||||||
|
}
|
||||||
|
out.append(" ")
|
||||||
|
} else if (started) {
|
||||||
|
out.append(ch)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesBuffer != null) {
|
||||||
|
out.append(bytesBuffer.decodeToString(0, bytesPos))
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (!started) source else out.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
if (source.isEmpty()) {
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
var out: StringBuilder? = null
|
||||||
|
var i = 0
|
||||||
|
while (i < source.length) {
|
||||||
|
val ch = source[i]
|
||||||
|
if (ch.isUnreserved() || ch in allow) {
|
||||||
|
out?.append(ch)
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
if (out == null) {
|
||||||
|
out = StringBuilder(source.length)
|
||||||
|
out.append(source, 0, i)
|
||||||
|
}
|
||||||
|
val cp = source.codePointAt(i)
|
||||||
|
when {
|
||||||
|
cp < 0x80 -> {
|
||||||
|
if (spaceToPlus && ch == ' ') {
|
||||||
|
out.append('+')
|
||||||
|
} else {
|
||||||
|
out.appendEncodedByte(cp)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
Character.isBmpCodePoint(cp) -> {
|
||||||
|
for (b in ch.toString().encodeToByteArray()) {
|
||||||
|
out.appendEncodedByte(b.toInt())
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
Character.isSupplementaryCodePoint(cp) -> {
|
||||||
|
val high = Character.highSurrogateOf(cp)
|
||||||
|
val low = Character.lowSurrogateOf(cp)
|
||||||
|
for (b in charArrayOf(high, low).concatToString().encodeToByteArray()) {
|
||||||
|
out.appendEncodedByte(b.toInt())
|
||||||
|
}
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out?.toString() ?: source
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Unicode code point at the specified index.
|
||||||
|
*
|
||||||
|
* The `index` parameter is the regular `CharSequence` index, i.e. the number of `Char`s from the start of the character
|
||||||
|
* sequence.
|
||||||
|
*
|
||||||
|
* If the code point at the specified index is part of the Basic Multilingual Plane (BMP), its value can be represented
|
||||||
|
* using a single `Char` and this method will behave exactly like [CharSequence.get].
|
||||||
|
* Code points outside the BMP are encoded using a surrogate pair – a `Char` containing a value in the high surrogate
|
||||||
|
* range followed by a `Char` containing a value in the low surrogate range. Together these two `Char`s encode a single
|
||||||
|
* code point in one of the supplementary planes. This method will do the necessary decoding and return the value of
|
||||||
|
* that single code point.
|
||||||
|
*
|
||||||
|
* In situations where surrogate characters are encountered that don't form a valid surrogate pair starting at `index`,
|
||||||
|
* this method will return the surrogate code point itself, behaving like [CharSequence.get].
|
||||||
|
*
|
||||||
|
* If the `index` is out of bounds of this character sequence, this method throws an [IndexOutOfBoundsException].
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* // Text containing code points outside the BMP (encoded as a surrogate pairs)
|
||||||
|
* val text = "\uD83E\uDD95\uD83E\uDD96"
|
||||||
|
*
|
||||||
|
* var index = 0
|
||||||
|
* while (index < text.length) {
|
||||||
|
* val codePoint = text.codePointAt(index)
|
||||||
|
* // (Do something with codePoint...)
|
||||||
|
* index += CodePoints.charCount(codePoint)
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
private fun CharSequence.codePointAt(index: Int): Int {
|
||||||
|
if (index !in indices) throw IndexOutOfBoundsException("index $index was not in range $indices")
|
||||||
|
|
||||||
|
val firstChar = this[index]
|
||||||
|
if (firstChar.isHighSurrogate()) {
|
||||||
|
val nextChar = getOrNull(index + 1)
|
||||||
|
if (nextChar?.isLowSurrogate() == true) {
|
||||||
|
return Character.toCodePoint(firstChar, nextChar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstChar.code
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2001-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
|
||||||
|
*
|
||||||
|
* https://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.jvm.JvmField
|
||||||
|
|
||||||
|
const val standardContent = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_."
|
||||||
|
|
||||||
|
val invalidContent = listOf("sdkjfh%", "sdkjfh%6", "sdkjfh%xx", "sdfjfh%-1")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of unencoded content paired with the encoded content.
|
||||||
|
*/
|
||||||
|
val decodedToEncoded = listOf(
|
||||||
|
TestData("a test &", "a%20test%20%26"),
|
||||||
|
TestData(
|
||||||
|
"!abcdefghijklmnopqrstuvwxyz%%ABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.~=",
|
||||||
|
"%21abcdefghijklmnopqrstuvwxyz%25%25ABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.%7E%3D"
|
||||||
|
),
|
||||||
|
TestData("%#okékÉȢ smile!😁", "%25%23ok%C3%A9k%C3%89%C8%A2%20smile%21%F0%9F%98%81"),
|
||||||
|
TestData("\uD808\uDC00\uD809\uDD00\uD808\uDF00\uD808\uDD00", "%F0%92%80%80%F0%92%94%80%F0%92%8C%80%F0%92%84%80"),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TestData(
|
||||||
|
@JvmField
|
||||||
|
val unencoded: String,
|
||||||
|
@JvmField
|
||||||
|
val encoded: String,
|
||||||
|
)
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2001-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
|
||||||
|
*
|
||||||
|
* https://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 kotlin.test.*
|
||||||
|
import kotlin.test.DefaultAsserter.assertEquals
|
||||||
|
import kotlin.test.DefaultAsserter.assertSame
|
||||||
|
|
||||||
|
class UrlEncoderUtilTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decodeURL() {
|
||||||
|
for ((unencoded, encoded) in decodedToEncoded) {
|
||||||
|
assertEquals(unencoded, decode(encoded))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decodeWithException() {
|
||||||
|
for (source in invalidContent) {
|
||||||
|
assertFailsWith<IllegalArgumentException>(
|
||||||
|
message = "decode($source)",
|
||||||
|
block = { decode(source) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decodeWhenNoneNeeded() {
|
||||||
|
assertSame(standardContent, decode(standardContent))
|
||||||
|
assertEquals("decode('')", decode(""), "")
|
||||||
|
assertEquals("decode(' ')", decode(" "), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decodeWithPlusToSpace() {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun encodeURL() {
|
||||||
|
for ((unencoded, encoded) in decodedToEncoded) {
|
||||||
|
assertEquals(encoded, encode(unencoded))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun encodeEmptyOrBlank() {
|
||||||
|
assertTrue(encode("", allow = "").isEmpty(), "encode('','')")
|
||||||
|
assertEquals("encode('')", encode(""), "")
|
||||||
|
assertEquals("encode(' ')", encode(" "), "%20")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun encodeWhenNoneNeeded() {
|
||||||
|
assertSame(encode(standardContent), standardContent)
|
||||||
|
assertSame("with empty allow", encode(standardContent, allow = ""), standardContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun encodeWithAllow() {
|
||||||
|
assertEquals("encode(x, =?)", "?test=a%20test", encode("?test=a test", allow = "=?"))
|
||||||
|
assertEquals("encode(aaa, a)", "aaa", encode("aaa", "a"))
|
||||||
|
assertEquals("encode(' ')", " ", encode(" ", " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun encodeWithSpaceToPlus() {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2001-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
|
||||||
|
*
|
||||||
|
* https://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 org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static net.thauvin.erik.urlencoder.TestDataKt.getDecodedToEncoded;
|
||||||
|
import static net.thauvin.erik.urlencoder.UrlEncoderUtil.decode;
|
||||||
|
import static net.thauvin.erik.urlencoder.UrlEncoderUtil.encode;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class UrlEncoderJavaTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void decodeURL() {
|
||||||
|
assertAll(
|
||||||
|
getDecodedToEncoded()
|
||||||
|
.stream()
|
||||||
|
.map(data ->
|
||||||
|
() -> assertEquals(data.unencoded, decode(data.encoded))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encodeURL() {
|
||||||
|
assertAll(
|
||||||
|
getDecodedToEncoded()
|
||||||
|
.stream()
|
||||||
|
.map(data ->
|
||||||
|
() -> assertEquals(data.encoded, encode(data.unencoded))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue