diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d0bae46 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,106 @@ +name: build + +on: + workflow_dispatch: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: + - ubuntu-20.04 + - macos-12 + - windows-2022 + variant: + - posix + include: + - os: windows-2022 + variant: cygwin + - os: windows-2022 + variant: cmd + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '11' + + - name: Setup Kotlin + uses: fwilhe2/setup-kotlin@main + with: + version: 1.7.21 + + - name: Install dependencies for ${{ runner.os }} + shell: bash + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + choco install zip + # Overwrite the WSL bash.exe + # cp /c/msys64/usr/bin/bash.exe /c/Windows/System32/bash.exe + mv /c/Windows/System32/bash.exe /c/Windows/System32/wsl-bash.exe + fi + + - name: Run tests for Posix (and MSYS on Windows) + if: matrix.variant == 'posix' + shell: bash + run: | + # For Windows this action is running MSYS Os type + + echo "OsType: $OSTYPE" + + chmod +x ./gradlew + + ./gradlew clean assemble test || { echo 'Compilation or Unit tests failed' ; exit 1; } + + if [[ "$OSTYPE" == "linux"* ]]; then + echo "Linux test..." + ./gradlew -DosType=$OSTYPE -DincludeTags='posix | linux' itest + elif [[ "$OSTYPE" == "darwin"* ]]; then + echo "MacOs test..." + ./gradlew -DosType=$OSTYPE -DincludeTags='posix | macos' itest + elif [[ "$OSTYPE" == "cygwin" ]]; then + echo "Cygwin test..." + ./gradlew -DosType=$OSTYPE -DincludeTags='posix | cygwin' itest + elif [[ "$OSTYPE" == "msys" ]]; then + echo "MSys test..." + ./gradlew -DosType=$OSTYPE -DincludeTags='posix | msys' itest + elif [[ "$OSTYPE" == "freebsd"* ]]; then + echo "FreeBsd test..." + ./gradlew -DosType=$OSTYPE -DincludeTags='posix' itest + else + echo "Unknown OS" + exit 1 + fi + + - name: Run tests specific for Windows (cmd shell) + if: matrix.variant == 'cmd' + shell: cmd + run: | + echo "Windows test..." + .\gradlew.bat -DosType=windows -DincludeTags="windows" clean assemble test itest + + - name: Install Cygwin (only Windows) + if: matrix.variant == 'cygwin' + uses: egor-tensin/setup-cygwin@v3 + + - name: Run tests specific for Cygwin + if: matrix.variant == 'cygwin' + shell: C:\tools\cygwin\bin\bash.exe --login --norc -eo pipefail -o igncr '{0}' + run: | + echo $OSTYPE + echo "Cygwin test..." + echo "Changing directory to $GITHUB_WORKSPACE ..." + cd $GITHUB_WORKSPACE + ./gradlew clean assemble test || { echo 'Compilation or Unit tests failed' ; exit 1; } + ./gradlew -DosType=$OSTYPE -DincludeTags='posix | cygwin' itest diff --git a/TODO.adoc b/TODO.adoc index 070f554..31ceed6 100644 --- a/TODO.adoc +++ b/TODO.adoc @@ -1,3 +1,18 @@ -= shell 0.6 features: += shell 0.6.0-SNAPSHOT features: -* Use https://github.com/JetBrains/pty4j[PTY4j] instead of ProcessBuilder to read color codes from input and append them to output +* Move shell test classes from kscript to the library +* dry-run, with test output provider +* printing of commands +* Command for encapsulating commands? +* Throw on exitCode != 0 and/or skipErrors +* verifier for different types like in assertk/assertj +* log masking (shallow - in printout and deep in stderr/stdout); masking list of the words; is the design with Streams valid for that? +* Encoding of special characters in output (\n, \, etc.) +* Interface for ShellExecutor (to allow mocking) +* Printing Output of commands +* Possibility of parametrization of commands (properties) + masking (for tests and interoperability) +* Silent mode () +* Use https://github.com/JetBrains/pty4j[PTY4j] instead of ProcessBuilder to read color codes from input and append them to output (https://stackoverflow.com/questions/45789329/java-capture-process-output-with-color) +* Providing different types of shell (e.g. 'sh' or 'zsh') and executing without shell +* Library for testing +* Ideas: Generic verifier? ShellExecutorBuilder? diff --git a/build.gradle.kts b/build.gradle.kts index d91c53c..37e65ec 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,51 +1,50 @@ -val kotlinVersion: String = "1.7.21" + +val kotlinVersion: String = "2.1.21" plugins { - kotlin("jvm") version "1.7.21" - id("com.adarshr.test-logger") version "3.2.0" + kotlin("jvm") version "2.1.21" + id("com.adarshr.test-logger") version "4.0.0" `maven-publish` signing + idea } repositories { + mavenLocal() mavenCentral() } group = "io.github.kscripting" -version = "0.5.2" +version = "0.6.0-SNAPSHOT" + +kotlin { + jvmToolchain(17) +} + +configurations.all { + resolutionStrategy.cacheDynamicVersionsFor(0, "seconds") + resolutionStrategy.cacheChangingModulesFor(0, "seconds") +} + +/* sourceSets { - create("integration") { -// test { //With that idea can understand that 'integration' is test source set and do not complain about test -// names starting with upper case, but it doesn't compile correctly with it - java.srcDir("$projectDir/src/integration/kotlin") - resources.srcDir("$projectDir/src/integration/resources") + create("itest") { + kotlin.srcDir("$projectDir/src/itest/kotlin") + resources.srcDir("$projectDir/src/itest/resources") + compileClasspath += main.get().output + test.get().output runtimeClasspath += main.get().output + test.get().output } -// } } configurations { - get("integrationImplementation").apply { extendsFrom(get("testImplementation")) } + get("itestImplementation").apply { extendsFrom(get("testImplementation")) } } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) - } - - withJavadocJar() - withSourcesJar() -} -tasks.withType().all { - kotlinOptions { - jvmTarget = "1.8" - } -} -tasks.create("integration") { +tasks.create("itest") { val itags = System.getProperty("includeTags") ?: "" val etags = System.getProperty("excludeTags") ?: "" @@ -65,11 +64,10 @@ tasks.create("integration") { description = "Runs the integration tests." group = "verification" - testClassesDirs = sourceSets["integration"].output.classesDirs - classpath = sourceSets["integration"].runtimeClasspath + testClassesDirs = sourceSets["itest"].output.classesDirs + classpath = sourceSets["itest"].runtimeClasspath outputs.upToDateWhen { false } - mustRunAfter(tasks["test"]) - //dependsOn(tasks["assemble"], tasks["test"]) + dependsOn(tasks["assemble"]) doLast { println("Include tags: $itags") @@ -79,10 +77,31 @@ tasks.create("integration") { tasks.create("printIntegrationClasspath") { doLast { - println(sourceSets["integration"].runtimeClasspath.asPath) + println(sourceSets["itest"].runtimeClasspath.asPath) } } +idea { + module { + testSources.from(sourceSets["itest"].kotlin.srcDirs) + } +} + +val testToolsJar by tasks.registering(Jar::class) { + //archiveFileName.set("eulenspiegel-testHelpers-$version.jar") + archiveClassifier.set("test") + include("io/github/kscripting/shell/integration/tools/*") + from(sourceSets["itest"].output) +} + + + +*/ +*/ + +//val publishArtifact = artifacts.add("archives", testToolsJar) +val publishArtifact = artifacts.add("archives", tasks.jar) //TODO: above line is correct - current should be removed + testlogger { showStandardStreams = true showFullStackTraces = false @@ -92,35 +111,42 @@ tasks.test { useJUnitPlatform() } +val licencesSpec = Action { + license { + name.set("MIT License") + url.set("https://opensource.org/licenses/MIT") + } +} + +val developersSpec = Action { + developer { + id.set("aartiPl") + name.set("Marcin Kuszczak") + email.set("aarti@interia.pl") + } +} + +val scmSpec = Action { + connection.set("scm:git:git://https://github.com/kscripting/shell.git") + developerConnection.set("scm:git:ssh:https://github.com/kscripting/shell.git") + url.set("https://github.com/kscripting/shell") +} + publishing { publications { - create("mavenJava") { + create("shell") { artifactId = "shell" from(components["java"]) + artifact(publishArtifact) pom { - name.set("kscript") + name.set("shell") description.set("Shell - library for interoperability with different system shells") url.set("https://github.com/kscripting/shell") - licenses { - license { - name.set("MIT License") - url.set("https://opensource.org/licenses/MIT") - } - } - developers { - developer { - id.set("aartiPl") - name.set("Marcin Kuszczak") - email.set("aarti@interia.pl") - } - } - scm { - connection.set("scm:git:git://https://github.com/kscripting/shell.git") - developerConnection.set("scm:git:ssh:https://github.com/kscripting/shell.git") - url.set("https://github.com/kscripting/shell") - } + licenses(licencesSpec) + developers(developersSpec) + scm(scmSpec) } } } @@ -140,28 +166,27 @@ publishing { } signing { - sign(publishing.publications["mavenJava"]) + sign(publishing.publications["shell"]) } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") - implementation("org.jetbrains.kotlin:kotlin-scripting-common:$kotlinVersion") - implementation("org.jetbrains.kotlin:kotlin-scripting-jvm:$kotlinVersion") - implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies-maven-all:$kotlinVersion") - implementation("io.arrow-kt:arrow-core:1.1.2") - implementation("org.apache.commons:commons-lang3:3.12.0") - - implementation("org.slf4j:slf4j-nop:2.0.4") + api("net.igsoft:typeutils:0.7.0-SNAPSHOT") - testImplementation("org.junit.platform:junit-platform-suite-engine:1.9.0") - testImplementation("org.junit.platform:junit-platform-suite-api:1.9.0") - testImplementation("org.junit.platform:junit-platform-suite-commons:1.9.0") - testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.0") - testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.0") - testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25") + implementation("org.apache.commons:commons-lang3:3.12.0") + implementation("org.slf4j:slf4j-nop:2.0.5") + + val junitPlatformVersion = "1.13.1" + val junitEngineVersion = "5.13.1" + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("org.junit.platform:junit-platform-suite-engine:${junitPlatformVersion}") + testImplementation("org.junit.platform:junit-platform-suite-api:${junitPlatformVersion}") + testImplementation("org.junit.platform:junit-platform-suite-commons:${junitPlatformVersion}") + testImplementation("org.junit.jupiter:junit-jupiter:$junitEngineVersion") + testImplementation("org.junit.jupiter:junit-jupiter-params:$junitEngineVersion") + testImplementation("com.willowtreeapps.assertk:assertk:0.28.1") testImplementation("io.mockk:mockk:1.13.2") testImplementation(kotlin("script-runtime")) diff --git a/src/doEcho b/src/doEcho new file mode 100644 index 0000000..c9de237 --- /dev/null +++ b/src/doEcho @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +if [[ "-f" = "$1" ]]; then + cat $2 + exit +fi + +echo "$1" diff --git a/src/doEcho.bat b/src/doEcho.bat new file mode 100644 index 0000000..816a0ef --- /dev/null +++ b/src/doEcho.bat @@ -0,0 +1,8 @@ +@echo off + +IF "-f"=="%1" ( + type %2 + exit +) + +echo %1 diff --git a/src/itest/kotlin/io/github/kscripting/shell/integration/ShellExecutorTest.kt b/src/itest/kotlin/io/github/kscripting/shell/integration/ShellExecutorTest.kt new file mode 100644 index 0000000..26b9c29 --- /dev/null +++ b/src/itest/kotlin/io/github/kscripting/shell/integration/ShellExecutorTest.kt @@ -0,0 +1,63 @@ +package io.github.kscripting.shell.integration + +import io.github.kscripting.shell.integration.tools.ShellTestBase +import io.github.kscripting.shell.integration.tools.TestContext +import io.github.kscripting.shell.integration.tools.TestContext.execPath +import io.github.kscripting.shell.integration.tools.TestContext.testPath +import io.github.kscripting.shell.util.Sanitizer +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ShellExecutorTest : ShellTestBase { + + @BeforeAll + fun setup() { + + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Simple echo of parameters works`() { + verify("doEcho test", 0, "test[nl]", "") + } + + @Test + @Tag("posix") + @Tag("windows") + fun `Unicode characters output works`() { + val path = testPath.resolve("unicodeOutput.txt") + verify( + "doEcho -f $path", + 0, + path.readText(), + "", + inputSanitizer = Sanitizer.EMPTY_SANITIZER, + outputSanitizer = Sanitizer.EMPTY_SANITIZER + ) + } + + @Test + @Tag("posix") + fun `Call command utilizing input stream`() { + verify("read INPUT; echo \$INPUT", 0, "Input to READ[nl]", "", inputStream = "Input to READ".byteInputStream()) + } + +// @Test +// @Tag("windows") +// fun `Call command utilizing input stream (windows)`() { +// verify("set /p INPUT=\"\" && echo %INPUT%", 0, "Input to READ[nl]", "", inputStream = "Input to READ".byteInputStream()) +// } + + companion object { + init { + TestContext.copyFile("src/doEcho", execPath) + TestContext.copyFile("src/doEcho.bat", execPath) + TestContext.copyFile("src/unicodeOutput.txt", testPath) + Thread.sleep(1000) //It takes time to copy and set executable bit [tests fail without this] + } + } +} diff --git a/src/itest/kotlin/io/github/kscripting/shell/integration/tools/ShellTestBase.kt b/src/itest/kotlin/io/github/kscripting/shell/integration/tools/ShellTestBase.kt new file mode 100644 index 0000000..d1330bf --- /dev/null +++ b/src/itest/kotlin/io/github/kscripting/shell/integration/tools/ShellTestBase.kt @@ -0,0 +1,82 @@ +package io.github.kscripting.shell.integration.tools + +import io.github.kscripting.shell.model.ProcessResult +import io.github.kscripting.shell.process.EnvAdjuster +import io.github.kscripting.shell.util.Sanitizer +import org.junit.jupiter.api.BeforeAll +import java.io.InputStream + +interface ShellTestBase { + fun genericEquals(value: T) = GenericEquals(value) + + fun any() = AnyMatch() + fun eq(string: String, ignoreCase: Boolean = false) = Equals(string, ignoreCase) + fun startsWith(string: String, ignoreCase: Boolean = false) = StartsWith(string, ignoreCase) + fun contains(string: String, ignoreCase: Boolean = false) = Contains(string, ignoreCase) + + fun verify( + command: String, + exitCode: Int = 0, + stdOut: TestMatcher, + stdErr: String = "", + inputSanitizer: Sanitizer? = null, + outputSanitizer: Sanitizer? = null, + inputStream: InputStream? = null, + envAdjuster: EnvAdjuster = {} + ): ProcessResult = + verify(command, exitCode, stdOut, eq(stdErr), inputSanitizer, outputSanitizer, inputStream, envAdjuster) + + fun verify( + command: String, + exitCode: Int = 0, + stdOut: String, + stdErr: TestMatcher, + inputSanitizer: Sanitizer? = null, + outputSanitizer: Sanitizer? = null, + inputStream: InputStream? = null, + envAdjuster: EnvAdjuster = {} + ): ProcessResult = + verify(command, exitCode, eq(stdOut), stdErr, inputSanitizer, outputSanitizer, inputStream, envAdjuster) + + fun verify( + command: String, + exitCode: Int = 0, + stdOut: String = "", + stdErr: String = "", + inputSanitizer: Sanitizer? = null, + outputSanitizer: Sanitizer? = null, + inputStream: InputStream? = null, + envAdjuster: EnvAdjuster = {} + ): ProcessResult = + verify(command, exitCode, eq(stdOut), eq(stdErr), inputSanitizer, outputSanitizer, inputStream, envAdjuster) + + fun verify( + command: String, + exitCode: Int = 0, + stdOut: TestMatcher, + stdErr: TestMatcher, + inputSanitizer: Sanitizer? = null, + outputSanitizer: Sanitizer? = null, + inputStream: InputStream? = null, + envAdjuster: EnvAdjuster = {} + ): ProcessResult { + val processResult = runProcess(command, inputSanitizer, outputSanitizer, inputStream, envAdjuster) + println(processResult) + + val extCde = genericEquals(exitCode) + + extCde.checkAssertion("ExitCode", processResult.exitCode) + stdOut.checkAssertion("StdOut", processResult.stdout) + stdErr.checkAssertion("StdErr", processResult.stderr) + println() + + return processResult + } + + companion object : ShellTestCompanionBase() { + @BeforeAll + @JvmStatic + fun setUp() { + } + } +} diff --git a/src/itest/kotlin/io/github/kscripting/shell/integration/tools/ShellTestCompanionBase.kt b/src/itest/kotlin/io/github/kscripting/shell/integration/tools/ShellTestCompanionBase.kt new file mode 100644 index 0000000..836aaff --- /dev/null +++ b/src/itest/kotlin/io/github/kscripting/shell/integration/tools/ShellTestCompanionBase.kt @@ -0,0 +1,41 @@ +package io.github.kscripting.shell.integration.tools + +import io.github.kscripting.shell.ShellExecutor +import io.github.kscripting.shell.model.ProcessResult +import io.github.kscripting.shell.process.EnvAdjuster +import io.github.kscripting.shell.util.Sanitizer +import java.io.InputStream + +abstract class ShellTestCompanionBase { + open fun commonEnvAdjuster(specificEnvAdjuster: EnvAdjuster = {}): EnvAdjuster { + return { map -> + map[TestContext.pathEnvVariableName] = TestContext.pathEnvVariableCalculatedPath + specificEnvAdjuster(map) + } + } + + open fun runProcess( + command: String, + inputSanitizer: Sanitizer?, + outputSanitizer: Sanitizer?, + inputStream: InputStream?, + envAdjuster: EnvAdjuster + ): ProcessResult { + //In MSYS all quotes should be single quotes, otherwise content is interpreted e.g. backslashes. + //(MSYS bash interpreter is also replacing double quotes into the single quotes: see: bash -xc 'kscript "println(1+1)"') + val newCommand = when { + TestContext.osType.isPosixHostedOnWindows() -> command.replace('"', '\'') + else -> command + } + + return ShellExecutor.evalAndGobble( + newCommand, + TestContext.osType, + null, + inputSanitizer = inputSanitizer ?: TestContext.defaultInputSanitizer, + outputSanitizer = outputSanitizer ?: TestContext.defaultOutputSanitizer, + inputStream = inputStream, + envAdjuster = commonEnvAdjuster(envAdjuster) + ) + } +} diff --git a/src/itest/kotlin/io/github/kscripting/shell/integration/tools/TestContext.kt b/src/itest/kotlin/io/github/kscripting/shell/integration/tools/TestContext.kt new file mode 100644 index 0000000..51427dd --- /dev/null +++ b/src/itest/kotlin/io/github/kscripting/shell/integration/tools/TestContext.kt @@ -0,0 +1,55 @@ +package io.github.kscripting.shell.integration.tools + +import io.github.kscripting.os.Os +import io.github.kscripting.os.WindowsVfs +import io.github.kscripting.os.instance.WindowsOs +import io.github.kscripting.os.model.* +import io.github.kscripting.shell.ShellExecutor +import io.github.kscripting.shell.util.Sanitizer +import net.igsoft.typeutils.globalcontext.GlobalContext + +@Suppress("MemberVisibilityCanBePrivate") +object TestContext { + val windowsVfs = WindowsVfs( "/home/admin") + + val osType: OsType = GlobalOsType.find(System.getProperty("osType")) ?: GlobalOsType.native + + val projectPath: OsPath<*> = OsPath(GlobalOsType.native, System.getProperty("projectPath")).toHosted(osType) + val execPath: OsPath<*> = projectPath.resolve("build/shell_test/bin") + val testPath: OsPath<*> = projectPath.resolve("build/shell_test/tmp") + + val pathEnvVariableName = if (osType.isWindowsLike()) "Path" else "PATH" + val pathEnvVariableValue: String = System.getenv()[pathEnvVariableName]!! + val pathEnvVariableSeparator: String = if (osType.isWindowsLike() || osType.isPosixHostedOnWindows()) ";" else ":" + val pathEnvVariableCalculatedPath: String = + "${execPath.toHosted(osType).path}$pathEnvVariableSeparator$pathEnvVariableValue" + + val nl: String = when { + osType.isPosixHostedOnWindows() -> "\n" + else -> System.getProperty("line.separator") + } + + val defaultInputSanitizer = Sanitizer(listOf("[bs]" to "\\", "[nl]" to nl, "[tb]" to "\t")) + val defaultOutputSanitizer = defaultInputSanitizer.swapped() + + init { + println("osType : $osType") + println("nativeType : ${GlobalOsType.native}") + println("projectDir : $projectPath") + println("execDir : ${execPath.toHosted(osType)}") + println("Kotlin version : ${ShellExecutor.evalAndGobble("kotlin -version", osType).stdout}") + println("Env path : $pathEnvVariableCalculatedPath") + + execPath.createDirectories() + } + + fun copyFile(source: String, target: OsPath) { + val sourceFile = projectPath.resolve(source).toNativeFile() + val targetFile = target.resolve(sourceFile.name).toNativeFile() + + sourceFile.copyTo(targetFile, overwrite = true) + if (target == execPath) { + targetFile.setExecutable(true) + } + } +} diff --git a/src/itest/kotlin/io/github/kscripting/shell/integration/tools/TestMatcher.kt b/src/itest/kotlin/io/github/kscripting/shell/integration/tools/TestMatcher.kt new file mode 100644 index 0000000..c60d131 --- /dev/null +++ b/src/itest/kotlin/io/github/kscripting/shell/integration/tools/TestMatcher.kt @@ -0,0 +1,49 @@ +package io.github.kscripting.shell.integration.tools + +import org.opentest4j.AssertionFailedError + +abstract class TestMatcher(protected val expectedValue: T, private val expressionName: String) { + abstract fun matches(value: T): Boolean + + fun checkAssertion(assertionName: String, value: T) { + if (matches(value)) { + return + } + + throw AssertionFailedError( + """| + |Expected that '$assertionName' value: + |'${value.toString()}' + |$expressionName + |'${expectedValue.toString()}' + | + |""".trimMargin() + ) + } +} + +class GenericEquals(expectedValue: T) : TestMatcher(expectedValue, "is equal to") { + override fun matches(value: T): Boolean = (value == expectedValue) +} + +class AnyMatch : TestMatcher("", "has any value") { + override fun matches(value: String): Boolean = true +} + +class Equals(private val expectedString: String, private val ignoreCase: Boolean) : + TestMatcher(expectedString, "is equal to") { + override fun matches(value: String): Boolean = + value.equals(expectedString, ignoreCase) +} + +class StartsWith(private val expectedString: String, private val ignoreCase: Boolean) : + TestMatcher(expectedString, "starts with") { + override fun matches(value: String): Boolean = + value.startsWith(expectedString, ignoreCase) +} + +class Contains(private val expectedString: String, private val ignoreCase: Boolean) : + TestMatcher(expectedString, "contains") { + override fun matches(value: String): Boolean = + value.contains(expectedString, ignoreCase) +} diff --git a/src/main/kotlin/io/github/kscripting/os/Os.kt b/src/main/kotlin/io/github/kscripting/os/Os.kt new file mode 100644 index 0000000..c6d167d --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/Os.kt @@ -0,0 +1,18 @@ +package io.github.kscripting.os + +import net.igsoft.typeutils.marker.AutoTypedMarker + +interface Os { + //LINUX("linux"), MACOS("darwin"), WINDOWS("windows"), CYGWIN("cygwin"), MSYS("msys"), FREEBSD("freebsd"); + // Exact comparison (it.osName.equals(name, true)) seems to be not feasible as there is also e.g. "darwin21" + // "darwin19", "linux-musl" (for Docker Alpine), "linux-gnu" and maybe even other osTypes. But it seems that + // startsWith() covers all cases. + val marker: AutoTypedMarker + val osTypePrefix: String + + val type: OsType + + //val environment: Map + //val systemProperties: Map + val vfs: Vfs +} diff --git a/src/main/kotlin/io/github/kscripting/os/OsBuilder.kt b/src/main/kotlin/io/github/kscripting/os/OsBuilder.kt new file mode 100644 index 0000000..adc4ec1 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/OsBuilder.kt @@ -0,0 +1,25 @@ +package io.github.kscripting.os + +import org.apache.commons.lang3.SystemUtils + +class OsBuilder { + + fun build(): Os { + TODO() + + } + + + val native: OsType = guessNativeType() + +// fun findByOsTypeString(osTypeString: String): OsTypeNew? = +// GlobalOsType.find { osTypeString.startsWith(it.os.osTypePrefix, true) } + + private fun guessNativeType(): OsType = when { + SystemUtils.IS_OS_MAC -> OsType.MACOS + SystemUtils.IS_OS_WINDOWS -> OsType.WINDOWS + SystemUtils.IS_OS_FREE_BSD -> OsType.FREEBSD + else -> OsType.LINUX + } + +} diff --git a/src/main/kotlin/io/github/kscripting/os/OsProvider.kt b/src/main/kotlin/io/github/kscripting/os/OsProvider.kt new file mode 100644 index 0000000..d6d4904 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/OsProvider.kt @@ -0,0 +1,80 @@ +package io.github.kscripting.os + +import io.github.kscripting.os.instance.LinuxOs +import net.igsoft.typeutils.marker.AutoTypedMarker + +interface OsProvider { + val native: Os + fun provide(marker: AutoTypedMarker = native.marker): Os + + companion object { + private val defaultOsProvider = DefaultOsProvider() + private val providerStack = java.util.ArrayDeque() + + fun push(osProvider: OsProvider) { + synchronized(providerStack) { + providerStack.push(osProvider) + } + } + + fun poll() { + synchronized(providerStack) { + providerStack.poll() + } + } + + + val current: OsProvider get() { + synchronized(providerStack) { + return providerStack.peek() ?: defaultOsProvider + } + } + } +} + +@Synchronized +fun osContext(block: (OsProvider) -> Unit) { + block(OsProvider.current) +} + +@Synchronized +fun osContext(osProvider: OsProvider, block: (OsProvider) -> Unit) { + OsProvider.push(osProvider) + block(osProvider) + OsProvider.poll() +} + + +class DefaultOsProvider : OsProvider { + override val native: Os = TODO() + + override fun provide(marker: AutoTypedMarker): Os { + TODO() + } +} + + +class MockProvider : OsProvider { + override val native: Os + get() = TODO("Not yet implemented") + + override fun provide(marker: AutoTypedMarker): Os { + TODO("Not yet implemented") + } +} + +fun main() { + osContext { + val os = it.provide(LinuxOs.marker) + } + + osContext(MockProvider()) { + val os = it.provide(LinuxOs.marker) + + osContext { + val subOs = it.provide(LinuxOs.marker) + } + } + + OsProvider.current.provide(LinuxOs.marker) +} diff --git a/src/main/kotlin/io/github/kscripting/os/OsType.kt b/src/main/kotlin/io/github/kscripting/os/OsType.kt new file mode 100644 index 0000000..ea7fd96 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/OsType.kt @@ -0,0 +1,11 @@ +package io.github.kscripting.os + +enum class OsType { + LINUX, WINDOWS, CYGWIN, MSYS, MACOS, FREEBSD; + + fun isPosixLike() = + (this == LINUX || this == MACOS || this == FREEBSD || this == CYGWIN || this == MSYS) + + fun isPosixHostedOnWindows() = (this == CYGWIN || this == MSYS) + fun isWindowsLike() = (this == WINDOWS) +} diff --git a/src/main/kotlin/io/github/kscripting/os/Vfs.kt b/src/main/kotlin/io/github/kscripting/os/Vfs.kt new file mode 100644 index 0000000..be6a3b8 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/Vfs.kt @@ -0,0 +1,25 @@ +package io.github.kscripting.os + +import io.github.kscripting.os.model.OsPath + + +interface Vfs { + val type: OsType + val pathSeparator: String + val userHome: OsPath + + //Relaxed validation: + //1. It doesn't matter if there is '/' or '\' used as path separator - both are treated the same + //2. Duplicated or trailing slashes '/' and backslashes '\' are ignored + fun createOsPath(path: String): OsPath + fun createOsPath(root: String, pathParts: List): OsPath = + createOsPath(root.trim() + "/" + pathParts.joinToString("/")) + + fun createOsPath(pathParts: List): OsPath = + createOsPath(pathParts.joinToString("/")) + + fun createOsPath(vararg pathParts: String): OsPath = + createOsPath(pathParts.joinToString("/")) + + fun isValid(path: String): Result +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/CygwinOs.kt b/src/main/kotlin/io/github/kscripting/os/instance/CygwinOs.kt new file mode 100644 index 0000000..f01cba5 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/CygwinOs.kt @@ -0,0 +1,10 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.OsType +import net.igsoft.typeutils.marker.AutoTypedMarker + +class CygwinOs(override val vfs: CygwinVfs, override val nativeOs: WindowsOs) : HostedOs { + override val marker: AutoTypedMarker = AutoTypedMarker.create("Cygwin") + override val type: OsType = OsType.CYGWIN + override val osTypePrefix: String = "cygwin" +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/CygwinVfs.kt b/src/main/kotlin/io/github/kscripting/os/instance/CygwinVfs.kt new file mode 100644 index 0000000..dca532e --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/CygwinVfs.kt @@ -0,0 +1,45 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.OsType +import io.github.kscripting.os.Vfs +import io.github.kscripting.os.util.createPosixOsPath +import io.github.kscripting.os.model.OsPath +import io.github.kscripting.os.util.toHostedConverter + +class CygwinVfs(override val nativeFsRoot: OsPath, userHome: String) : HostedVfs, + PosixVfs(OsType.CYGWIN) { + + override fun toNative(providedOsPath: OsPath): OsPath { + val osPath = providedOsPath + val newParts = mutableListOf() + var newRoot = "" + + if (osPath.isAbsolute) { + if (osPath.pathParts[0].equals("cygdrive", true)) { //Paths referring /cygdrive + newRoot = osPath.pathParts[1] + ":\\" + newParts.addAll(osPath.pathParts.subList(2, osPath.pathParts.size)) + } else if (osPath.root == "~") { //Paths starting with ~ + newRoot = this.nativeFsRoot.root + newParts.addAll(this.nativeFsRoot.pathParts) + newParts.addAll(this.userHome.pathParts) + newParts.addAll(osPath.pathParts) + } else { //Any other path like: /usr/bin + newRoot = this.nativeFsRoot.root + newParts.addAll(this.nativeFsRoot.pathParts) + newParts.addAll(osPath.pathParts) + } + } else { + newParts.addAll(osPath.pathParts) + } + + return OsPath(nativeFsRoot.vfs, newRoot, newParts) + } + + override fun toHosted(osPath: OsPath): OsPath { + return toHostedConverter(this, osPath) + } + + override val userHome: OsPath = createPosixOsPath(this, userHome) + override fun createOsPath(path: String): OsPath = createPosixOsPath(this, path) +} + diff --git a/src/main/kotlin/io/github/kscripting/os/instance/FreeBsdOs.kt b/src/main/kotlin/io/github/kscripting/os/instance/FreeBsdOs.kt new file mode 100644 index 0000000..f8cb485 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/FreeBsdOs.kt @@ -0,0 +1,11 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.Os +import io.github.kscripting.os.OsType +import net.igsoft.typeutils.marker.AutoTypedMarker + +class FreeBsdOs(override val vfs: FreeBsdVfs) : Os { + override val marker: AutoTypedMarker = AutoTypedMarker.create("FreeBsd") + override val type: OsType = OsType.FREEBSD + override val osTypePrefix: String = "freebsd" +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/FreeBsdVfs.kt b/src/main/kotlin/io/github/kscripting/os/instance/FreeBsdVfs.kt new file mode 100644 index 0000000..402adc8 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/FreeBsdVfs.kt @@ -0,0 +1,10 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.OsType +import io.github.kscripting.os.util.createPosixOsPath +import io.github.kscripting.os.model.OsPath + +class FreeBsdVfs(userHome: String) : PosixVfs(OsType.FREEBSD) { + override val userHome: OsPath = createOsPath(userHome) + override fun createOsPath(path: String): OsPath = createPosixOsPath(this, path) +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/HostedOs.kt b/src/main/kotlin/io/github/kscripting/os/instance/HostedOs.kt new file mode 100644 index 0000000..cbb50f0 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/HostedOs.kt @@ -0,0 +1,7 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.Os + +interface HostedOs : Os { + val nativeOs: Os +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/HostedVfs.kt b/src/main/kotlin/io/github/kscripting/os/instance/HostedVfs.kt new file mode 100644 index 0000000..a6d52dd --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/HostedVfs.kt @@ -0,0 +1,11 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.Vfs +import io.github.kscripting.os.model.OsPath + +interface HostedVfs : Vfs { + val nativeFsRoot: OsPath + + fun toNative(osPath: OsPath): OsPath + fun toHosted(osPath: OsPath): OsPath +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/LinuxOs.kt b/src/main/kotlin/io/github/kscripting/os/instance/LinuxOs.kt new file mode 100644 index 0000000..f6b6dc2 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/LinuxOs.kt @@ -0,0 +1,15 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.Os +import io.github.kscripting.os.OsType +import net.igsoft.typeutils.marker.AutoTypedMarker + +class LinuxOs(override val vfs: LinuxVfs) : Os { + override val marker: AutoTypedMarker = AutoTypedMarker.create("Linux") + override val type: OsType = OsType.LINUX + override val osTypePrefix: String = "linux" + + companion object { + val marker: AutoTypedMarker = AutoTypedMarker.create("Linux") + } +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/LinuxVfs.kt b/src/main/kotlin/io/github/kscripting/os/instance/LinuxVfs.kt new file mode 100644 index 0000000..8d9b5e1 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/LinuxVfs.kt @@ -0,0 +1,10 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.OsType +import io.github.kscripting.os.util.createPosixOsPath +import io.github.kscripting.os.model.OsPath + +class LinuxVfs(userHome: String) : PosixVfs(OsType.LINUX) { + override val userHome: OsPath = createOsPath(userHome) + override fun createOsPath(path: String): OsPath = createPosixOsPath(this, path) +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/MacOs.kt b/src/main/kotlin/io/github/kscripting/os/instance/MacOs.kt new file mode 100644 index 0000000..f106f5b --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/MacOs.kt @@ -0,0 +1,11 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.Os +import io.github.kscripting.os.OsType +import net.igsoft.typeutils.marker.AutoTypedMarker + +class MacOs(override val vfs: MacOsVfs) : Os { + override val marker: AutoTypedMarker = AutoTypedMarker.create("Mac") + override val type: OsType = OsType.MACOS + override val osTypePrefix: String = "darwin" +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/MacOsVfs.kt b/src/main/kotlin/io/github/kscripting/os/instance/MacOsVfs.kt new file mode 100644 index 0000000..724db38 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/MacOsVfs.kt @@ -0,0 +1,10 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.OsType +import io.github.kscripting.os.util.createPosixOsPath +import io.github.kscripting.os.model.OsPath + +class MacOsVfs(userHome: String) : PosixVfs(OsType.MACOS) { + override val userHome: OsPath = createOsPath(userHome) + override fun createOsPath(path: String): OsPath = createPosixOsPath(this, path) +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/MsysOs.kt b/src/main/kotlin/io/github/kscripting/os/instance/MsysOs.kt new file mode 100644 index 0000000..dcc71ca --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/MsysOs.kt @@ -0,0 +1,10 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.OsType +import net.igsoft.typeutils.marker.AutoTypedMarker + +class MsysOs(override val nativeOs: WindowsOs, override val vfs: MsysVfs) : HostedOs { + override val marker: AutoTypedMarker = AutoTypedMarker.create("Msys") + override val type: OsType = OsType.MSYS + override val osTypePrefix: String = "msys" +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/MsysVfs.kt b/src/main/kotlin/io/github/kscripting/os/instance/MsysVfs.kt new file mode 100644 index 0000000..3394521 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/MsysVfs.kt @@ -0,0 +1,42 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.OsType +import io.github.kscripting.os.Vfs +import io.github.kscripting.os.util.createPosixOsPath +import io.github.kscripting.os.model.OsPath +import io.github.kscripting.os.util.toHostedConverter + +class MsysVfs(override val nativeFsRoot: OsPath, userHome: String) : HostedVfs, PosixVfs(OsType.MSYS) { + override val userHome: OsPath = createOsPath(userHome) + override fun createOsPath(path: String): OsPath = createPosixOsPath(this, path) + + override fun toNative(providedOsPath: OsPath): OsPath { + val osPath = providedOsPath + val newParts = mutableListOf() + var newRoot = "" + + if (osPath.isAbsolute) { + if (osPath.pathParts[0].length == 1 && (osPath.pathParts[0][0].code in 65..90 || osPath.pathParts[0][0].code in 97..122)) { //Paths referring with drive letter at the beginning + newRoot = osPath.pathParts[0] + ":\\" + newParts.addAll(osPath.pathParts.subList(1, osPath.pathParts.size)) + } else if (osPath.root == "~") { //Paths starting with ~ + newRoot = this.nativeFsRoot.root + newParts.addAll(this.nativeFsRoot.pathParts) + newParts.addAll(this.userHome.pathParts) + newParts.addAll(osPath.pathParts) + } else { //Any other path like: /usr/bin + newRoot = this.nativeFsRoot.root + newParts.addAll(this.nativeFsRoot.pathParts) + newParts.addAll(osPath.pathParts) + } + } else { + newParts.addAll(osPath.pathParts) + } + + return OsPath(nativeFsRoot.vfs, newRoot, newParts) + } + + override fun toHosted(osPath: OsPath): OsPath { + return toHostedConverter(this, osPath) + } +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/PosixVfs.kt b/src/main/kotlin/io/github/kscripting/os/instance/PosixVfs.kt new file mode 100644 index 0000000..1ed0f66 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/PosixVfs.kt @@ -0,0 +1,22 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.OsType +import io.github.kscripting.os.Vfs + +abstract class PosixVfs(override val type: OsType) : Vfs { + override val pathSeparator: String = "/" + override fun isValid(path: String): Result { + // NOTE: https://stackoverflow.com/a/1311070/5321061 + for (char in path.withIndex()) { + if (char.value in INVALID_CHARS) { + return Result.failure(IllegalArgumentException("Invalid character '${char.value}' in path '$path'")) + } + } + + return Result.success(Unit) + } + + companion object { + private const val INVALID_CHARS = "\u0000" + } +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/WindowsOs.kt b/src/main/kotlin/io/github/kscripting/os/instance/WindowsOs.kt new file mode 100644 index 0000000..1c311ce --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/WindowsOs.kt @@ -0,0 +1,11 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.Os +import io.github.kscripting.os.OsType +import net.igsoft.typeutils.marker.AutoTypedMarker + +class WindowsOs(override val vfs: WindowsVfs) : Os { + override val marker: AutoTypedMarker = AutoTypedMarker.create("Windows") + override val type: OsType = OsType.WINDOWS + override val osTypePrefix: String = "windows" +} diff --git a/src/main/kotlin/io/github/kscripting/os/instance/WindowsVfs.kt b/src/main/kotlin/io/github/kscripting/os/instance/WindowsVfs.kt new file mode 100644 index 0000000..d12cd8c --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/instance/WindowsVfs.kt @@ -0,0 +1,51 @@ +package io.github.kscripting.os.instance + +import io.github.kscripting.os.OsType +import io.github.kscripting.os.Vfs +import io.github.kscripting.os.util.createFinalPath +import io.github.kscripting.os.model.OsPath + +class WindowsVfs(userHome: String) : Vfs { + override val type: OsType = OsType.WINDOWS + override val pathSeparator: String = "\\" + + //https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names + //The rule here is more strict than necessary, but it is at least good practice to follow such a rule. + private val windowsDriveRegex = + "^([a-zA-Z]:(?=[\\\\/])|\\\\\\\\(?:[^*:<>?\\\\/|]+\\\\[^*:<>?\\\\/|]+|\\?\\\\(?:[a-zA-Z]:(?=\\\\)|(?:UNC\\\\)?[^*:<>?\\\\/|]+\\\\[^*:<>?\\\\/|]+)))".toRegex() + + + override val userHome: OsPath = createOsPath(userHome) + + override fun createOsPath(path: String): OsPath { + isValid(path).getOrThrow() + + //Detect root + val root = when { + path.startsWith("~/") || path.startsWith("~\\") -> "~" + else -> { + val match = windowsDriveRegex.find(path) + val matchedRoot = match?.groupValues?.get(1) + if (matchedRoot != null) matchedRoot + "\\" else "" + } + } + + return createFinalPath(this, path, root) + } + + override fun isValid(path: String): Result { + val isAbsolute = path.length >= 2 && path[1] == ':' && path[0] != '\\' + + for(char in path.withIndex()) { + if (!(isAbsolute && char.index == 1) && char.value in INVALID_CHARS) { + return Result.failure(IllegalArgumentException("Invalid character '${char.value}' in path '$path'")) + } + } + + return Result.success(Unit) + } + + companion object { + private const val INVALID_CHARS = "<>:\"|?*" + } +} diff --git a/src/main/kotlin/io/github/kscripting/os/model/GlobalOsType.kt b/src/main/kotlin/io/github/kscripting/os/model/GlobalOsType.kt new file mode 100644 index 0000000..d25cdad --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/model/GlobalOsType.kt @@ -0,0 +1,48 @@ +package io.github.kscripting.os.model + +import io.github.kscripting.os.Os +import io.github.kscripting.os.instance.* +import net.igsoft.typeutils.globalcontext.GlobalContext +import net.igsoft.typeutils.marker.AutoTypedMarker +import net.igsoft.typeutils.marker.DefaultTypedMarker +import net.igsoft.typeutils.marker.TypedMarker +import net.igsoft.typeutils.typedenum.TypedEnumCompanion +import org.apache.commons.lang3.SystemUtils + + +class GlobalOsType private constructor(private val marker: TypedMarker) : DefaultTypedMarker(marker) { + val os: T get() = GlobalContext.getValue(marker) + + fun isPosixLike() = + (this == LINUX || this == MACOS || this == FREEBSD || this == CYGWIN || this == MSYS) + + fun isPosixHostedOnWindows() = (this == CYGWIN || this == MSYS) + fun isWindowsLike() = (this == WINDOWS) + + companion object : TypedEnumCompanion>() { + val LINUX: GlobalOsType = GlobalOsType(AutoTypedMarker.create()) + val WINDOWS: GlobalOsType = GlobalOsType(AutoTypedMarker.create()) + val CYGWIN: GlobalOsType = GlobalOsType(AutoTypedMarker.create()) + val MSYS: GlobalOsType = GlobalOsType(AutoTypedMarker.create()) + val MACOS: GlobalOsType = GlobalOsType(AutoTypedMarker.create()) + val FREEBSD: GlobalOsType = GlobalOsType(AutoTypedMarker.create()) + + val native: GlobalOsType = guessNativeType() + + fun findByOsTypeString(osTypeString: String): GlobalOsType? = + find { osTypeString.startsWith(it.os.osTypePrefix, true) } + + private fun guessNativeType(): GlobalOsType { + when { + SystemUtils.IS_OS_LINUX -> return LINUX + SystemUtils.IS_OS_MAC -> return MACOS + SystemUtils.IS_OS_WINDOWS -> return WINDOWS + SystemUtils.IS_OS_FREE_BSD -> return FREEBSD + } + + return LINUX + } + } + + override fun toString(): String = findName(this) +} diff --git a/src/main/kotlin/io/github/kscripting/os/model/OsPath.kt b/src/main/kotlin/io/github/kscripting/os/model/OsPath.kt new file mode 100644 index 0000000..6cb769f --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/model/OsPath.kt @@ -0,0 +1,23 @@ +package io.github.kscripting.os.model + +import io.github.kscripting.os.OsType +import io.github.kscripting.os.Vfs + +sealed interface OsPathError { + object EmptyPath : OsPathError, RuntimeException() + data class InvalidConversion(val errorMessage: String) : OsPathError, RuntimeException(errorMessage) +} + +//Path representation for different OSes +@Suppress("MemberVisibilityCanBePrivate") +//TODO: should be only instantiated from VFS, not in any place +data class OsPath(@Transient internal val vfs: Vfs, val root: String, val pathParts: List) { + val osType: OsType = vfs.type + val isRelative: Boolean get() = root.isEmpty() && pathParts.isNotEmpty() + val isAbsolute: Boolean get() = root.isNotEmpty() + + val path: String get() = root + pathParts.joinToString(vfs.pathSeparator) { it } + val leaf: String get() = if (pathParts.isEmpty()) root else pathParts.last() + + override fun toString(): String = "$path [$osType]" +} diff --git a/src/main/kotlin/io/github/kscripting/os/model/OsPathExt.kt b/src/main/kotlin/io/github/kscripting/os/model/OsPathExt.kt new file mode 100644 index 0000000..d81e73c --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/model/OsPathExt.kt @@ -0,0 +1,91 @@ +package io.github.kscripting.os.model + +import io.github.kscripting.os.Vfs +import java.io.File + +//import io.github.kscripting.os.Vfs +import io.github.kscripting.os.instance.HostedVfs +import io.github.kscripting.os.util.normalize +import java.net.URI +import java.nio.file.Path +import java.nio.file.Paths + +//import java.io.File +//import java.net.URI +//import java.nio.charset.Charset +//import java.nio.file.OpenOption +//import java.nio.file.Path +//import java.nio.file.Paths +//import kotlin.io.path.* +// +fun OsPath.toNative(): OsPath = (vfs as? HostedVfs)?.toNative(this) ?: this + + +fun List.startsWith(list: List): Boolean = (this.size >= list.size && this.subList(0, list.size) == list) + +fun OsPath.startsWith(osPath: OsPath): Boolean = root == osPath.root && pathParts.startsWith(osPath.pathParts) + + +//// Conversion to OsPath +//fun File.toOsPath(): OsPath = OsPath(GlobalOsType.native, absolutePath) +// +//fun Path.toOsPath(): OsPath = OsPath(GlobalOsType.native, absolutePathString()) +// +//fun URI.toOsPath(): OsPath = +// if (this.scheme == "file") File(this).toOsPath() else throw IllegalArgumentException("Invalid conversion from URL to OsPath") + + +//// Conversion from OsPath +fun OsPath.toNativePath(): Path = Paths.get(toNative().path) +fun OsPath.toNativeFile(): File = File(toNative().path) +fun OsPath.toNativeUri(): URI = File(toNative().path).toURI() + + +//// OsPath operations +//fun OsPath.exists(): Boolean = toNativePath().exists() +// +//fun OsPath.createDirectories(): OsPath = OsPath(nativeType, toNativePath().createDirectories().pathString) +// +//fun OsPath.copyTo(target: OsPath, overwrite: Boolean = false): OsPath = +// OsPath(nativeType, toNativePath().copyTo(target.toNativePath(), overwrite).pathString) +// +// +//fun OsPath.writeText(text: CharSequence, charset: Charset = Charsets.UTF_8, vararg options: OpenOption): Unit = +// toNativePath().writeText(text, charset, *options) +// +//fun OsPath.readText(charset: Charset = Charsets.UTF_8): String = toNativePath().readText(charset) +// + +operator fun OsPath.div(osPath: OsPath): OsPath = resolve(osPath) + +operator fun OsPath.div(path: String): OsPath = resolve(path) + +fun OsPath.resolve(vararg pathParts: String): OsPath = resolve(vfs.createOsPath(*pathParts)) + +fun OsPath.resolve(osPath: OsPath): OsPath { + if (osType != osPath.osType) { + throw IllegalArgumentException("Paths from different OS's: '${osType}' path can not be resolved with '${osPath.osType}' path") + } + + if (osPath.isAbsolute) { + throw IllegalArgumentException("Can not resolve absolute or relative path '${path}' using absolute path '${osPath.path}'") + } + + val newPathParts = buildList { + addAll(pathParts) + addAll(osPath.pathParts) + } + + return OsPath(vfs, root, normalize(root, newPathParts)) +} + +//// OsPath accessors +// +////val OsPath.rootOsPath +//// get() = OsPath.createOrThrow(osType, root) +// +//val OsPath.nativeType +// get() = if (osType.isPosixHostedOnWindows()) GlobalOsType.WINDOWS else osType +// +//val OsPath.extension +// get() = leaf.substringAfterLast('.', "") diff --git a/src/main/kotlin/io/github/kscripting/os/util/VfsUtil.kt b/src/main/kotlin/io/github/kscripting/os/util/VfsUtil.kt new file mode 100644 index 0000000..e691d7b --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/os/util/VfsUtil.kt @@ -0,0 +1,128 @@ +package io.github.kscripting.os.util + +import io.github.kscripting.os.Vfs +import io.github.kscripting.os.instance.CygwinVfs +import io.github.kscripting.os.instance.HostedVfs +import io.github.kscripting.os.model.OsPath +import io.github.kscripting.os.model.startsWith + + +fun createPosixOsPath(vfs: Vfs, path: String): OsPath { + vfs.isValid(path).getOrThrow() + + //Detect root + val root: String = when { + path.startsWith("~/") || path.startsWith("~\\") -> "~" + path.startsWith("/") -> "/" + else -> "" + } + + return createFinalPath(vfs, path, root) +} + +fun createFinalPath(vfs: Vfs, path: String, root: String): OsPath { + //Remove also empty path parts - there were duplicated or trailing slashes / backslashes in initial path + val pathWithoutRoot = path.drop(root.length) + val pathPartsResolved = pathWithoutRoot.split('/', '\\').filter { it.isNotBlank() } + return OsPath(vfs, root, normalize(root, pathPartsResolved)) +} + +fun toHostedConverter(vfs: T, osPath: OsPath): OsPath { + val newParts = mutableListOf() + var newRoot = "" + + if (osPath.isAbsolute) { + val nativeFsRoot = vfs.nativeFsRoot + + if (osPath.startsWith(nativeFsRoot)) { + if (osPath.pathParts.subList(nativeFsRoot.pathParts.size, osPath.pathParts.size) + .startsWith(vfs.userHome.pathParts) + ) { + //It is user home: ~ + newRoot = "~/" + newParts.addAll( + osPath.pathParts.subList( + nativeFsRoot.pathParts.size + vfs.userHome.pathParts.size, + osPath.pathParts.size + ) + ) + } else { + //It is hostedOs root: / + newRoot = "/" + newParts.addAll(osPath.pathParts.subList(nativeFsRoot.pathParts.size, osPath.pathParts.size)) + } + } else { + //Otherwise: + //root is like 'C:\' + val drive = osPath.root.dropLast(2).lowercase() + + newRoot = "/" + + //TODO: not generic!! + if (vfs is CygwinVfs) { + newParts.add("cygdrive") + } + + newParts.add(drive) + + newParts.addAll(osPath.pathParts) + } + } else { + newParts.addAll(osPath.pathParts) + } + + return OsPath(vfs, newRoot, newParts) +} + +fun normalize(root: String, pathParts: List): List { + //Relative: + // ./../ --> ../ + // ./a/../ --> ./ + // ./a/ --> ./a + // ../a --> ../a + // ../../a --> ../../a + + //Absolute: + // /../ --> invalid (above root) + // /a/../ --> / + + val isAbsolute = root.isNotEmpty() + val newParts = mutableListOf() + var index = 0 + + while (index < pathParts.size) { + if (pathParts[index] == ".") { + //Just skip . without adding it to newParts + } else if (pathParts[index] == "..") { + if (isAbsolute && newParts.size == 0) { + throw IllegalArgumentException("Path after normalization goes beyond root element: '${root}'") + } + + if (newParts.size > 0) { + when (newParts.last()) { + "." -> { + //It's the first element - other dots should be already removed before + newParts.removeAt(newParts.size - 1) + newParts.add("..") + } + + ".." -> { + newParts.add("..") + } + + else -> { + newParts.removeAt(newParts.size - 1) + } + } + } else { + newParts.add("..") + } + } else { + newParts.add(pathParts[index]) + } + + index += 1 + } + + return newParts +} diff --git a/src/main/kotlin/io/github/kscripting/shell/ShellExecutor.kt b/src/main/kotlin/io/github/kscripting/shell/ShellExecutor.kt index 1423eca..00ca2ff 100644 --- a/src/main/kotlin/io/github/kscripting/shell/ShellExecutor.kt +++ b/src/main/kotlin/io/github/kscripting/shell/ShellExecutor.kt @@ -1,91 +1,124 @@ package io.github.kscripting.shell -import io.github.kscripting.shell.model.GobbledProcessResult -import io.github.kscripting.shell.model.OsPath -import io.github.kscripting.shell.model.OsType +import io.github.kscripting.os.OsType +import io.github.kscripting.os.model.OsPath import io.github.kscripting.shell.model.ProcessResult +import io.github.kscripting.shell.model.ShellType import io.github.kscripting.shell.process.EnvAdjuster import io.github.kscripting.shell.process.ProcessRunner import io.github.kscripting.shell.process.ProcessRunner.DEFAULT_ERR_PRINTERS import io.github.kscripting.shell.process.ProcessRunner.DEFAULT_OUT_PRINTERS +import io.github.kscripting.shell.util.Sanitizer +import io.github.kscripting.shell.util.Sanitizer.Companion.EMPTY_SANITIZER import java.io.ByteArrayOutputStream +import java.io.InputStream import java.io.PrintStream import java.nio.charset.StandardCharsets +import java.util.regex.Pattern +@Suppress("MemberVisibilityCanBePrivate") object ShellExecutor { + private val SPLIT_PATTERN = Pattern.compile("([^\"]\\S*|\".+?\")\\s*") + private val UTF_8 = StandardCharsets.UTF_8.name() + private val DEFAULT_SHELL_MAPPER: Map = mapOf( + OsType.WINDOWS to ShellType.CMD, + OsType.LINUX to ShellType.BASH, + OsType.FREEBSD to ShellType.BASH, + OsType.MACOS to ShellType.BASH, + OsType.CYGWIN to ShellType.BASH, + OsType.MSYS to ShellType.BASH, + ) fun evalAndGobble( - osType: OsType, command: String, + osType: OsType, workingDirectory: OsPath? = null, - envAdjuster: EnvAdjuster = {}, waitTimeMinutes: Int = 10, inheritInput: Boolean = false, + inputSanitizer: Sanitizer = EMPTY_SANITIZER, + outputSanitizer: Sanitizer = inputSanitizer.swapped(), outPrinter: List = emptyList(), - errPrinter: List = emptyList() - ): GobbledProcessResult { + errPrinter: List = emptyList(), + inputStream: InputStream? = null, + shellMapper: Map = DEFAULT_SHELL_MAPPER, + envAdjuster: EnvAdjuster = {} + ): ProcessResult { val outStream = ByteArrayOutputStream(1024) val errStream = ByteArrayOutputStream(1024) - val utf8 = StandardCharsets.UTF_8.name() - - var result: ProcessResult + var result: Int - PrintStream(outStream, true, utf8).use { additionalOutPrinter -> - PrintStream(errStream, true, utf8).use { additionalErrPrinter -> + PrintStream(outStream, true, UTF_8).use { additionalOutPrinter -> + PrintStream(errStream, true, UTF_8).use { additionalErrPrinter -> result = eval( - osType, command, + osType, workingDirectory, - envAdjuster, waitTimeMinutes, inheritInput, + inputSanitizer, + outputSanitizer, outPrinter + additionalOutPrinter, - errPrinter + additionalErrPrinter + errPrinter + additionalErrPrinter, + inputStream, + shellMapper, + envAdjuster ) } } - return GobbledProcessResult(result.command, result.exitCode, outStream.toString(utf8), errStream.toString(utf8)) + return ProcessResult(result, outStream.toString(UTF_8), errStream.toString(UTF_8)) } fun eval( - osType: OsType, command: String, + osType: OsType, workingDirectory: OsPath? = null, - envAdjuster: EnvAdjuster = {}, waitTimeMinutes: Int = 10, inheritInput: Boolean = false, + inputSanitizer: Sanitizer = EMPTY_SANITIZER, + outputSanitizer: Sanitizer = inputSanitizer.swapped(), outPrinter: List = DEFAULT_OUT_PRINTERS, - errPrinter: List = DEFAULT_ERR_PRINTERS - ): ProcessResult { - //NOTE: cmd is an argument to shell (bash/cmd), so it should stay not split by whitespace as a single string - if (osType == OsType.WINDOWS) { - // if the first character in args in `cmd /c ` is a quote, cmd will remove it as well as the - // last quote character within args before processing the term, which removes our quotes. - return ProcessRunner.runProcess( - "cmd", - "/c", - " $command", - workingDirectory = workingDirectory, - envAdjuster = envAdjuster, - waitTimeMinutes = waitTimeMinutes, - inheritInput = inheritInput, - outPrinter = outPrinter, - errPrinter = errPrinter - ) + errPrinter: List = DEFAULT_ERR_PRINTERS, + inputStream: InputStream? = null, + shellMapper: Map = DEFAULT_SHELL_MAPPER, + envAdjuster: EnvAdjuster = {} + ): Int { + val sanitizedCommand = inputSanitizer.sanitize(command) + + val commandList = when (val shellType = shellMapper.getValue(osType)) { + //NOTE: usually command is an argument to shell (bash/cmd), so it should stay not split by whitespace as + //a single string, but when there is no shell, we have to split the command + ShellType.NONE -> { + val result = mutableListOf() + //Split by whitespace preserving spaces inside quotes + val matcher = SPLIT_PATTERN.matcher(sanitizedCommand) + while (matcher.find()) { + result.add(matcher.group(1)) // Add .replace("\"", "") to remove surrounding quotes. + } + result + } + + ShellType.CMD -> + // For Windows: if the first character in args in `cmd /c ` is a quote, cmd will remove it as well + // as the last quote character within args before processing the term, which removes our quotes. + // Empty character before command preserves quotes correctly. + shellType.executorCommand.toList() + " $sanitizedCommand" + + else -> + shellType.executorCommand.toList() + sanitizedCommand } return ProcessRunner.runProcess( - "bash", - "-c", - command, + commandList, workingDirectory = workingDirectory, envAdjuster = envAdjuster, waitTimeMinutes = waitTimeMinutes, inheritInput = inheritInput, + outputSanitizer = outputSanitizer, outPrinter = outPrinter, - errPrinter = errPrinter + errPrinter = errPrinter, + inputStream = inputStream ) } } diff --git a/src/main/kotlin/io/github/kscripting/shell/model/CommandTimeoutException.kt b/src/main/kotlin/io/github/kscripting/shell/model/CommandTimeoutException.kt new file mode 100644 index 0000000..8a80ee2 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/shell/model/CommandTimeoutException.kt @@ -0,0 +1,3 @@ +package io.github.kscripting.shell.model + +class CommandTimeoutException(override val message: String) : RuntimeException(message) diff --git a/src/main/kotlin/io/github/kscripting/shell/model/GobbledProcessResult.kt b/src/main/kotlin/io/github/kscripting/shell/model/GobbledProcessResult.kt deleted file mode 100644 index e28f545..0000000 --- a/src/main/kotlin/io/github/kscripting/shell/model/GobbledProcessResult.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.kscripting.shell.model - -import io.github.kscripting.shell.util.ShellEscapeUtils.whitespaceCharsToSymbols - -data class GobbledProcessResult(val command: String, val exitCode: Int, val stdout: String, val stderr: String) { - override fun toString(): String { - return """|Command : '${whitespaceCharsToSymbols(command)}' - |Exit Code : $exitCode - |Stdout : '${whitespaceCharsToSymbols(stdout)}' - |Stderr : '${whitespaceCharsToSymbols(stderr)}' - |""".trimMargin() - } -} diff --git a/src/main/kotlin/io/github/kscripting/shell/model/OsPath.kt b/src/main/kotlin/io/github/kscripting/shell/model/OsPath.kt deleted file mode 100644 index 30c2555..0000000 --- a/src/main/kotlin/io/github/kscripting/shell/model/OsPath.kt +++ /dev/null @@ -1,272 +0,0 @@ -package io.github.kscripting.shell.model - -import arrow.core.Either -import arrow.core.left -import arrow.core.right - -//Path representation for different OSes -data class OsPath(val osType: OsType, val pathType: PathType, val pathParts: List, val pathSeparator: Char) { - - fun resolve(vararg pathParts: String): OsPath { - return resolve(createOrThrow(osType, pathParts.joinToString(pathSeparator.toString()))) - } - - fun resolve(path: OsPath): OsPath { - require(osType == path.osType) { - "Paths from different OS's: '${this.osType.name}' path can not be resolved with '${path.osType.name}' path" - } - - require(path.pathType != PathType.ABSOLUTE) { - "Can not resolve absolute, relative or undefined path '${stringPath()}' using absolute path '${path.stringPath()}'" - } - - val newPath = stringPath() + pathSeparator + path.stringPath() - val newPathParts = buildList { - addAll(pathParts) - addAll(path.pathParts) - } - - val normalizedPath = when (val result = normalize(newPath, newPathParts, pathType)) { - is Either.Right -> result.value - is Either.Left -> throw IllegalArgumentException(result.value) - } - - return OsPath(osType, pathType, normalizedPath, pathSeparator) - } - - //Not all conversions make sense: only Windows to CygWin and Msys and vice versa - fun convert(targetOsType: OsType): OsPath { - if (this.osType == targetOsType) { - return this - } - - if ((this.osType.isPosixLike() && targetOsType.isPosixLike()) || (this.osType.isWindowsLike() && targetOsType.isWindowsLike())) { - return OsPath(targetOsType, pathType, pathParts, pathSeparator) - } - - val toPosix = osType.isWindowsLike() && targetOsType.isPosixHostedOnWindows() - val fromPosix = osType.isPosixHostedOnWindows() && targetOsType.isWindowsLike() - - require(toPosix || fromPosix) { - "Only conversion between Windows and Posix hosted on Windows paths are supported" - } - - val newParts = mutableListOf() - - when { - toPosix -> { - val drive: String - - if (pathType == PathType.ABSOLUTE) { - drive = pathParts[0][0].lowercase() - - newParts.add("/") - - if (targetOsType == OsType.CYGWIN) { - newParts.add("cygdrive") - newParts.add(drive) - } else { - newParts.add(drive) - } - - newParts.addAll(pathParts.subList(1, pathParts.size)) - } else { - newParts.addAll(pathParts) - } - } - - fromPosix -> { - if (pathType == PathType.ABSOLUTE) { - if (osType == OsType.CYGWIN) { - newParts.add(pathParts[2] + ":") - newParts.addAll(pathParts.subList(3, pathParts.size)) - } else { - newParts.add(pathParts[1] + ":") - newParts.addAll(pathParts.subList(2, pathParts.size)) - } - } else { - newParts.addAll(pathParts) - } - } - - else -> throw IllegalArgumentException("Invalid conversion: ${pathType.name} to ${targetOsType.name}") - } - - return OsPath(targetOsType, pathType, newParts, resolvePathSeparator(targetOsType)) - } - - fun stringPath(): String { - if (osType.isPosixLike() && pathParts.isNotEmpty() && pathParts[0] == "/") { - return "/" + pathParts.subList(1, pathParts.size).joinToString(pathSeparator.toString()) { it } - } - - return pathParts.joinToString(pathSeparator.toString()) { it } - } - - override fun toString(): String = stringPath() - - companion object { - //https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names - //The rule here is more strict than necessary, but it is at least good practice to follow such a rule. - private val forbiddenCharacters = buildSet { - add('<') - add('>') - add(':') - add('"') - add('|') - add('?') - add('*') - for (i in 0 until 32) { - add(i.toChar()) - } - } - - private const val alphaChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - - fun resolvePathSeparator(osType: OsType) = if (osType.isPosixLike()) { - '/' - } else { - '\\' - } - - fun createOrThrow(osType: OsType, root: String, vararg pathParts: String): OsPath { - return when (val result = internalCreate(osType, root, *pathParts)) { - is Either.Right -> result.value - is Either.Left -> throw IllegalArgumentException(result.value) - } - } - - fun create(osType: OsType, root: String, vararg pathParts: String): OsPath? { - return when (val result = internalCreate(osType, root, *pathParts)) { - is Either.Right -> result.value - is Either.Left -> null - } - } - - //Relaxed validation: - //1. It doesn't matter if there is '/' or '\' used as path separator - both are treated he same - //2. Duplicated or trailing slashes '/' and backslashes '\' are just ignored - private fun internalCreate(osType: OsType, root: String, vararg pathParts: String): Either { - val pathSeparatorCharacter = resolvePathSeparator(osType) - - val path = listOf(root, *pathParts).joinToString(pathSeparatorCharacter.toString()) - val pathPartsResolved = path.split('/', '\\').toMutableList() - - //Validate root element of path and find out if it is absolute or relative - val rootElementSizeInInputPath: Int - val pathType: PathType - - when { - pathPartsResolved.isEmpty() -> { - pathType = PathType.UNDEFINED - rootElementSizeInInputPath = 0 - } - - pathPartsResolved[0] == "~" -> { - pathType = PathType.ABSOLUTE - rootElementSizeInInputPath = 1 - } - - pathPartsResolved[0] == ".." || pathPartsResolved[0] == "." -> { - pathType = PathType.RELATIVE - rootElementSizeInInputPath = pathPartsResolved[0].length - } - - osType.isPosixLike() && path.startsWith("/") -> { - //After split first element is empty for absolute paths on Linux; assigning correct value below - pathPartsResolved.add(0, "/") - pathType = PathType.ABSOLUTE - rootElementSizeInInputPath = 1 - } - - osType.isWindowsLike() && pathPartsResolved[0].length == 2 && pathPartsResolved[0][1] == ':' && alphaChars.contains( - pathPartsResolved[0][0] - ) -> { - pathType = PathType.ABSOLUTE - rootElementSizeInInputPath = 2 - } - - else -> { - //This is undefined path - pathType = PathType.UNDEFINED - rootElementSizeInInputPath = 0 - } - } - - val forbiddenCharacter = - path.substring(rootElementSizeInInputPath).find { forbiddenCharacters.contains(it) } - - if (forbiddenCharacter != null) { - return "Invalid character '$forbiddenCharacter' in path '$path'".left() - } - - //Remove empty path parts - there were duplicated or trailing slashes / backslashes in initial path - pathPartsResolved.removeAll { it.isEmpty() } - - val normalizedPath = when (val result = normalize(path, pathPartsResolved, pathType)) { - is Either.Right -> result.value - is Either.Left -> return result.value.left() - } - - return OsPath(osType, pathType, normalizedPath, pathSeparatorCharacter).right() - } - - fun normalize(path: String, pathParts: List, pathType: PathType): Either> { - //Relative: - // ./../ --> ../ - // ./a/../ --> ./ - // ./a/ --> ./a - // ../a --> ../a - // ../../a --> ../../a - - //Absolute: - // /../ --> invalid (above root) - // /a/../ --> / - - val newParts = mutableListOf() - var index = 0 - - if (pathType != PathType.UNDEFINED) { - newParts.add(pathParts[0]) - index = 1 - } - - while (index < pathParts.size) { - if (pathParts[index] == ".") { - //Just skip . without adding it to newParts - } else if (pathParts[index] == "..") { - - if (pathType == PathType.ABSOLUTE && newParts.size == 1) { - return "Path after normalization goes beyond root element: '$path'".left() - } - - if (newParts.size > 0) { - when (newParts.last()) { - "." -> { - //It's the first element - other dots should be already removed before - newParts.removeAt(newParts.size - 1) - newParts.add("..") - } - - ".." -> { - newParts.add("..") - } - - else -> { - newParts.removeAt(newParts.size - 1) - } - } - } else { - newParts.add("..") - } - } else { - newParts.add(pathParts[index]) - } - - index += 1 - } - - return newParts.right() - } - } -} diff --git a/src/main/kotlin/io/github/kscripting/shell/model/OsPathExt.kt b/src/main/kotlin/io/github/kscripting/shell/model/OsPathExt.kt deleted file mode 100644 index 89f9225..0000000 --- a/src/main/kotlin/io/github/kscripting/shell/model/OsPathExt.kt +++ /dev/null @@ -1,64 +0,0 @@ -package io.github.kscripting.shell.model - -import java.io.File -import java.net.URI -import java.nio.charset.Charset -import java.nio.file.OpenOption -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.io.path.* - - -// Conversion to OsPath - -fun File.toOsPath(): OsPath = OsPath.createOrThrow(OsType.native, absolutePath) - -fun Path.toOsPath(): OsPath = OsPath.createOrThrow(OsType.native, absolutePathString()) - -fun URI.toOsPath(): OsPath = - if (this.scheme == "file") File(this).toOsPath() else throw IllegalArgumentException("Invalid conversion from URL to OsPath") - - -// Conversion from OsPath - -fun OsPath.toNativePath(): Path = Paths.get(toNativeOsPath().stringPath()) - -fun OsPath.toNativeOsPath() = if (osType.isPosixHostedOnWindows()) convert(OsType.WINDOWS) else this - -fun OsPath.toNativeFile(): File = toNativePath().toFile() - - -// OsPath operations - -fun OsPath.exists() = toNativePath().exists() - -fun OsPath.createDirectories(): OsPath = OsPath.createOrThrow(nativeType, toNativePath().createDirectories().pathString) - -fun OsPath.copyTo(target: OsPath, overwrite: Boolean = false): OsPath = - OsPath.createOrThrow(nativeType, toNativePath().copyTo(target.toNativePath(), overwrite).pathString) - -fun OsPath.writeText(text: CharSequence, charset: Charset = Charsets.UTF_8, vararg options: OpenOption): Unit = - toNativePath().writeText(text, charset, *options) - -fun OsPath.readText(charset: Charset = Charsets.UTF_8): String = toNativePath().readText(charset) - - -// OsPath accessors - -val OsPath.leaf - get() = if (pathParts.isEmpty()) "" else pathParts.last() - -val OsPath.root - get() = if (pathParts.isEmpty()) "" else pathParts.first() - -val OsPath.rootOsPath - get() = OsPath.createOrThrow(osType, root) - -val OsPath.parent - get() = toNativePath().parent - -val OsPath.nativeType - get() = if (osType.isPosixHostedOnWindows()) OsType.WINDOWS else osType - -val OsPath.extension - get() = leaf.substringAfterLast('.', "") diff --git a/src/main/kotlin/io/github/kscripting/shell/model/OsType.kt b/src/main/kotlin/io/github/kscripting/shell/model/OsType.kt deleted file mode 100644 index cd50efb..0000000 --- a/src/main/kotlin/io/github/kscripting/shell/model/OsType.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.kscripting.shell.model - -import org.apache.commons.lang3.SystemUtils - -enum class OsType(val osName: String) { - LINUX("linux"), MACOS("darwin"), WINDOWS("windows"), CYGWIN("cygwin"), MSYS("msys"), FREEBSD("freebsd"); - - fun isPosixLike() = (this == LINUX || this == MACOS || this == FREEBSD || this == CYGWIN || this == MSYS) - fun isPosixHostedOnWindows() = (this == CYGWIN || this == MSYS) - fun isWindowsLike() = (this == WINDOWS) - - companion object { - val native: OsType = guessNativeType() - - fun findOrThrow(name: String): OsType = find(name) ?: throw IllegalArgumentException("Unsupported OS: '$name'") - - // Exact comparison (it.osName.equals(name, true)) seems to be not feasible as there is also e.g. "darwin21" - // "darwin19", "linux-musl" (for Docker Alpine), "linux-gnu" and maybe even other osTypes. But it seems that - // startsWith() covers all cases. - fun find(name: String): OsType? = values().find { name.startsWith(it.osName, true) } - - private fun guessNativeType(): OsType { - when { - SystemUtils.IS_OS_LINUX -> return LINUX - SystemUtils.IS_OS_MAC -> return MACOS - SystemUtils.IS_OS_WINDOWS -> return WINDOWS - SystemUtils.IS_OS_FREE_BSD -> return FREEBSD - } - - return LINUX - } - } -} diff --git a/src/main/kotlin/io/github/kscripting/shell/model/PathType.kt b/src/main/kotlin/io/github/kscripting/shell/model/PathType.kt deleted file mode 100644 index ebae207..0000000 --- a/src/main/kotlin/io/github/kscripting/shell/model/PathType.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.kscripting.shell.model - - -enum class PathType { - ABSOLUTE, RELATIVE, UNDEFINED -} diff --git a/src/main/kotlin/io/github/kscripting/shell/model/ProcessResult.kt b/src/main/kotlin/io/github/kscripting/shell/model/ProcessResult.kt index ad709da..8f938c7 100644 --- a/src/main/kotlin/io/github/kscripting/shell/model/ProcessResult.kt +++ b/src/main/kotlin/io/github/kscripting/shell/model/ProcessResult.kt @@ -1,11 +1,3 @@ package io.github.kscripting.shell.model -import io.github.kscripting.shell.util.ShellEscapeUtils.whitespaceCharsToSymbols - -data class ProcessResult(val command: String, val exitCode: Int) { - override fun toString(): String { - return """|Command : '${whitespaceCharsToSymbols(command)}' - |Exit Code : $exitCode - |""".trimMargin() - } -} +data class ProcessResult(val exitCode: Int, val stdout: String, val stderr: String) diff --git a/src/main/kotlin/io/github/kscripting/shell/model/ScriptContext.kt b/src/main/kotlin/io/github/kscripting/shell/model/ScriptContext.kt deleted file mode 100644 index 425b1ef..0000000 --- a/src/main/kotlin/io/github/kscripting/shell/model/ScriptContext.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.kscripting.shell.model - -data class ScriptContext( - val osType: OsType, - val workingDir: OsPath, - val executorDir: OsPath, - val scriptLocation: ScriptLocation -) diff --git a/src/main/kotlin/io/github/kscripting/shell/model/ScriptLocation.kt b/src/main/kotlin/io/github/kscripting/shell/model/ScriptLocation.kt deleted file mode 100644 index 6a6daef..0000000 --- a/src/main/kotlin/io/github/kscripting/shell/model/ScriptLocation.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.kscripting.shell.model - -import java.net.URI - -data class ScriptLocation( - val level: Int, - val scriptSource: ScriptSource, - val scriptType: ScriptType, - val sourceUri: URI?, - val sourceContextUri: URI, - val scriptName: String //without Kotlin extension (but possibly with other extensions) -) diff --git a/src/main/kotlin/io/github/kscripting/shell/model/ScriptSource.kt b/src/main/kotlin/io/github/kscripting/shell/model/ScriptSource.kt deleted file mode 100644 index 2642083..0000000 --- a/src/main/kotlin/io/github/kscripting/shell/model/ScriptSource.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.kscripting.shell.model - -enum class ScriptSource { FILE, HTTP, STD_INPUT, OTHER_FILE, PARAMETER } diff --git a/src/main/kotlin/io/github/kscripting/shell/model/ScriptType.kt b/src/main/kotlin/io/github/kscripting/shell/model/ScriptType.kt deleted file mode 100644 index 4da5076..0000000 --- a/src/main/kotlin/io/github/kscripting/shell/model/ScriptType.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.kscripting.shell.model - -enum class ScriptType(val extension: String) { - KT(".kt"), KTS(".kts"); - - companion object { - fun findByExtension(name: String): ScriptType? = values().find { type -> name.endsWith(type.extension, true) } - } -} diff --git a/src/main/kotlin/io/github/kscripting/shell/model/ShellType.kt b/src/main/kotlin/io/github/kscripting/shell/model/ShellType.kt new file mode 100644 index 0000000..6617453 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/shell/model/ShellType.kt @@ -0,0 +1,8 @@ +package io.github.kscripting.shell.model + +enum class ShellType(vararg val executorCommand: String) { + NONE(), + BASH("bash", "-c"), + SH("sh", "-c"), + CMD("cmd", "/c") +} diff --git a/src/main/kotlin/io/github/kscripting/shell/process/ProcessRunner.kt b/src/main/kotlin/io/github/kscripting/shell/process/ProcessRunner.kt index 4fc3f2f..977daa5 100644 --- a/src/main/kotlin/io/github/kscripting/shell/process/ProcessRunner.kt +++ b/src/main/kotlin/io/github/kscripting/shell/process/ProcessRunner.kt @@ -1,8 +1,11 @@ package io.github.kscripting.shell.process -import io.github.kscripting.shell.model.OsPath -import io.github.kscripting.shell.model.ProcessResult -import io.github.kscripting.shell.model.toNativeFile +import io.github.kscripting.os.model.OsPath +import io.github.kscripting.os.model.toNativeFile +import io.github.kscripting.shell.model.CommandTimeoutException +import io.github.kscripting.shell.util.Sanitizer +import io.github.kscripting.shell.util.Sanitizer.Companion.EMPTY_SANITIZER +import java.io.InputStream import java.io.PrintStream import java.util.concurrent.TimeUnit @@ -18,11 +21,21 @@ object ProcessRunner { envAdjuster: EnvAdjuster = {}, waitTimeMinutes: Int = 10, inheritInput: Boolean = false, + outputSanitizer: Sanitizer = EMPTY_SANITIZER, outPrinter: List = DEFAULT_OUT_PRINTERS, errPrinter: List = DEFAULT_ERR_PRINTERS, - ): ProcessResult { + inputStream: InputStream? = null + ): Int { return runProcess( - command.asList(), workingDirectory, envAdjuster, waitTimeMinutes, inheritInput, outPrinter, errPrinter + command.asList(), + workingDirectory, + envAdjuster, + waitTimeMinutes, + inheritInput, + outputSanitizer, + outPrinter, + errPrinter, + inputStream ) } @@ -32,23 +45,33 @@ object ProcessRunner { envAdjuster: EnvAdjuster = {}, waitTimeMinutes: Int = 10, inheritInput: Boolean = false, + outputSanitizer: Sanitizer = EMPTY_SANITIZER, outPrinter: List = DEFAULT_OUT_PRINTERS, errPrinter: List = DEFAULT_ERR_PRINTERS, - ): ProcessResult { + inputStream: InputStream? = null + ): Int { try { // simplify with https://stackoverflow.com/questions/35421699/how-to-invoke-external-command-from-within-kotlin-code val process = ProcessBuilder(command) .directory(workingDirectory?.toNativeFile()) .redirectInput(if (inheritInput) ProcessBuilder.Redirect.INHERIT else ProcessBuilder.Redirect.PIPE) + .redirectOutput(ProcessBuilder.Redirect.PIPE) .apply { envAdjuster(environment()) }.start() + // My own explanation is here: https://github.com/lxc/lxd/issues/6856 + val outputStream = process.outputStream + if (!inheritInput && outputStream != null) { + inputStream?.transferTo(outputStream) + outputStream.close() + } + // we need to gobble the streams to prevent that the internal pipes hit their respective buffer limits, which // would lock the sub-process execution (see see https://github.com/kscripting/kscript/issues/55 // https://stackoverflow.com/questions/14165517/processbuilder-forwarding-stdout-and-stderr-of-started-processes-without-blocki - val inputStreamReader = StreamGobbler(process.inputStream, outPrinter).start() - val errorStreamReader = StreamGobbler(process.errorStream, errPrinter).start() + val inputStreamReader = StreamGobbler(outputSanitizer, process.inputStream, outPrinter).start() + val errorStreamReader = StreamGobbler(outputSanitizer, process.errorStream, errPrinter).start() val exitedNormally = process.waitFor(waitTimeMinutes.toLong(), TimeUnit.MINUTES) @@ -56,10 +79,10 @@ object ProcessRunner { errorStreamReader.finish() if (!exitedNormally) { - throw IllegalStateException("Command has timed out after $waitTimeMinutes minutes.") + throw CommandTimeoutException("Command has timed out after $waitTimeMinutes minutes.") } - return ProcessResult(command.joinToString(" "), process.exitValue()) + return process.exitValue() } catch (e: Exception) { throw IllegalStateException("Error executing command: '$command'", e) } diff --git a/src/main/kotlin/io/github/kscripting/shell/process/StreamGobbler.kt b/src/main/kotlin/io/github/kscripting/shell/process/StreamGobbler.kt index a3137f5..4e3d7cd 100644 --- a/src/main/kotlin/io/github/kscripting/shell/process/StreamGobbler.kt +++ b/src/main/kotlin/io/github/kscripting/shell/process/StreamGobbler.kt @@ -1,5 +1,6 @@ package io.github.kscripting.shell.process +import io.github.kscripting.shell.util.Sanitizer import java.io.BufferedReader import java.io.InputStream import java.io.InputStreamReader @@ -7,6 +8,7 @@ import java.io.PrintStream class StreamGobbler( + private val outputSanitizer: Sanitizer, inputStream: InputStream, private val printStream: List, ) { @@ -29,13 +31,23 @@ class StreamGobbler( private fun readInputStreamSequentially() { val charArray = CharArray(1024) var length: Int + var rest = "" while (reader.read(charArray).also { length = it } != -1) { - val content = String(charArray, 0, length) + var content = rest + String(charArray, 0, length) + rest = outputSanitizer.calculatePotentialMatch(content) + content = outputSanitizer.sanitize(content.dropLast(rest.length)) + outputToPrintStreams(content) + } + + if (rest.isNotEmpty()) { + outputToPrintStreams(outputSanitizer.sanitize(rest)) + } + } - printStream.forEach { - it.print(content) - } + private fun outputToPrintStreams(content: String) { + printStream.forEach { + it.print(content) } } } diff --git a/src/main/kotlin/io/github/kscripting/shell/util/Sanitizer.kt b/src/main/kotlin/io/github/kscripting/shell/util/Sanitizer.kt new file mode 100644 index 0000000..2b7f359 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/shell/util/Sanitizer.kt @@ -0,0 +1,61 @@ +package io.github.kscripting.shell.util + +//Mapping: +//[bs] --> \\ +//[nl] --> \n +class Sanitizer(substitutions: List> = emptyList()) { + private val substitutions: List> = substitutions.sortedByDescending { it.first.length } + + constructor(vararg substitutions: Pair) : this(substitutions.toList()) + + fun sanitize(string: String): String { + var result = string + + for (substitution in substitutions) { + result = result.replace(substitution.first, substitution.second) + } + + return result + } + + fun calculatePotentialMatch(content: String): String { + //It will be always max. key size - 1 (string was not matched because one character was missing); + //We can simplify and remove that rest always; drawback: if shorter substrings are part of the longer substring + //then longer substring will never be matched + + if (substitutions.isEmpty() || substitutions[0].first.length == 1) { + //if there are no substitutions or they are no longer than 1 character then just return + return "" + } + + var rest = "" + var checkFactor = 1 + + while (rest.isEmpty() && checkFactor < substitutions[0].first.length) { + for (substitution in substitutions) { + val substitutionKey = substitution.first + + if (checkFactor >= substitutionKey.length) { + break + } + + val potentialEnding = substitutionKey.substring(0, substitutionKey.length - checkFactor) + + if (content.endsWith(potentialEnding)) { + rest = potentialEnding + break + } + } + + checkFactor += 1 + } + + return rest + } + + fun swapped() = Sanitizer(substitutions.map { it.second to it.first }.sortedByDescending { it.first }) + + companion object { + val EMPTY_SANITIZER = Sanitizer() + } +} diff --git a/src/main/kotlin/io/github/kscripting/shell/util/ShellEscapeUtils.kt b/src/main/kotlin/io/github/kscripting/shell/util/ShellEscapeUtils.kt deleted file mode 100644 index f7185ad..0000000 --- a/src/main/kotlin/io/github/kscripting/shell/util/ShellEscapeUtils.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.kscripting.shell.util - -object ShellEscapeUtils { - fun whitespaceCharsToSymbols(string: String): String = - string.replace("\\", "[bs]").lines().joinToString("[nl]") -} diff --git a/src/test/kotlin/io/github/kscripting/os/model/GenericOsPathTest.kt b/src/test/kotlin/io/github/kscripting/os/model/GenericOsPathTest.kt new file mode 100644 index 0000000..020d926 --- /dev/null +++ b/src/test/kotlin/io/github/kscripting/os/model/GenericOsPathTest.kt @@ -0,0 +1,21 @@ +package io.github.kscripting.os.model + +import assertk.assertThat +import assertk.assertions.isEqualTo +import io.github.kscripting.os.instance.LinuxVfs +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GenericOsPathTest { + private var simplePath = OsPath(LinuxVfs("/home/admin"), "", listOf("home", "admin")) + + @Test + fun `Test Empty paths`() { + assertThat(simplePath.toString()).isEqualTo("home/admin [LINUX]") + assertThat(simplePath.leaf).isEqualTo("admin") + + //println(simplePath.flatMap { it.resolve("mk", "km") }.flatMap { it.parent }) + + } +} diff --git a/src/test/kotlin/io/github/kscripting/os/model/HostedOsPathTest.kt b/src/test/kotlin/io/github/kscripting/os/model/HostedOsPathTest.kt new file mode 100644 index 0000000..8782ec2 --- /dev/null +++ b/src/test/kotlin/io/github/kscripting/os/model/HostedOsPathTest.kt @@ -0,0 +1,92 @@ +package io.github.kscripting.os.model + +import assertk.assertThat +import assertk.assertions.isEqualTo +import io.github.kscripting.os.instance.CygwinVfs +import io.github.kscripting.os.instance.MsysVfs +import io.github.kscripting.os.instance.WindowsVfs +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class HostedOsPathTest { + private val windowsVfs = WindowsVfs("C:\\Users\\Admin\\.kscript") + private val cygwinVfs = CygwinVfs(windowsVfs.createOsPath("C:\\Programs\\Cygwin\\"), "/home/admin") + private val msysVfs = MsysVfs(windowsVfs.createOsPath("C:\\Programs\\Msys\\"), "/home/admin") + + @Test + fun `Test Cygwin to Windows`() { + assertThat( + cygwinVfs.createOsPath("/cygdrive/c/home/admin/.kscript").toNative().path + ).isEqualTo("c:\\home\\admin\\.kscript") + + assertThat( + cygwinVfs.createOsPath("~/.kscript").toNative().path + ).isEqualTo("C:\\Programs\\Cygwin\\home\\admin\\.kscript") + + assertThat( + cygwinVfs.createOsPath("/usr/local/bin/sdk").toNative().path + ).isEqualTo("C:\\Programs\\Cygwin\\usr\\local\\bin\\sdk") + + assertThat( + cygwinVfs.createOsPath("../home/admin/.kscript").toNative().path + ).isEqualTo("..\\home\\admin\\.kscript") + } + + @Test + fun `Test Windows to Cygwin`() { + assertThat( + cygwinVfs.toHosted(windowsVfs.createOsPath("C:\\home\\admin\\.kscript")).path + ).isEqualTo("/cygdrive/c/home/admin/.kscript") + + assertThat( + cygwinVfs.toHosted(windowsVfs.createOsPath("..\\home\\admin\\.kscript")).path + ).isEqualTo("../home/admin/.kscript") + + assertThat( + cygwinVfs.toHosted(windowsVfs.createOsPath("C:\\Programs\\Cygwin\\home\\admin\\.kscript")).path + ).isEqualTo("~/.kscript") + + assertThat( + cygwinVfs.toHosted(windowsVfs.createOsPath("C:\\Programs\\Cygwin\\usr\\local\\sdk")).path + ).isEqualTo("/usr/local/sdk") + } + + @Test + fun `Test MSys to Windows`() { + assertThat( + msysVfs.createOsPath("/c/home/admin/.kscript").toNative().path + ).isEqualTo("c:\\home\\admin\\.kscript") + + assertThat( + msysVfs.createOsPath("~/.kscript").toNative().path + ).isEqualTo("C:\\Programs\\Msys\\home\\admin\\.kscript") + + assertThat( + msysVfs.createOsPath("/usr/local/bin/sdk").toNative().path + ).isEqualTo("C:\\Programs\\Msys\\usr\\local\\bin\\sdk") + + assertThat( + msysVfs.createOsPath("../home/admin/.kscript").toNative().path + ).isEqualTo("..\\home\\admin\\.kscript") + } + + @Test + fun `Test Windows to MSys`() { + assertThat( + msysVfs.toHosted(windowsVfs.createOsPath("C:\\home\\admin\\.kscript")).path + ).isEqualTo("/c/home/admin/.kscript") + + assertThat( + msysVfs.toHosted(windowsVfs.createOsPath("..\\home\\admin\\.kscript")).path + ).isEqualTo("../home/admin/.kscript") + + assertThat( + msysVfs.toHosted(windowsVfs.createOsPath("C:\\Programs\\Msys\\home\\admin\\.kscript")).path + ).isEqualTo("~/.kscript") + + assertThat( + msysVfs.toHosted(windowsVfs.createOsPath("C:\\Programs\\Msys\\usr\\local\\sdk")).path + ).isEqualTo("/usr/local/sdk") + } +} diff --git a/src/test/kotlin/io/github/kscripting/os/model/PosixOsPathTest.kt b/src/test/kotlin/io/github/kscripting/os/model/PosixOsPathTest.kt new file mode 100644 index 0000000..556a229 --- /dev/null +++ b/src/test/kotlin/io/github/kscripting/os/model/PosixOsPathTest.kt @@ -0,0 +1,156 @@ +package io.github.kscripting.os.model + +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.hasMessage +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.prop +import io.github.kscripting.os.OsType +import io.github.kscripting.os.instance.LinuxVfs +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PosixOsPathTest { + private val linuxVfs = LinuxVfs("userhome") + + @Test + fun `Test Posix paths`() { + assertThat(linuxVfs.createOsPath("/")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("/") + it.prop(OsPath::pathParts).isEqualTo(emptyList()) + } + + assertThat(linuxVfs.createOsPath("/home/admin/.kscript")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("/") + it.prop(OsPath::pathParts).isEqualTo(listOf("home", "admin", ".kscript")) + } + + assertThat(linuxVfs.createOsPath("./home/admin/.kscript")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("home", "admin", ".kscript")) + } + + assertThat(linuxVfs.createOsPath("")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(emptyList()) + } + + assertThat(linuxVfs.createOsPath("file.txt")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("file.txt")) + } + + assertThat(linuxVfs.createOsPath(".")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(emptyList()) + } + + assertThat(linuxVfs.createOsPath("../home/admin/.kscript")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("..", "home", "admin", ".kscript")) + } + + assertThat(linuxVfs.createOsPath("..")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("..")) + } + + //Duplicated separators are accepted + assertThat(linuxVfs.createOsPath("..//home////admin/.kscript/")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("..", "home", "admin", ".kscript")) + } + + //Both types of separator are accepted + assertThat(linuxVfs.createOsPath("..//home\\admin\\.kscript/")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("..", "home", "admin", ".kscript")) + } + } + + @Test + fun `Normalization of Posix paths`() { + assertThat(linuxVfs.createOsPath("/home/admin/.kscript/../../")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("/") + it.prop(OsPath::pathParts).isEqualTo(listOf("home")) + } + + assertThat(linuxVfs.createOsPath("./././../../script")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("..", "..", "script")) + } + + assertThat(linuxVfs.createOsPath("/a/b/c/../d/script")).let { + it.prop(OsPath::osType).isEqualTo(OsType.LINUX) + it.prop(OsPath::root).isEqualTo("/") + it.prop(OsPath::pathParts).isEqualTo(listOf("a", "b", "d", "script")) + } + + assertFailure { linuxVfs.createOsPath("/.kscript/../../") }.isInstanceOf(IllegalArgumentException::class) + .hasMessage("Path after normalization goes beyond root element: '/'") + } + + @Test + fun `Test invalid Posix paths`() { + assertFailure { + linuxVfs.createOsPath("/ad\u0000asdf") + }.isInstanceOf(IllegalArgumentException::class.java).hasMessage("Invalid character '\u0000' in path '/ad\u0000asdf'") + } + + @Test + fun `Test Posix stringPath`() { + assertThat( + linuxVfs.createOsPath("/home/admin/.kscript").path + ).isEqualTo("/home/admin/.kscript") + assertThat(linuxVfs.createOsPath("/a/b/c/../d/script").path).isEqualTo("/a/b/d/script") + assertThat(linuxVfs.createOsPath("./././../../script").path).isEqualTo("../../script") + assertThat(linuxVfs.createOsPath("script/file.txt").path).isEqualTo("script/file.txt") + } + + @Test + fun `Test Posix resolve`() { + assertThat( + linuxVfs.createOsPath("/").resolve(linuxVfs.createOsPath("./.kscript/")) + .path + ).isEqualTo("/.kscript") + + assertThat( + linuxVfs.createOsPath("/home/admin/").resolve(linuxVfs.createOsPath("./.kscript/")).path + ).isEqualTo("/home/admin/.kscript") + + assertThat( + linuxVfs.createOsPath("./home/admin/").resolve(linuxVfs.createOsPath("./.kscript/")).path + ).isEqualTo("home/admin/.kscript") + + assertThat( + linuxVfs.createOsPath("../home/admin/").resolve(linuxVfs.createOsPath("./.kscript/")).path + ).isEqualTo("../home/admin/.kscript") + + assertThat( + linuxVfs.createOsPath("..").resolve(linuxVfs.createOsPath("./.kscript/")).path + ).isEqualTo("../.kscript") + + assertThat( + linuxVfs.createOsPath(".").resolve(linuxVfs.createOsPath("./.kscript/")).path + ).isEqualTo(".kscript") + + assertFailure { + linuxVfs.createOsPath("./home/admin").resolve(linuxVfs.createOsPath("/run")) + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("Can not resolve absolute or relative path 'home/admin' using absolute path '/run'") + } +} diff --git a/src/test/kotlin/io/github/kscripting/os/model/WindowsOsPathTest.kt b/src/test/kotlin/io/github/kscripting/os/model/WindowsOsPathTest.kt new file mode 100644 index 0000000..3d87c18 --- /dev/null +++ b/src/test/kotlin/io/github/kscripting/os/model/WindowsOsPathTest.kt @@ -0,0 +1,127 @@ +package io.github.kscripting.os.model + +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.hasMessage +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.prop +import io.github.kscripting.os.OsType +import io.github.kscripting.os.instance.WindowsVfs +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class WindowsOsPathTest { + private val windowsVfs = WindowsVfs("C:\\Users\\Admin\\.kscript") + + @Test + fun `Test Windows paths`() { + assertThat(windowsVfs.createOsPath("C:\\")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("C:\\") + it.prop(OsPath::pathParts).isEqualTo(emptyList()) + } + + assertThat(windowsVfs.createOsPath("C:\\home\\admin\\.kscript")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("C:\\") + it.prop(OsPath::pathParts).isEqualTo(listOf("home", "admin", ".kscript")) + } + + assertThat(windowsVfs.createOsPath("")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(emptyList()) + } + + assertThat(windowsVfs.createOsPath("file.txt")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("file.txt")) + } + + assertThat(windowsVfs.createOsPath(".")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(emptyList()) + } + + assertThat(windowsVfs.createOsPath(".\\home\\admin\\.kscript")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("home", "admin", ".kscript")) + } + + assertThat(windowsVfs.createOsPath("..\\home\\admin\\.kscript")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("..", "home", "admin", ".kscript")) + } + + assertThat(windowsVfs.createOsPath("..")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("..")) + } + + //Duplicated separators are accepted + assertThat(windowsVfs.createOsPath("C:\\home\\\\\\\\admin\\.kscript\\")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("C:\\") + it.prop(OsPath::pathParts).isEqualTo(listOf("home", "admin", ".kscript")) + } + + //Both types of separator are accepted + assertThat(windowsVfs.createOsPath("C:/home\\admin/.kscript////")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("C:\\") + it.prop(OsPath::pathParts).isEqualTo(listOf("home", "admin", ".kscript")) + } + } + + @Test + fun `Normalization of Windows paths`() { + assertThat(windowsVfs.createOsPath("C:\\home\\admin\\.kscript\\..\\..\\")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("C:\\") + it.prop(OsPath::pathParts).isEqualTo(listOf("home")) + } + + assertThat(windowsVfs.createOsPath(".\\.\\.\\..\\..\\script")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("") + it.prop(OsPath::pathParts).isEqualTo(listOf("..", "..", "script")) + } + + assertThat(windowsVfs.createOsPath("C:\\a\\b\\c\\..\\d\\script")).let { + it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) + it.prop(OsPath::root).isEqualTo("C:\\") + it.prop(OsPath::pathParts).isEqualTo(listOf("a", "b", "d", "script")) + } + + assertFailure { + windowsVfs.createOsPath("C:\\.kscript\\..\\..\\") + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("Path after normalization goes beyond root element: 'C:\\'") + } + + @Test + fun `Test invalid Windows paths`() { + assertFailure { windowsVfs.createOsPath("C:\\adas?df") } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("Invalid character '?' in path 'C:\\adas?df'") + + assertFailure { windowsVfs.createOsPath("home:\\vagrant") } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("Invalid character ':' in path 'home:\\vagrant'") + } + + @Test + fun `Test Windows stringPath`() { + assertThat(windowsVfs.createOsPath("C:\\home\\admin\\.kscript").path).isEqualTo("C:\\home\\admin\\.kscript") + assertThat(windowsVfs.createOsPath("c:\\a\\b\\c\\..\\d\\script").path).isEqualTo("c:\\a\\b\\d\\script") + assertThat(windowsVfs.createOsPath(".\\.\\.\\..\\..\\script").path).isEqualTo("..\\..\\script") + assertThat(windowsVfs.createOsPath("script\\file.txt").path).isEqualTo("script\\file.txt") + } +} diff --git a/src/test/kotlin/io/github/kscripting/shell/model/OsPathTest.kt b/src/test/kotlin/io/github/kscripting/shell/model/OsPathTest.kt deleted file mode 100644 index d09e842..0000000 --- a/src/test/kotlin/io/github/kscripting/shell/model/OsPathTest.kt +++ /dev/null @@ -1,325 +0,0 @@ -package io.github.kscripting.shell.model - -import assertk.assertThat -import assertk.assertions.* -import org.junit.jupiter.api.Test - -class OsPathTest { - // ************************************************** LINUX PATHS ************************************************** - @Test - fun `Test Linux paths`() { - assertThat(OsPath.createOrThrow(OsType.LINUX, "/")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("/")) - it.prop(OsPath::pathType).isEqualTo(PathType.ABSOLUTE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - assertThat(OsPath.createOrThrow(OsType.LINUX, "/home/admin/.kscript")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("/", "home", "admin", ".kscript")) - it.prop(OsPath::pathType).isEqualTo(PathType.ABSOLUTE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - assertThat(OsPath.createOrThrow(OsType.LINUX, "./home/admin/.kscript")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf(".", "home", "admin", ".kscript")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - assertThat(OsPath.createOrThrow(OsType.LINUX, "")).let { - it.prop(OsPath::pathParts).isEqualTo(emptyList()) - it.prop(OsPath::pathType).isEqualTo(PathType.UNDEFINED) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - assertThat(OsPath.createOrThrow(OsType.LINUX, "file.txt")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("file.txt")) - it.prop(OsPath::pathType).isEqualTo(PathType.UNDEFINED) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - assertThat(OsPath.createOrThrow(OsType.LINUX, ".")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf(".")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - assertThat(OsPath.createOrThrow(OsType.LINUX, "../home/admin/.kscript")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("..", "home", "admin", ".kscript")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - assertThat(OsPath.createOrThrow(OsType.LINUX, "..")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("..")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - //Duplicated separators are accepted - assertThat(OsPath.createOrThrow(OsType.LINUX, "..//home////admin/.kscript/")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("..", "home", "admin", ".kscript")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - //Both types of separator are accepted - assertThat(OsPath.createOrThrow(OsType.LINUX, "..//home\\admin\\.kscript/")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("..", "home", "admin", ".kscript")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - } - - @Test - fun `Normalization of Linux paths`() { - assertThat(OsPath.createOrThrow(OsType.LINUX, "/home/admin/.kscript/../../")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("/", "home")) - it.prop(OsPath::pathType).isEqualTo(PathType.ABSOLUTE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - assertThat(OsPath.createOrThrow(OsType.LINUX, "./././../../script")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("..", "..", "script")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - assertThat(OsPath.createOrThrow(OsType.LINUX, "/a/b/c/../d/script")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("/", "a", "b", "d", "script")) - it.prop(OsPath::pathType).isEqualTo(PathType.ABSOLUTE) - it.prop(OsPath::osType).isEqualTo(OsType.LINUX) - } - - assertThat { OsPath.createOrThrow(OsType.LINUX, "/.kscript/../../") }.isFailure() - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("Path after normalization goes beyond root element: '/.kscript/../../'") - } - - @Test - fun `Test invalid Linux paths`() { - assertThat { OsPath.createOrThrow(OsType.LINUX, "/ad*asdf") }.isFailure() - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("Invalid character '*' in path '/ad*asdf'") - } - - @Test - fun `Test Linux stringPath`() { - assertThat( - OsPath.createOrThrow(OsType.LINUX, "/home/admin/.kscript").stringPath() - ).isEqualTo("/home/admin/.kscript") - assertThat(OsPath.createOrThrow(OsType.LINUX, "/a/b/c/../d/script").stringPath()).isEqualTo("/a/b/d/script") - assertThat(OsPath.createOrThrow(OsType.LINUX, "./././../../script").stringPath()).isEqualTo("../../script") - assertThat(OsPath.createOrThrow(OsType.LINUX, "script/file.txt").stringPath()).isEqualTo("script/file.txt") - } - - @Test - fun `Test Linux resolve`() { - assertThat( - OsPath.createOrThrow(OsType.LINUX, "/").resolve(OsPath.createOrThrow(OsType.LINUX, "./.kscript/")) - .stringPath() - ).isEqualTo("/.kscript") - - assertThat( - OsPath.createOrThrow(OsType.LINUX, "/home/admin/") - .resolve(OsPath.createOrThrow(OsType.LINUX, "./.kscript/")).stringPath() - ).isEqualTo("/home/admin/.kscript") - - assertThat( - OsPath.createOrThrow(OsType.LINUX, "./home/admin/") - .resolve(OsPath.createOrThrow(OsType.LINUX, "./.kscript/")) - .stringPath() - ).isEqualTo("./home/admin/.kscript") - - assertThat( - OsPath.createOrThrow(OsType.LINUX, "../home/admin/") - .resolve(OsPath.createOrThrow(OsType.LINUX, "./.kscript/")) - .stringPath() - ).isEqualTo("../home/admin/.kscript") - - assertThat( - OsPath.createOrThrow(OsType.LINUX, "..").resolve(OsPath.createOrThrow(OsType.LINUX, "./.kscript/")) - .stringPath() - ).isEqualTo("../.kscript") - - assertThat( - OsPath.createOrThrow(OsType.LINUX, ".").resolve(OsPath.createOrThrow(OsType.LINUX, "./.kscript/")) - .stringPath() - ).isEqualTo("./.kscript") - - assertThat { - OsPath.createOrThrow(OsType.LINUX, "./home/admin").resolve(OsPath.createOrThrow(OsType.WINDOWS, ".\\run")) - }.isFailure() - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("Paths from different OS's: 'LINUX' path can not be resolved with 'WINDOWS' path") - - assertThat { - OsPath.createOrThrow(OsType.LINUX, "./home/admin").resolve(OsPath.createOrThrow(OsType.LINUX, "/run")) - }.isFailure() - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("Can not resolve absolute, relative or undefined path './home/admin' using absolute path '/run'") - } - - // ************************************************* WINDOWS PATHS ************************************************* - - @Test - fun `Test Windows paths`() { - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "C:\\")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("C:")) - it.prop(OsPath::pathType).isEqualTo(PathType.ABSOLUTE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "C:\\home\\admin\\.kscript")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("C:", "home", "admin", ".kscript")) - it.prop(OsPath::pathType).isEqualTo(PathType.ABSOLUTE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "")).let { - it.prop(OsPath::pathParts).isEqualTo(emptyList()) - it.prop(OsPath::pathType).isEqualTo(PathType.UNDEFINED) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "file.txt")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("file.txt")) - it.prop(OsPath::pathType).isEqualTo(PathType.UNDEFINED) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - assertThat(OsPath.createOrThrow(OsType.WINDOWS, ".")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf(".")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - assertThat(OsPath.createOrThrow(OsType.WINDOWS, ".\\home\\admin\\.kscript")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf(".", "home", "admin", ".kscript")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "..\\home\\admin\\.kscript")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("..", "home", "admin", ".kscript")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "..")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("..")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - //Duplicated separators are accepted - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "C:\\home\\\\\\\\admin\\.kscript\\")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("C:", "home", "admin", ".kscript")) - it.prop(OsPath::pathType).isEqualTo(PathType.ABSOLUTE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - //Both types of separator are accepted - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "C:/home\\admin/.kscript////")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("C:", "home", "admin", ".kscript")) - it.prop(OsPath::pathType).isEqualTo(PathType.ABSOLUTE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - } - - @Test - fun `Normalization of Windows paths`() { - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "C:\\home\\admin\\.kscript\\..\\..\\")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("C:", "home")) - it.prop(OsPath::pathType).isEqualTo(PathType.ABSOLUTE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - assertThat(OsPath.createOrThrow(OsType.WINDOWS, ".\\.\\.\\..\\..\\script")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("..", "..", "script")) - it.prop(OsPath::pathType).isEqualTo(PathType.RELATIVE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "C:\\a\\b\\c\\..\\d\\script")).let { - it.prop(OsPath::pathParts).isEqualTo(listOf("C:", "a", "b", "d", "script")) - it.prop(OsPath::pathType).isEqualTo(PathType.ABSOLUTE) - it.prop(OsPath::osType).isEqualTo(OsType.WINDOWS) - } - - assertThat { OsPath.createOrThrow(OsType.WINDOWS, "C:\\.kscript\\..\\..\\") }.isFailure() - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("Path after normalization goes beyond root element: 'C:\\.kscript\\..\\..\\'") - } - - @Test - fun `Test invalid Windows paths`() { - assertThat { OsPath.createOrThrow(OsType.WINDOWS, "C:\\adas?df") }.isFailure() - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("Invalid character '?' in path 'C:\\adas?df'") - - assertThat { OsPath.createOrThrow(OsType.WINDOWS, "home:\\vagrant") }.isFailure() - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("Invalid character ':' in path 'home:\\vagrant'") - } - - @Test - fun `Test Windows stringPath`() { - assertThat( - OsPath.createOrThrow(OsType.WINDOWS, "C:\\home\\admin\\.kscript").stringPath() - ).isEqualTo("C:\\home\\admin\\.kscript") - assertThat( - OsPath.createOrThrow(OsType.WINDOWS, "c:\\a\\b\\c\\..\\d\\script").stringPath() - ).isEqualTo("c:\\a\\b\\d\\script") - assertThat( - OsPath.createOrThrow(OsType.WINDOWS, ".\\.\\.\\..\\..\\script").stringPath() - ).isEqualTo("..\\..\\script") - assertThat(OsPath.createOrThrow(OsType.WINDOWS, "script\\file.txt").stringPath()).isEqualTo("script\\file.txt") - } - - // ****************************************** WINDOWS <-> CYGWIN <-> MSYS ****************************************** - - @Test - fun `Test Windows to Cygwin`() { - assertThat( - OsPath.createOrThrow(OsType.WINDOWS, "C:\\home\\admin\\.kscript").convert(OsType.CYGWIN).stringPath() - ).isEqualTo("/cygdrive/c/home/admin/.kscript") - - assertThat( - OsPath.createOrThrow(OsType.WINDOWS, "..\\home\\admin\\.kscript").convert(OsType.CYGWIN).stringPath() - ).isEqualTo("../home/admin/.kscript") - } - - @Test - fun `Test Cygwin to Windows`() { - assertThat( - OsPath.createOrThrow(OsType.CYGWIN, "/cygdrive/c/home/admin/.kscript").convert(OsType.WINDOWS).stringPath() - ).isEqualTo("c:\\home\\admin\\.kscript") - - assertThat( - OsPath.createOrThrow(OsType.CYGWIN, "../home/admin/.kscript").convert(OsType.WINDOWS).stringPath() - ).isEqualTo("..\\home\\admin\\.kscript") - } - - @Test - fun `Test Windows to MSys`() { - assertThat( - OsPath.createOrThrow(OsType.WINDOWS, "C:\\home\\admin\\.kscript").convert(OsType.MSYS).stringPath() - ).isEqualTo("/c/home/admin/.kscript") - - assertThat( - OsPath.createOrThrow(OsType.WINDOWS, "..\\home\\admin\\.kscript").convert(OsType.MSYS).stringPath() - ).isEqualTo("../home/admin/.kscript") - } - - @Test - fun `Test MSys to Windows`() { - assertThat( - OsPath.createOrThrow(OsType.MSYS, "/c/home/admin/.kscript").convert(OsType.WINDOWS).stringPath() - ).isEqualTo("c:\\home\\admin\\.kscript") - - assertThat( - OsPath.createOrThrow(OsType.MSYS, "../home/admin/.kscript").convert(OsType.WINDOWS).stringPath() - ).isEqualTo("..\\home\\admin\\.kscript") - } -} diff --git a/src/test/kotlin/io/github/kscripting/shell/util/SanitizerTest.kt b/src/test/kotlin/io/github/kscripting/shell/util/SanitizerTest.kt new file mode 100644 index 0000000..ea38b7c --- /dev/null +++ b/src/test/kotlin/io/github/kscripting/shell/util/SanitizerTest.kt @@ -0,0 +1,45 @@ +package io.github.kscripting.shell.util + +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.jupiter.api.Test + + +class SanitizerTest { + @Test + fun `Test sanitize`() { + val sanitizer = Sanitizer("" to "<***>") + assertThat(sanitizer.sanitize("This is !!!")).isEqualTo("This is <***>!!!") + } + + @Test + fun `Test swapped`() { + val sanitizer = Sanitizer("" to "<***>").swapped() + assertThat(sanitizer.sanitize("This is <***>!!!")).isEqualTo("This is !!!") + } + + @Test + fun `Test calculatePotentialMatch - corner cases`() { + assertThat(Sanitizer.EMPTY_SANITIZER.calculatePotentialMatch("abcd")).isEqualTo("") + + val sanitizer = Sanitizer("[" to "]") + assertThat(sanitizer.calculatePotentialMatch("abcd[bs[")).isEqualTo("") + } + + @Test + fun `Test calculatePotentialMatch - normal cases`() { + val sanitizer = Sanitizer("" to "\\", "" to "\t", "" to "\n") + + assertThat(sanitizer.calculatePotentialMatch("")).isEqualTo("") + assertThat(sanitizer.calculatePotentialMatch("abcd")).isEqualTo("") + assertThat(sanitizer.calculatePotentialMatch("abcd" to "", "" to "", "" to "") + assertThat(longSanitizer.calculatePotentialMatch("abcd")).isEqualTo("") + assertThat(longSanitizer.calculatePotentialMatch("abcdabcd