Last active
October 12, 2025 19:36
-
-
Save iseki0/eb4495e72d65fd10c0dce408241d0fae to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @file:OptIn(ExperimentalForeignApi::class) | |
| package path | |
| import kotlinx.cinterop.ExperimentalForeignApi | |
| import platform.windows.GetLogicalDrives | |
| internal object WindowsFileSystem : FileSystem { | |
| override val separator: String get() = "\\" | |
| override val roots: List<Path> | |
| get() = run { | |
| var mask = GetLogicalDrives() | |
| List(mask.countOneBits()) { | |
| val ch = 'A' + mask.takeLowestOneBit().countTrailingZeroBits() | |
| mask = mask and mask.takeLowestOneBit().inv() | |
| WindowsPath.of("""$ch:\""") | |
| } | |
| } | |
| } | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @file:OptIn(ExperimentalForeignApi::class) | |
| package path | |
| import kotlinx.cinterop.ExperimentalForeignApi | |
| import kotlinx.cinterop.allocArray | |
| import kotlinx.cinterop.convert | |
| import kotlinx.cinterop.memScoped | |
| import kotlinx.cinterop.toKString | |
| import platform.windows.CloseHandle | |
| import platform.windows.CreateFileW | |
| import platform.windows.FILE_FLAG_BACKUP_SEMANTICS | |
| import platform.windows.FILE_READ_ATTRIBUTES | |
| import platform.windows.FILE_SHARE_DELETE | |
| import platform.windows.FILE_SHARE_READ | |
| import platform.windows.FILE_SHARE_WRITE | |
| import platform.windows.GetFinalPathNameByHandleW | |
| import platform.windows.GetFullPathNameW | |
| import platform.windows.INVALID_HANDLE_VALUE | |
| import platform.windows.MAX_PATH | |
| import platform.windows.OPEN_EXISTING | |
| import platform.windows.PathIsRelativeW | |
| import platform.windows.TRUE | |
| import platform.windows.WCHARVar | |
| internal class WindowsPath : Path { | |
| internal val value: String | |
| val filenameIdx: Int | |
| private constructor(value: String) { | |
| this.value = value | |
| filenameIdx = WindowsPathUtil.getFilenameIndex(value) | |
| } | |
| companion object { | |
| internal fun ofNormalized(path: String) = WindowsPath(path) | |
| internal fun of(path: String) = ofNormalized(WindowsPathUtil.normalizePath(path, collapse = false)) | |
| } | |
| override val fileSystem: FileSystem | |
| get() = WindowsFileSystem | |
| override val isAbsolute: Boolean get() = PathIsRelativeW(value) != TRUE | |
| override val parent: Path? | |
| get() = if (filenameIdx == -1) null else of(value.substring(0, filenameIdx)) | |
| override val filename: String? | |
| get() = if (filenameIdx != -1) value.substring(filenameIdx) else null | |
| override fun join(vararg other: String): Path = other.fold(value) { base, other -> | |
| WindowsPathUtil.joinPath(base, WindowsPathUtil.normalizePath(other, collapse = false)) | |
| }.let(::of) | |
| override fun normalization(): Path = ofNormalized(WindowsPathUtil.normalizePath(value)) | |
| override fun toAbsolute(): Path = ofNormalized(getFullPath(value)) | |
| override fun evalSymlink(): Path = of(evalSymlink(value)) | |
| override fun equals(other: Any?): Boolean { | |
| if (this === other) return true | |
| if (other == null || this::class != other::class) return false | |
| other as WindowsPath | |
| return value == other.value | |
| } | |
| override fun hashCode(): Int { | |
| return value.hashCode() | |
| } | |
| override fun toString(): String { | |
| return value | |
| } | |
| } | |
| private fun getFullPath(s: String): String { | |
| memScoped { | |
| var buf = allocArray<WCHARVar>(MAX_PATH) | |
| var n = GetFullPathNameW(s, MAX_PATH.convert(), buf, null) | |
| if (n >= MAX_PATH.convert()) { | |
| buf = allocArray<WCHARVar>(n.convert()) | |
| n = GetFullPathNameW(s, n, buf, null) | |
| } | |
| if (n == 0u) { | |
| throw InvalidPathException(s, formatErrorCode()) | |
| } | |
| return buf.toKString() | |
| } | |
| } | |
| private fun evalSymlink(s: String): String { | |
| memScoped { | |
| val h = CreateFileW( | |
| s, FILE_READ_ATTRIBUTES.toUInt(), (FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE).toUInt(), | |
| lpSecurityAttributes = null, | |
| dwCreationDisposition = OPEN_EXISTING.toUInt(), | |
| dwFlagsAndAttributes = FILE_FLAG_BACKUP_SEMANTICS.toUInt(), | |
| hTemplateFile = null, | |
| ) | |
| if (h == INVALID_HANDLE_VALUE) { | |
| throw translateIOError(file = s) | |
| } | |
| try { | |
| var buf = allocArray<WCHARVar>(MAX_PATH) | |
| var n = GetFinalPathNameByHandleW(h, buf, MAX_PATH.convert(), 0u) | |
| if (n >= MAX_PATH.convert()) { | |
| buf = allocArray<WCHARVar>(n.convert()) | |
| n = GetFinalPathNameByHandleW(h, buf, n, 0u) | |
| } | |
| if (n == 0u) { | |
| error("GetFinalPathNameByHandleW failed: ${formatErrorCode()}") | |
| } | |
| return buf.toKString() | |
| } finally { | |
| CloseHandle(h) | |
| } | |
| } | |
| } | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package path | |
| import kotlin.jvm.JvmInline | |
| internal data object WindowsPathUtil { | |
| private fun invalidPath( | |
| input: CharSequence, reason: String, | |
| ) = InvalidPathException(input.toString(), reason) | |
| private fun Char.isSeparator() = this == '\\' || this == '/' | |
| private fun Char.isWinDriveLetter() = this in 'A'..'Z' || this in 'a'..'z' | |
| private fun Char.isPathForbidden(): Boolean = when (this) { | |
| '<', '>', ':', '"', '/', '\\', '|', '?', '*' -> true | |
| else -> this.code in 0x00..0x1F | |
| } | |
| private fun CharSequence.endWithDoubleDotSegment(index: Int): Boolean = | |
| this[index] == '.' && index - 2 > 0 && this[index - 1] == '.' && this[index - 2].isPathForbidden() | |
| private fun CharSequence.endWithSingleDotSegment(index: Int): Boolean = | |
| this[index] == '.' && index - 1 > 0 && this[index - 1].isPathForbidden() | |
| private fun CharSequence.findLastSeparatorInPath(end: Int, start: Int): Int { | |
| for (i in end downTo start) { | |
| if (this[i].isSeparator()) return i | |
| if (this[i].isPathForbidden()) throwForbiddenCharInPath(i) | |
| } | |
| return -1 | |
| } | |
| private fun CharSequence.throwForbiddenCharInPath(index: Int): Nothing { | |
| throw invalidPath(this, "Filename contains forbidden character at $index: ${this[index]}") | |
| } | |
| private fun CharSequence.findPathStartOfUnc(isLong: Boolean): Int { | |
| // Determine the prefix length: \\?\UNC\ (8) for long UNC paths, \\ (2) for normal UNC paths | |
| val startIndex = if (isLong) 8 else 2 | |
| if (length <= startIndex) throw invalidPath(this, "UNC path is missing hostname") | |
| var i = startIndex | |
| // Scans a single UNC segment (e.g., server name or share name) until '\' or end of string. | |
| // Also checks for any forbidden characters. | |
| fun scanSegment(start: Int): Int { | |
| var pos = start | |
| while (pos < length) { | |
| val ch = this[pos] | |
| if (ch == '\\') break | |
| if (ch.isPathForbidden()) { | |
| throwForbiddenCharInPath(pos) | |
| } | |
| pos++ | |
| } | |
| return pos | |
| } | |
| // Step 1: Scan the server name segment | |
| i = scanSegment(i) | |
| if (i == startIndex) { | |
| throw invalidPath(this, "The UNC path is missing a server name.") | |
| } | |
| // Step 2: There must be a '\' after the server segment | |
| if (i >= length || this[i] != '\\') return -1 | |
| i++ // Skip '\' | |
| // Step 3: Scan the share name segment | |
| val shareStart = i | |
| i = scanSegment(i) | |
| if (i == shareStart) { | |
| throw invalidPath(this, "The UNC path is missing a share name.") | |
| } | |
| return i | |
| } | |
| @JvmInline | |
| internal value class PathProp private constructor(private val v: ULong) { | |
| companion object { | |
| const val IS_LONG_OFF = 40 | |
| const val IS_UNC_OFF = 41 | |
| const val HAS_DOS_PREFIX = 42 | |
| const val IS_ABS_OFF = 43 | |
| const val IS_NO_PATH_OFF = 44 | |
| private inline val Boolean.l: ULong get() = if (this) 1uL else 0uL | |
| fun analyze(input: CharSequence): PathProp { | |
| val isLongPath: Boolean | |
| val isUncPath: Boolean | |
| var start = 0 | |
| when { | |
| input.startsWith("""\\?\UNC\""") -> { | |
| start = input.findPathStartOfUnc(isLong = true) | |
| isLongPath = true | |
| isUncPath = true | |
| } | |
| input.startsWith("""\\?\""") -> { | |
| start = 4 | |
| isLongPath = true | |
| isUncPath = false | |
| } | |
| input.length >= 2 && input[0].isSeparator() && input[1].isSeparator() -> { | |
| start = input.findPathStartOfUnc(isLong = false) | |
| isLongPath = false | |
| isUncPath = true | |
| } | |
| else -> { | |
| isLongPath = false | |
| isUncPath = false | |
| } | |
| } | |
| val hasDosPrefix: Boolean | |
| if (!isUncPath && input.length >= start + 2 && input[start].isWinDriveLetter() && input[start + 1] == ':') { | |
| start += 2 | |
| hasDosPrefix = true | |
| } else { | |
| hasDosPrefix = false | |
| } | |
| val isAbsolute = start <= input.lastIndex && input[start].isSeparator() | |
| val isNoPath = start > input.lastIndex | |
| var v = start.toUInt().toULong() | |
| v = v or (isLongPath.l shl IS_LONG_OFF) | |
| v = v or (isUncPath.l shl IS_UNC_OFF) | |
| v = v or (hasDosPrefix.l shl HAS_DOS_PREFIX) | |
| v = v or (isAbsolute.l shl IS_ABS_OFF) | |
| v = v or (isNoPath.l shl IS_NO_PATH_OFF) | |
| return PathProp(v) | |
| } | |
| } | |
| private fun testb(off: Int) = (v shr off and 1uL) != 0uL | |
| /** | |
| * Path has long path prefix (e.g., `\\?\`). | |
| */ | |
| val isLong: Boolean get() = testb(IS_LONG_OFF) | |
| /** | |
| * Path is a UNC path (e.g., `\\server\share`). | |
| */ | |
| val isUNC: Boolean get() = testb(IS_UNC_OFF) | |
| /** | |
| * Path has DOS drive letter prefix (e.g., `C:`). | |
| */ | |
| val hasDosPrefix: Boolean get() = testb(HAS_DOS_PREFIX) | |
| /** | |
| * Path is absolute (e.g., `C:\folder` or `\\server\share`). | |
| */ | |
| val isAbs: Boolean get() = testb(IS_ABS_OFF) | |
| /** | |
| * Path contains no segments (e.g., `C:` or `\\server\share`). | |
| */ | |
| val isNoPath: Boolean get() = testb(IS_NO_PATH_OFF) | |
| /** | |
| * The index of the first character after the prefix (e.g., after `C:` or `\\server\share`). | |
| */ | |
| val start: Int get() = v.toUInt().toInt() | |
| } | |
| fun normalizePath(input: CharSequence, collapse: Boolean = true): String { | |
| if (input.isEmpty()) return "" | |
| val buf = CharArray(input.length + 1) | |
| val props = PathProp.analyze(input) | |
| val start = props.start | |
| val isLongPath = props.isLong | |
| val isUncPath = props.isUNC | |
| val hasDosPrefix = props.hasDosPrefix | |
| val isAbsolute = props.isAbs | |
| val isNoPath = props.isNoPath | |
| if (!isAbsolute && isUncPath && !isNoPath) { | |
| throw invalidPath(input, "UNC path must be absolute") | |
| } | |
| if (!isAbsolute && isLongPath) { | |
| throw invalidPath(input, "Long path prefix is only supported for absolute paths") | |
| } | |
| var isSep = true | |
| var p = buf.lastIndex | |
| var i = input.lastIndex | |
| var skipLevel = 0 | |
| while (i >= start) { | |
| val ch = input[i] | |
| if (ch.isSeparator() && isSep) { | |
| i-- | |
| continue | |
| } | |
| if (ch.isSeparator()) { | |
| buf[p--] = '\\' | |
| i-- | |
| isSep = true | |
| continue | |
| } | |
| if (ch.isPathForbidden()) { | |
| input.throwForbiddenCharInPath(i) | |
| } | |
| if (ch == '.' && collapse) { | |
| if (input.endWithDoubleDotSegment(i)) { | |
| skipLevel++ | |
| i -= 3 // skip \.. | |
| continue | |
| } | |
| if (input.endWithSingleDotSegment(i)) { | |
| i -= 2 // skip \. | |
| continue | |
| } | |
| } | |
| if (skipLevel > 0) { | |
| val ii = input.findLastSeparatorInPath(i, start) | |
| i = if (ii == -1) { | |
| // cannot go back anymore | |
| start - 1 | |
| } else { | |
| ii - 1 | |
| } | |
| skipLevel-- | |
| continue | |
| } | |
| isSep = ch.isSeparator() | |
| buf[p--] = ch | |
| i-- | |
| } | |
| if (!isAbsolute) { | |
| repeat(skipLevel) { | |
| buf[p--] = '.' | |
| buf[p--] = '.' | |
| if (it != skipLevel - 1) { | |
| buf[p--] = '\\' | |
| } | |
| } | |
| } | |
| // handle the prefix \, check is abs | |
| val alreadyWriteSlash = p + 1 <= buf.lastIndex && buf[p + 1] == '\\' | |
| if (isAbsolute && !alreadyWriteSlash || isNoPath) { | |
| buf[p--] = '\\' | |
| } else if (!isAbsolute && alreadyWriteSlash) { | |
| buf[p++] = 0.toChar() | |
| } | |
| // copy the prefix | |
| for (i in start - 1 downTo 0) { | |
| buf[p--] = input[i] | |
| } | |
| if (hasDosPrefix && buf[p + start - 1].isLowerCase()) { | |
| buf[p + start - 1] = buf[p + start - 1].uppercaseChar() | |
| } | |
| val offset = p + 1 | |
| return buf.concatToString(offset).let(::normalizePathForInMaxPathConvention) | |
| } | |
| private fun normalizePathForInMaxPathConvention(input: CharSequence): String { | |
| if (input.isEmpty()) return "" | |
| val props = PathProp.analyze(input) | |
| if (!props.isAbs) return input.toString() | |
| if (props.isLong) { | |
| if (props.isUNC) { | |
| if (input.length < 264) { | |
| return '\\' + input.substring(7) | |
| } | |
| return input.toString() | |
| } | |
| if (input.length < 264) { | |
| return input.substring(4) | |
| } | |
| return input.toString() | |
| } | |
| if (input.length >= 260) { | |
| if (props.isUNC) { | |
| return """\\?\UNC\""" + input.substring(2) | |
| } | |
| return """\\?\$input""" | |
| } | |
| return input.toString() | |
| } | |
| fun getFilenameIndex(path: CharSequence): Int { | |
| val props = PathProp.analyze(path) | |
| if (props.isNoPath) return -1 | |
| val i = path.lastIndexOf('\\') | |
| if (props.start == path.lastIndex && path[props.start] == '\\') return -1 | |
| if (i < props.start) { | |
| return props.start | |
| } | |
| return i + 1 | |
| } | |
| fun joinPath(base: CharSequence, other: CharSequence): String { | |
| if (other.isEmpty()) { | |
| return base.toString() | |
| } | |
| if (base.isEmpty()) { | |
| return other.toString() | |
| } | |
| var base = base | |
| var other = other | |
| val otherProps = PathProp.analyze(other) | |
| val baseProps = PathProp.analyze(base) | |
| val otherDriver = otherProps.start.let { other.take(it) } | |
| val baseDriver = baseProps.start.let { base.take(it) } | |
| if (otherProps.isAbs) { | |
| if (otherDriver != "" || baseDriver == "") { | |
| return other.toString() | |
| } | |
| base = baseDriver | |
| } else { | |
| if (otherDriver == baseDriver) { | |
| other = other.drop(otherProps.start) | |
| } else if (otherDriver != "") { | |
| return other.toString() | |
| } | |
| } | |
| return when { | |
| base.endsWith('\\') && other.startsWith('\\') -> "$base${other.substring(1)}" | |
| base.endsWith('\\') || other.startsWith('\\') -> "$base$other" | |
| else -> "$base\\$other" | |
| } | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment