blob: 257283d767ee5842df8d68d4108e1bbe4f84bcfe [file]
/*
* Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.jdk7.test
import java.io.File
import java.net.URI
import java.nio.channels.SeekableByteChannel
import java.nio.file.*
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileAttribute
import java.nio.file.attribute.FileAttributeView
import java.nio.file.attribute.UserPrincipalLookupService
import java.nio.file.spi.FileSystemProvider
import kotlin.io.path.*
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
import kotlin.test.assertTrue
class PathRecursiveCopyBetweenIncompatibleFileSystemsTest : AbstractPathTest() {
@Test // Regression test for KT-85020
fun copyBetweenFilesystemsWithIncompatiblePathSeparators() {
val root = createTempDirectory().cleanupRecursively()
val srcDirOriginal = root.resolve("src").also {
it.resolve("a")
.resolve("b")
.resolve("c")
.resolve("test.txt")
.createParentDirectories()
.writeText("hello")
}
val dst = root.resolve("dst").createDirectory()
val mockFs = SharpFileSystemProvider().newTestFileSystem(srcDirOriginal.fileSystem)
val mockedSrc = mockFs.wrap(srcDirOriginal)
mockedSrc.copyToRecursively(dst, followLinks = false, overwrite = false)
val expectedPaths = srcDirOriginal.walk(PathWalkOption.INCLUDE_DIRECTORIES)
.map { it.relativeTo(srcDirOriginal).toString() }
.toSet()
val actualPaths = dst.walk(PathWalkOption.INCLUDE_DIRECTORIES)
.map { it.relativeTo(dst).toString() }
.toSet()
assertEquals(expectedPaths, actualPaths)
}
@Test
fun copyFileWithNameCorrespondingToMultipleSegments() {
val root = createTempDirectory().cleanupRecursively()
val src = root.resolve("src").also {
it.resolve("a")
.resolve("b#test.txt")
.createParentDirectories()
.writeText("hello")
}
val dst = root.resolve("dst").createDirectory()
val mockFs = SharpFileSystemProvider().newTestFileSystem(dst.fileSystem)
val mockedDst = mockFs.wrap(dst)
var failed = false
src.copyToRecursively(mockedDst, followLinks = false, overwrite = false, onError = { srcPath, dstPath, exception ->
assertIs<IllegalFileNameException>(exception)
assertEquals(
src.fileSystem.getPath("a", "b#test.txt").toString(),
srcPath.relativeTo(src).toString()
)
assertEquals("a#b#test.txt", dstPath.relativeTo(mockedDst).toString())
failed = true
OnErrorResult.SKIP_SUBTREE
})
assertTrue(failed, "Expected onError to be called, but it never happened")
}
}
// FileSystemProvider, FileSystem and Path implementations below exists for testing purposes only,
// and solve the only one purpose - wrap an existing file system so that all
// its path will use "#" as a path separator. Thus, the name - SharpSomething.
private class SharpFileSystemProvider : FileSystemProvider() {
private var fileSystem: SharpFileSystem? = null
fun newTestFileSystem(delegateFileSystem: FileSystem): SharpFileSystem {
check(fileSystem == null) { "Only one test filesystem can be registered in the provider" }
return SharpFileSystem(this, delegateFileSystem).also { fileSystem = it }
}
override fun getScheme(): String = "sharp"
override fun newFileSystem(uri: URI, env: MutableMap<String, *>): FileSystem = TODO()
override fun getFileSystem(uri: URI): FileSystem = requireRegisteredFileSystem()
override fun getPath(uri: URI): Path = requireRegisteredFileSystem().getPath(uri.path ?: "")
override fun newByteChannel(
path: Path,
options: MutableSet<out OpenOption>,
vararg attrs: FileAttribute<*>,
): SeekableByteChannel = delegateProvider().newByteChannel(path.toDelegatePath(), options, *attrs)
override fun newDirectoryStream(dir: Path, filter: DirectoryStream.Filter<in Path>): DirectoryStream<Path> {
val testDir = dir as SharpPath
val stream = delegateProvider().newDirectoryStream(testDir.delegatePath) { delegatePath ->
filter.accept(testDir.fileSystem.wrap(delegatePath))
}
return object : DirectoryStream<Path> {
override fun iterator(): MutableIterator<Path> = object : MutableIterator<Path> {
private val iterator = stream.iterator()
override fun hasNext(): Boolean = iterator.hasNext()
override fun next(): Path = testDir.fileSystem.wrap(iterator.next())
override fun remove() = iterator.remove()
}
override fun close() = stream.close()
}
}
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
delegateProvider().createDirectory(dir.toDelegatePath(), *attrs)
}
override fun delete(path: Path) {
delegateProvider().delete(path.toDelegatePath())
}
override fun copy(source: Path, target: Path, vararg options: CopyOption) {
delegateProvider().copy(source.toDelegatePath(), target.toDelegatePath(), *options)
}
override fun move(source: Path, target: Path, vararg options: CopyOption) {
delegateProvider().move(source.toDelegatePath(), target.toDelegatePath(), *options)
}
override fun isSameFile(path: Path, path2: Path): Boolean {
if (path !is SharpPath || path2 !is SharpPath) return false
return delegateProvider().isSameFile(path.toDelegatePath(), path2.toDelegatePath())
}
override fun isHidden(path: Path): Boolean = delegateProvider().isHidden(path.toDelegatePath())
override fun getFileStore(path: Path): FileStore = delegateProvider().getFileStore(path.toDelegatePath())
override fun checkAccess(path: Path, vararg modes: AccessMode) {
delegateProvider().checkAccess(path.toDelegatePath(), *modes)
}
override fun <V : FileAttributeView> getFileAttributeView(path: Path, type: Class<V>, vararg options: LinkOption): V? {
return delegateProvider().getFileAttributeView(path.toDelegatePath(), type, *options)
}
override fun <A : BasicFileAttributes> readAttributes(path: Path, type: Class<A>, vararg options: LinkOption): A {
return delegateProvider().readAttributes(path.toDelegatePath(), type, *options)
}
override fun readAttributes(path: Path, attributes: String, vararg options: LinkOption): MutableMap<String, Any> {
return delegateProvider().readAttributes(path.toDelegatePath(), attributes, *options)
}
override fun setAttribute(path: Path, attribute: String, value: Any, vararg options: LinkOption) {
delegateProvider().setAttribute(path.toDelegatePath(), attribute, value, *options)
}
private fun requireRegisteredFileSystem(): SharpFileSystem {
return fileSystem ?: throw IllegalStateException("No test filesystem is registered in the provider")
}
private fun delegateProvider(): FileSystemProvider = requireRegisteredFileSystem().delegateFileSystem.provider()
}
private class SharpFileSystem(
private val provider: SharpFileSystemProvider,
val delegateFileSystem: FileSystem,
) : FileSystem() {
companion object {
const val PATH_SEPARATOR = "#"
}
override fun provider(): FileSystemProvider = provider
override fun close() = Unit
override fun isOpen(): Boolean = delegateFileSystem.isOpen
override fun isReadOnly(): Boolean = delegateFileSystem.isReadOnly
override fun getSeparator(): String = PATH_SEPARATOR
override fun getRootDirectories(): MutableIterable<Path> =
delegateFileSystem.rootDirectories.mapTo(ArrayList()) { wrap(it) }
override fun getFileStores(): MutableIterable<FileStore> = delegateFileSystem.fileStores
override fun supportedFileAttributeViews(): MutableSet<String> =
delegateFileSystem.supportedFileAttributeViews()
override fun getPath(first: String, vararg more: String): Path {
val convertedFirst = first.fromTestSeparators()
val convertedMore = more.map { it.fromTestSeparators() }.toTypedArray()
return wrap(delegateFileSystem.getPath(convertedFirst, *convertedMore))
}
override fun getPathMatcher(syntaxAndPattern: String): PathMatcher = TODO()
override fun getUserPrincipalLookupService(): UserPrincipalLookupService =
delegateFileSystem.userPrincipalLookupService
override fun newWatchService(): WatchService = TODO()
fun wrap(delegatePath: Path): SharpPath = SharpPath(this, delegatePath)
private fun String.fromTestSeparators(): String =
replace(PATH_SEPARATOR, delegateFileSystem.separator)
}
private class SharpPath(
val fileSystem: SharpFileSystem,
val delegatePath: Path,
) : Path {
override fun getFileSystem(): FileSystem = fileSystem
override fun isAbsolute(): Boolean = delegatePath.isAbsolute
override fun getRoot(): Path? = delegatePath.root?.let(fileSystem::wrap)
override fun getFileName(): Path? = delegatePath.fileName?.let(fileSystem::wrap)
override fun getParent(): Path? = delegatePath.parent?.let(fileSystem::wrap)
override fun getNameCount(): Int = delegatePath.nameCount
override fun getName(index: Int): Path = fileSystem.wrap(delegatePath.getName(index))
override fun subpath(beginIndex: Int, endIndex: Int): Path = fileSystem.wrap(delegatePath.subpath(beginIndex, endIndex))
override fun startsWith(other: Path): Boolean = other is SharpPath && delegatePath.startsWith(other.delegatePath)
override fun startsWith(other: String): Boolean = startsWith(fileSystem.getPath(other))
override fun endsWith(other: Path): Boolean = other is SharpPath && delegatePath.endsWith(other.delegatePath)
override fun endsWith(other: String): Boolean = endsWith(fileSystem.getPath(other))
override fun normalize(): Path = fileSystem.wrap(delegatePath.normalize())
override fun resolve(other: Path): Path {
other as SharpPath
return fileSystem.wrap(delegatePath.resolve(other.delegatePath))
}
override fun resolve(other: String): Path = resolve(fileSystem.getPath(other))
override fun resolveSibling(other: Path): Path {
other as SharpPath
return fileSystem.wrap(delegatePath.resolveSibling(other.delegatePath))
}
override fun resolveSibling(other: String): Path = resolveSibling(fileSystem.getPath(other))
override fun relativize(other: Path): Path = fileSystem.wrap(delegatePath.relativize((other as SharpPath).delegatePath))
override fun toUri(): URI = URI("${fileSystem.provider().scheme}:///${toString()}")
override fun toAbsolutePath(): Path = fileSystem.wrap(delegatePath.toAbsolutePath())
override fun toRealPath(vararg options: LinkOption): Path = fileSystem.wrap(delegatePath.toRealPath(*options))
override fun toFile(): File = delegatePath.toFile()
override fun register(
watcher: WatchService,
events: Array<out WatchEvent.Kind<*>>,
vararg modifiers: WatchEvent.Modifier,
): WatchKey = TODO()
override fun register(watcher: WatchService, vararg events: WatchEvent.Kind<*>): WatchKey = TODO()
override fun iterator(): MutableIterator<Path> =
delegatePath.mapTo(ArrayList()) { fileSystem.wrap(it) }.iterator()
override fun compareTo(other: Path): Int = delegatePath.compareTo((other as SharpPath).delegatePath)
override fun equals(other: Any?): Boolean =
other is SharpPath && fileSystem == other.fileSystem && delegatePath == other.delegatePath
override fun hashCode(): Int = 31 * fileSystem.hashCode() + delegatePath.hashCode()
override fun toString(): String =
delegatePath.toString().replace(fileSystem.delegateFileSystem.separator, SharpFileSystem.PATH_SEPARATOR)
}
private fun Path.toDelegatePath(): Path = (this as SharpPath).delegatePath