Skip to content

Commit 95d1e4c

Browse files
authored
Support JS IR target (#151)
Supported JS IR target
1 parent 6a172a7 commit 95d1e4c

File tree

10 files changed

+673
-11
lines changed

10 files changed

+673
-11
lines changed

core/build.gradle.kts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ kotlin {
2020
}
2121
}
2222

23+
js(IR) {
24+
nodejs {
25+
testTask {
26+
useMocha {
27+
timeout = "30s"
28+
}
29+
}
30+
}
31+
browser {
32+
testTask {
33+
filter.setExcludePatterns("*SmokeFileTest*")
34+
useMocha {
35+
timeout = "30s"
36+
}
37+
}
38+
}
39+
}
40+
2341
configureNativePlatforms()
2442
sourceSets {
2543
val commonMain by getting

core/common/src/files/Paths.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import kotlinx.io.Source
1111
/**
1212
* A wrapper around a string representing a file path allowing to read from and write to a
1313
* corresponding file using [Sink] and [Source].
14+
*
15+
* **This API is unstable and subject to change.**
1416
*/
1517
public expect class Path
1618

core/common/test/files/SmokeFileTest.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55

66
package kotlinx.io.files
77

8-
import kotlinx.io.createTempFile
9-
import kotlinx.io.deleteFile
10-
import kotlinx.io.readUtf8Line
11-
import kotlinx.io.writeUtf8
8+
import kotlinx.io.*
129
import kotlin.test.AfterTest
1310
import kotlin.test.BeforeTest
1411
import kotlin.test.Test
@@ -31,6 +28,7 @@ class SmokeFileTest {
3128
@Test
3229
fun testBasicFile() {
3330
val path = Path(tempFile!!)
31+
3432
path.sink().use {
3533
it.writeUtf8("example")
3634
}
@@ -39,4 +37,20 @@ class SmokeFileTest {
3937
assertEquals("example", it.readUtf8Line())
4038
}
4139
}
40+
41+
@OptIn(ExperimentalStdlibApi::class)
42+
@Test
43+
fun testReadWriteMultipleSegments() {
44+
val path = Path(tempFile!!)
45+
46+
val data = ByteArray((Segment.SIZE * 2.5).toInt()) { it.toByte() }
47+
48+
path.sink().use {
49+
it.write(data)
50+
}
51+
52+
path.source().use {
53+
assertArrayEquals(data, it.readByteArray())
54+
}
55+
}
4256
}

core/js/src/-PlatfromJs.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.io
7+
8+
import kotlinx.io.internal.commonAsUtf8ToByteArray
9+
10+
internal actual fun String.asUtf8ToByteArray(): ByteArray = commonAsUtf8ToByteArray()
11+
12+
public actual open class IOException actual constructor(
13+
message: String?,
14+
cause: Throwable?
15+
) : Exception(message, cause) {
16+
public actual constructor(message: String?) : this(message, null)
17+
}
18+
19+
public actual open class EOFException actual constructor(message: String?) : IOException(message)

core/js/src/RawSink.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.io
7+
8+
@OptIn(ExperimentalStdlibApi::class)
9+
public actual interface RawSink : AutoCloseableAlias {
10+
public actual fun write(source: Buffer, byteCount: Long)
11+
12+
public actual fun flush()
13+
14+
actual override fun close()
15+
}

core/js/src/SegmentPool.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.io
7+
8+
internal actual object SegmentPool {
9+
actual val MAX_SIZE: Int = 0
10+
11+
actual val byteCount: Int = 0
12+
13+
actual fun take(): Segment = Segment()
14+
15+
actual fun recycle(segment: Segment) {
16+
}
17+
}

core/js/src/files/PathsJs.kt

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.io.files
7+
8+
import kotlinx.io.*
9+
10+
private val fs: dynamic
11+
get(): dynamic {
12+
return try {
13+
js("require('fs')")
14+
} catch (t: Throwable) {
15+
null
16+
}
17+
}
18+
19+
private val buffer: dynamic
20+
get(): dynamic {
21+
return try {
22+
js("require('buffer')")
23+
} catch (t: Throwable) {
24+
null
25+
}
26+
}
27+
28+
public actual class Path internal constructor(private val path: String,
29+
@Suppress("UNUSED_PARAMETER") any: Any?) {
30+
override fun toString(): String = path
31+
}
32+
33+
public actual fun Path(path: String): Path {
34+
return Path(path, null)
35+
}
36+
37+
public actual fun Path.source(): Source {
38+
check(fs !== null) { "Module 'fs' was not found" }
39+
return FileSource(this).buffered()
40+
}
41+
42+
public actual fun Path.sink(): Sink {
43+
check(fs !== null) { "Module 'fs' was not found" }
44+
check(buffer !== null) { "Module 'buffer' was not found" }
45+
return FileSink(this).buffered()
46+
}
47+
48+
private class FileSource(private val path: Path) : RawSource {
49+
private var buffer: dynamic = null
50+
private var closed = false
51+
private var offset = 0
52+
53+
@OptIn(ExperimentalUnsignedTypes::class)
54+
override fun readAtMostTo(sink: Buffer, byteCount: Long): Long {
55+
check(!closed) { "Source is closed." }
56+
if (byteCount == 0L) {
57+
return 0
58+
}
59+
if (buffer === null) {
60+
buffer = fs.readFileSync(path.toString(), null)
61+
}
62+
val len: Int = buffer.length as Int
63+
if (offset >= len) {
64+
return -1L
65+
}
66+
val bytesToRead = minOf(byteCount, (len - offset))
67+
for (i in 0 until bytesToRead) {
68+
sink.writeByte(buffer.readInt8(offset++) as Byte)
69+
}
70+
71+
return bytesToRead
72+
}
73+
74+
override fun close() {
75+
closed = true
76+
}
77+
}
78+
79+
private class FileSink(private val path: Path) : RawSink {
80+
private var closed = false
81+
private var append = false
82+
83+
override fun write(source: Buffer, byteCount: Long) {
84+
check(!closed) { "Sink is closed." }
85+
if (byteCount == 0L) {
86+
return
87+
}
88+
89+
var remainingBytes = minOf(byteCount, source.size)
90+
while (remainingBytes > 0) {
91+
val head = source.head!!
92+
val segmentBytes = head.limit - head.pos
93+
val buf = buffer.Buffer.allocUnsafe(segmentBytes)
94+
buf.fill(head.data, head.pos, segmentBytes)
95+
if (append) {
96+
fs.appendFileSync(path.toString(), buf)
97+
} else {
98+
fs.writeFileSync(path.toString(), buf)
99+
append = true
100+
}
101+
102+
source.skip(segmentBytes.toLong())
103+
remainingBytes -= segmentBytes
104+
}
105+
}
106+
107+
override fun flush() = Unit
108+
109+
override fun close() {
110+
closed = true
111+
}
112+
}

core/js/test/utils.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.io
7+
8+
import kotlin.random.Random
9+
import kotlin.random.nextULong
10+
11+
private val os: dynamic
12+
get(): dynamic {
13+
return try {
14+
js("require('os')")
15+
} catch (t: Throwable) {
16+
null
17+
}
18+
}
19+
20+
private val fs: dynamic
21+
get(): dynamic {
22+
return try {
23+
js("require('fs')")
24+
} catch (t: Throwable) {
25+
null
26+
}
27+
}
28+
29+
private val nodePath: dynamic
30+
get(): dynamic {
31+
return try {
32+
js("require('path')")
33+
} catch (t: Throwable) {
34+
null
35+
}
36+
}
37+
38+
actual fun createTempFile(): String {
39+
while (true) {
40+
val tmpdir = os.tmpdir()
41+
val filename = Random.nextULong().toString()
42+
val fullpath = "$tmpdir${nodePath.sep}$filename.txt"
43+
44+
if (fs.existsSync(fullpath) as Boolean) {
45+
continue
46+
}
47+
return fullpath
48+
}
49+
}
50+
51+
actual fun deleteFile(path: String) {
52+
if (!fs.existsSync(path) as Boolean) {
53+
throw IOException("File does not exist: $path")
54+
}
55+
fs.rmSync(path)
56+
}

core/native/src/files/PathsNative.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ public actual class Path internal constructor(internal val path: String,
1919
public actual fun Path(path: String): Path = Path(path, null)
2020

2121
public actual fun Path.source(): Source {
22-
val openFile: CPointer<FILE> = fopen(path, "r")
22+
val openFile: CPointer<FILE> = fopen(path, "rb")
2323
?: throw IOException("Failed to open $path with ${strerror(errno)?.toKString()}")
2424
return FileSource(openFile).buffered()
2525
}
2626

2727
public actual fun Path.sink(): Sink {
28-
val openFile: CPointer<FILE> = fopen(path, "w")
28+
val openFile: CPointer<FILE> = fopen(path, "wb")
2929
?: throw IOException("Failed to open $path with ${strerror(errno)?.toKString()}")
3030
return FileSink(openFile).buffered()
3131
}

0 commit comments

Comments
 (0)