diff --git a/core/api/kotlinx-io-core.api b/core/api/kotlinx-io-core.api index e28bc59cd..25b6fa3a0 100644 --- a/core/api/kotlinx-io-core.api +++ b/core/api/kotlinx-io-core.api @@ -91,6 +91,7 @@ public abstract interface annotation class kotlinx/io/InternalIoApi : java/lang/ public final class kotlinx/io/JvmCoreKt { public static final fun asSink (Ljava/io/OutputStream;)Lkotlinx/io/RawSink; public static final fun asSource (Ljava/io/InputStream;)Lkotlinx/io/RawSource; + public static final fun getSystemLineSeparator ()Ljava/lang/String; } public abstract interface class kotlinx/io/RawSink : java/io/Flushable, java/lang/AutoCloseable { diff --git a/core/api/kotlinx-io-core.klib.api b/core/api/kotlinx-io-core.klib.api index 02d9050a7..c3e48dc9c 100644 --- a/core/api/kotlinx-io-core.klib.api +++ b/core/api/kotlinx-io-core.klib.api @@ -233,6 +233,8 @@ final val kotlinx.io.unsafe/SegmentReadContextImpl // kotlinx.io.unsafe/SegmentR final fun (): kotlinx.io.unsafe/SegmentReadContext // kotlinx.io.unsafe/SegmentReadContextImpl.|(){}[0] final val kotlinx.io.unsafe/SegmentWriteContextImpl // kotlinx.io.unsafe/SegmentWriteContextImpl|{}SegmentWriteContextImpl[0] final fun (): kotlinx.io.unsafe/SegmentWriteContext // kotlinx.io.unsafe/SegmentWriteContextImpl.|(){}[0] +final val kotlinx.io/SystemLineSeparator // kotlinx.io/SystemLineSeparator|{}SystemLineSeparator[0] + final fun (): kotlin/String // kotlinx.io/SystemLineSeparator.|(){}[0] final fun (kotlinx.io.files/Path).kotlinx.io.files/sink(): kotlinx.io/Sink // kotlinx.io.files/sink|sink@kotlinx.io.files.Path(){}[0] final fun (kotlinx.io.files/Path).kotlinx.io.files/source(): kotlinx.io/Source // kotlinx.io.files/source|source@kotlinx.io.files.Path(){}[0] diff --git a/core/apple/src/-PlatformApple.kt b/core/apple/src/-PlatformApple.kt new file mode 100644 index 000000000..1686fb31d --- /dev/null +++ b/core/apple/src/-PlatformApple.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. + */ + +package kotlinx.io + +/** + * Sequence of characters used as a line separator by the underlying platform. + * + * The value of this property is always `"\n"`. + */ +public actual val SystemLineSeparator: String = "\n" diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 2799ec449..b47ed2b8e 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -3,6 +3,8 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. */ +@file:OptIn(ExperimentalWasmDsl::class) + import org.gradle.internal.os.OperatingSystem import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl @@ -29,10 +31,23 @@ kotlin { useMocha { timeout = "300s" } + filter.setExcludePatterns("kotlinx.io.files.*") + } + } + } + wasmJs { + nodejs() + /* + browser { + testTask { + useKarma { + // how to configure browsers available in CI? + } + filter.setExcludePatterns("kotlinx.io.files.*") } } + */ } - @OptIn(ExperimentalWasmDsl::class) wasmWasi { nodejs { testTask { diff --git a/core/common/src/-CommonPlatform.kt b/core/common/src/-CommonPlatform.kt index a0c1e75d9..21db552a7 100644 --- a/core/common/src/-CommonPlatform.kt +++ b/core/common/src/-CommonPlatform.kt @@ -38,3 +38,8 @@ public expect open class EOFException : IOException { public constructor() public constructor(message: String?) } + +/** + * True if the underlying platform is Windows (assuming that it's possible to get this info). + */ +internal expect val isWindows: Boolean diff --git a/core/common/src/Core.kt b/core/common/src/Core.kt index be72be491..05977a5c9 100644 --- a/core/common/src/Core.kt +++ b/core/common/src/Core.kt @@ -43,3 +43,12 @@ private class DiscardingSink : RawSink { override fun flush() {} override fun close() {} } + +/** + * Sequence of characters used as a line separator by the underlying platform. + * + * The value of this property is platform-specific, but usually it is either `"\n"` or `"\r\n"`. + * + * See the documentation for a particular platform to get more information about the value of this property. + */ +public expect val SystemLineSeparator: String diff --git a/core/common/src/files/FileSystem.kt b/core/common/src/files/FileSystem.kt index 46c40af65..2a2ff704b 100644 --- a/core/common/src/files/FileSystem.kt +++ b/core/common/src/files/FileSystem.kt @@ -214,5 +214,3 @@ public expect class FileNotFoundException(message: String?) : IOException internal const val WindowsPathSeparator: Char = '\\' internal const val UnixPathSeparator: Char = '/' - -internal expect val isWindows: Boolean diff --git a/core/common/test/LineSeparatorTest.kt b/core/common/test/LineSeparatorTest.kt new file mode 100644 index 000000000..ca03a9874 --- /dev/null +++ b/core/common/test/LineSeparatorTest.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. + */ + +package kotlinx.io + +import kotlinx.io.isWindows +import kotlin.test.Test +import kotlin.test.assertEquals + +class LineSeparatorTest { + @Test + public fun testLineSeparator() { + if (isWindows) { + assertEquals("\r\n", SystemLineSeparator) + } else { + assertEquals("\n", SystemLineSeparator) + } + } +} diff --git a/core/common/test/files/SmokeFileTestWindows.kt b/core/common/test/files/SmokeFileTestWindows.kt index 23994807e..5c1178419 100644 --- a/core/common/test/files/SmokeFileTestWindows.kt +++ b/core/common/test/files/SmokeFileTestWindows.kt @@ -5,6 +5,7 @@ package kotlinx.io.files +import kotlinx.io.isWindows import kotlin.test.* class SmokeFileTestWindows { diff --git a/core/js/src/-PlatformJs.kt b/core/js/src/-PlatformJs.kt index 7e198e25c..5f868d230 100644 --- a/core/js/src/-PlatformJs.kt +++ b/core/js/src/-PlatformJs.kt @@ -31,3 +31,27 @@ internal actual fun withCaughtException(block: () -> Unit): Throwable? { return t } } + +/** + * Sequence of characters used as a line separator by the underlying platform. + * + * Value of this property depends on [Navigator.platform](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform): + * it is `"\r\n"` on Windows, and `"\n"` on all other platforms. + */ +public actual val SystemLineSeparator: String by lazy { + if (isWindows) { + "\r\n" + } else { + "\n" + } +} + +internal actual val isWindows: Boolean by lazy { + getPlatformName().startsWith("Win", ignoreCase = true) +} + +private fun getPlatformName(): String = js( + """ + (typeof navigator !== "undefined" && navigator.platform) || "unknown" + """ +) diff --git a/core/jvm/src/-JvmPlatform.kt b/core/jvm/src/-JvmPlatform.kt index 92e17424a..0d65280ef 100644 --- a/core/jvm/src/-JvmPlatform.kt +++ b/core/jvm/src/-JvmPlatform.kt @@ -24,3 +24,5 @@ package kotlinx.io public actual typealias IOException = java.io.IOException public actual typealias EOFException = java.io.EOFException + +internal actual val isWindows: Boolean = System.getProperty("os.name")?.startsWith("Windows") ?: false diff --git a/core/jvm/src/JvmCore.kt b/core/jvm/src/JvmCore.kt index e714f7740..a556c8916 100644 --- a/core/jvm/src/JvmCore.kt +++ b/core/jvm/src/JvmCore.kt @@ -109,3 +109,10 @@ internal val AssertionError.isAndroidGetsocknameError: Boolean get() { return cause != null && message?.contains("getsockname failed") ?: false } + +/** + * Sequence of characters used as a line separator by the underlying platform. + * + * Returns the same value as [System.lineSeparator]. + */ +public actual val SystemLineSeparator: String = System.lineSeparator() diff --git a/core/jvm/src/files/FileSystemJvm.kt b/core/jvm/src/files/FileSystemJvm.kt index b85ebdb7e..d19e75216 100644 --- a/core/jvm/src/files/FileSystemJvm.kt +++ b/core/jvm/src/files/FileSystemJvm.kt @@ -113,5 +113,3 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() public actual val SystemTemporaryDirectory: Path = Path(System.getProperty("java.io.tmpdir")) public actual typealias FileNotFoundException = java.io.FileNotFoundException - -internal actual val isWindows: Boolean = System.getProperty("os.name")?.startsWith("Windows") ?: false diff --git a/core/jvm/test/files/SmokeFileTestWindowsJVM.kt b/core/jvm/test/files/SmokeFileTestWindowsJVM.kt index 76d6ec4bc..d873a68b8 100644 --- a/core/jvm/test/files/SmokeFileTestWindowsJVM.kt +++ b/core/jvm/test/files/SmokeFileTestWindowsJVM.kt @@ -5,6 +5,7 @@ package kotlinx.io.files +import kotlinx.io.isWindows import kotlin.test.Test import kotlin.test.assertEquals diff --git a/core/mingw/src/-PlatformMingw.kt b/core/mingw/src/-PlatformMingw.kt new file mode 100644 index 000000000..c1ab9e3c7 --- /dev/null +++ b/core/mingw/src/-PlatformMingw.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. + */ + +package kotlinx.io + +/** + * Sequence of characters used as a line separator by the underlying platform. + * + * The value of this property is always `"\r\n"`. + */ +public actual val SystemLineSeparator: String = "\r\n" diff --git a/core/native/src/-NonJvmPlatform.kt b/core/native/src/-NonJvmPlatform.kt index 9294c3190..e25daf3c7 100644 --- a/core/native/src/-NonJvmPlatform.kt +++ b/core/native/src/-NonJvmPlatform.kt @@ -20,6 +20,8 @@ */ package kotlinx.io +import kotlin.experimental.ExperimentalNativeApi + public actual open class IOException : Exception { public actual constructor() : super() @@ -37,3 +39,6 @@ public actual open class EOFException : IOException { public constructor(message: String?, cause: Throwable?) : super(message, cause) } + +@OptIn(ExperimentalNativeApi::class) +internal actual val isWindows: Boolean = Platform.osFamily == OsFamily.WINDOWS diff --git a/core/native/src/files/FileSystemNative.kt b/core/native/src/files/FileSystemNative.kt index 1fa719b15..9a1ce9385 100644 --- a/core/native/src/files/FileSystemNative.kt +++ b/core/native/src/files/FileSystemNative.kt @@ -121,9 +121,6 @@ public actual open class FileNotFoundException actual constructor( // 777 in octal, rwx for all (owner, group and others). internal const val PermissionAllowAll: UShort = 511u -@OptIn(ExperimentalNativeApi::class) -internal actual val isWindows: Boolean = Platform.osFamily == OsFamily.WINDOWS - internal expect class OpaqueDirEntry : AutoCloseable { fun readdir(): String? override fun close() diff --git a/core/nodeFilesystemShared/src/files/FileSystemNodeJs.kt b/core/nodeFilesystemShared/src/files/FileSystemNodeJs.kt index 810b5ba02..ba36e42b0 100644 --- a/core/nodeFilesystemShared/src/files/FileSystemNodeJs.kt +++ b/core/nodeFilesystemShared/src/files/FileSystemNodeJs.kt @@ -128,4 +128,3 @@ public actual open class FileNotFoundException actual constructor( message: String?, ) : IOException(message) -internal actual val isWindows = os.platform() == "win32" diff --git a/core/nodeFilesystemShared/src/node/os.kt b/core/nodeFilesystemShared/src/node/os.kt index 431d60a8f..6a55a0691 100644 --- a/core/nodeFilesystemShared/src/node/os.kt +++ b/core/nodeFilesystemShared/src/node/os.kt @@ -16,6 +16,11 @@ internal external interface Os { * See https://nodejs.org/api/os.html#osplatform */ fun platform(): String + + /** + * See https://nodejs.org/api/os.html#oseol + */ + val EOL: String } internal expect val os: Os diff --git a/core/nodeFilesystemShared/test/files/SmokeFileTestWindowsNodeJs.kt b/core/nodeFilesystemShared/test/files/SmokeFileTestWindowsNodeJs.kt index bb84a4d03..d85fdaa0d 100644 --- a/core/nodeFilesystemShared/test/files/SmokeFileTestWindowsNodeJs.kt +++ b/core/nodeFilesystemShared/test/files/SmokeFileTestWindowsNodeJs.kt @@ -5,6 +5,7 @@ package kotlinx.io.files +import kotlinx.io.isWindows import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull diff --git a/core/unix/src/-PlatformUnix.kt b/core/unix/src/-PlatformUnix.kt new file mode 100644 index 000000000..1686fb31d --- /dev/null +++ b/core/unix/src/-PlatformUnix.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. + */ + +package kotlinx.io + +/** + * Sequence of characters used as a line separator by the underlying platform. + * + * The value of this property is always `"\n"`. + */ +public actual val SystemLineSeparator: String = "\n" diff --git a/core/wasmJs/src/-PlatformWasmJs.kt b/core/wasmJs/src/-PlatformWasmJs.kt index d93280360..b3fabfc3b 100644 --- a/core/wasmJs/src/-PlatformWasmJs.kt +++ b/core/wasmJs/src/-PlatformWasmJs.kt @@ -23,3 +23,25 @@ private fun catchJsThrowable(block: () -> Unit): JsAny? = js("""{ } }""") +/** + * Sequence of characters used as a line separator by the underlying platform. + * + * Value of this property depends on [Navigator.platform](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform): + * it is `"\r\n"` on Windows, and `"\n"` on all other platforms. + */ +public actual val SystemLineSeparator: String by lazy { + if (isWindows) { + "\r\n" + } else { + "\n" + } +} + +@JsFun(""" + () => (typeof navigator !== "undefined" && navigator.platform) || "unknown" +""") +private external fun getPlatformName(): String + +internal actual val isWindows by lazy { + getPlatformName().startsWith("Win", ignoreCase = true) +} diff --git a/core/wasmWasi/src/-PlatformWasmWasi.kt b/core/wasmWasi/src/-PlatformWasmWasi.kt new file mode 100644 index 000000000..4087fa4ad --- /dev/null +++ b/core/wasmWasi/src/-PlatformWasmWasi.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. + */ + +package kotlinx.io + +/** + * Sequence of characters used as a line separator by the underlying platform. + * + * The value of this property is always `"\n"`. + */ +public actual val SystemLineSeparator: String = "\n" + +/** + * The property affects paths processing and [SystemLineSeparator]. + * In Wasi, paths are always '/'-delimited, so it does not really affect paths processing. + * As of [SystemLineSeparator], it seems like there's not so much we can do right now. + */ +internal actual val isWindows: Boolean get() = false diff --git a/core/wasmWasi/src/files/FileSystemWasm.kt b/core/wasmWasi/src/files/FileSystemWasm.kt index 252fbe30b..8ab0860c2 100644 --- a/core/wasmWasi/src/files/FileSystemWasm.kt +++ b/core/wasmWasi/src/files/FileSystemWasm.kt @@ -372,9 +372,6 @@ public actual open class FileNotFoundException actual constructor( message: String?, ) : IOException(message) -// The property affects only paths processing and in Wasi paths are always '/'-delimited. -internal actual val isWindows: Boolean get() = false - internal object PreOpens { data class PreOpen(val path: Path, val fd: Int) diff --git a/core/wasmWasi/test/WasiFsTest.kt b/core/wasmWasi/test/WasiFsTest.kt index 40a835be5..9296f666a 100644 --- a/core/wasmWasi/test/WasiFsTest.kt +++ b/core/wasmWasi/test/WasiFsTest.kt @@ -7,6 +7,7 @@ package kotlinx.io.files import kotlinx.io.IOException import kotlinx.io.buffered +import kotlinx.io.isWindows import kotlinx.io.readLine import kotlinx.io.writeString import kotlin.test.*