我为 okio 编写了一个 pull request…
省流:support non UTF-8 mingw filesystem by magic-cucumber · Pull Request #1758 · square/okio · GitHub
前情提要
使用 kotlin-native,搭配 clikt 和 okio 写小玩具时,在自己的 mac 上运行的好好的,但是在好盆友的电脑上就报错…
相信各位佬们看到这里一定会会心一笑。毕竟就算是我在看到这种低级错误时的第一反应也是:在程序里转个码不就行了么?
但是如果问题就这么简单的话这个 pr 就不会诞生了…
踩坑记录
一开始以为是编码问题,让好盆友使用 chcp 65001 切换到 utf-8 编码,报错。
然后引入了 fleeksoft-charset 库,并自己编写平台函数实现编码检测,然后将路径字符串转换为正确的编码后再进行处理,依然报错
在 jvm-windows 环境下执行程序,不报错。于是将怀疑的目光转向了 okio 库本身…
源码窥探
根据报错堆栈,问题出在 commonMetadata() 函数:
@Throws(IOException::class) internal fun FileSystem.commonMetadata(path: Path): FileMetadata {
return metadataOrNull(path) ?: throw FileNotFoundException("no such file: $path")
}
由于报错仅发生在 windows 平台,而 kotlin-native 在 windows 上使用 minGW,故查看 PosixFileSystem
这里是一层包装方法,为 unix/mingw 提供了不同的实现,我们仅关注 mingw 部分。
override fun metadataOrNull(path: Path) = variantMetadataOrNull(path)
internal actual fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata? {
return memScoped {
val stat = alloc<_stat64>()
if (_stat64(path.toString(), stat.ptr) != 0) {
if (errno == ENOENT) return null throw errnoToIOException(errno)
}
return@memScoped FileMetadata(
isRegularFile = stat.st_mode.toInt() and S_IFMT == S_IFREG,
isDirectory = stat.st_mode.toInt() and S_IFMT == S_IFDIR,
symlinkTarget = null,
size = stat.st_size,
createdAtMillis = stat.st_ctime * 1000L,
lastModifiedAtMillis = stat.st_mtime * 1000L,
lastAccessedAtMillis = stat.st_atime * 1000L,
)
}
}
可以看到它的底层是调用了 stat64 函数 (尽管它不是那么的标准),而问题在于 stat 系列函数要求传入窄字符,对于 posix-api,有专门的_w 前缀函数提供了宽字符版本。
许多 Windows API 函数具有 “A”(ANSI)和 “W”(宽、Unicode)版本。 “A” 版本基于 Windows 代码页处理文本,而 “W” 版本处理 Unicode 文本。
既然如此,我们只需要将其改写成 宽字符 版本的就可以了
if (_wstat64(path.toString().wcstr, stat.ptr) != 0) {
… 吗?
我单独写了一个测试,在本机提前创建中文字符文件,并调用 metadataOrNull() 函数。完美运行,并且没有任何错误
然而当我将编译的 snapshot 包发布到本地供我的小玩具引用时,它还是报了一模一样的错,,只不过被报错的文件从我输入的文件夹变成了其中的一个子文件。
这只能说明 okio.Path 本身就有问题。
请允许我我贴一下我编写的业务函数:
fun Path.walk(consumer: (Path) -> Unit) {
if (isFile) {
consumer(this)
return
}
for (child in list()) {
child.walk(consumer)
}
}
val Path.isFile
get() = metadata().isRegularFile
fun Path.list() = FileSystem.SYSTEM.list(this)
我的程序涉及到递归遍历文件夹,而正好遍历到子文件就报错了,相信佬 u 们一定会将目光聚集到 list() 函数。
private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? {
val opendir = opendir(dir.toString())
?: if (throwOnFailure) throw errnoToIOException(errno) else return null try {
val result = mutableListOf<Path>()
val buffer = Buffer()
set_posix_errno(0) // If readdir() returns null it's either the end or an error. while (true) {
val dirent: CPointer<dirent> = readdir(opendir) ?: break val childPath = buffer.writeNullTerminated(
bytes = dirent[0].d_name,
).toPath(normalize = true)
if (childPath == SELF_DIRECTORY_ENTRY || childPath == PARENT_DIRECTORY_ENTRY) {
continue // exclude '.' and '..' from the results.
}
result += dir / childPath
}
if (errno != 0) {
if (throwOnFailure) {
throw errnoToIOException(errno)
} else {
return null
}
}
result.sort()
return result
} finally {
closedir(opendir) // Ignore errno from closedir.
}
}
注意到这个 list 函数是直接写在 PosixFileSystem.kt 里的,它没有针对 unix/mingw 的变种,且仍然使用了 窄字符 api。因此要想修复它也很简单,将这个函数仿照其他的函数,根据 unix/mingw 拆分为对应的变种。
受限于篇幅,这里只贴出 windows 部分的函数
internal actual fun PosixFileSystem.variantList(dir: Path, throwOnFailure: Boolean): List<Path>? {
val opendir = _wopendir(dir.toString().wcstr)
?: if (throwOnFailure) throw errnoToIOException(errno) else return null try {
val result = mutableListOf<Path>()
set_posix_errno(0) // If readdir() returns null it's either the end or an error. while (true) {
val dirent: CPointer<_wdirent> = _wreaddir(opendir) ?: break val childPath = dirent[0].d_name.toKString().toPath()
if (childPath == SELF_DIRECTORY_ENTRY || childPath == PARENT_DIRECTORY_ENTRY) {
continue // exclude '.' and '..' from the results.
}
result += dir / childPath
}
if (errno != 0) {
if (throwOnFailure) {
throw errnoToIOException(errno)
} else {
return null
}
}
result.sort()
return result
} finally {
_wclosedir(opendir) // Ignore errno from closedir.
}
}
其他的函数是否也存在这个问题呢?答案是有的,这里便不再赘述。
具体参考话题上方的 PR 链接。
彩蛋
我们都知道,在 windows 系统中,如果如果该文件被占用,则删除此文件的操作会失败。
而在我调试我的小玩具中,当删除临时文件失败后,报错信息居然也是乱码?
于是我在这里贴出负责读取 IO 错误 并 抛出异常的代码…
internal fun errnoToIOException(errno: Int): IOException {
val message = strerror(errno)
val messageString = if (message != null) {
Buffer().writeNullTerminated(message).readUtf8()
} else {
"errno: $errno"
}
return when (errno) {
ENOENT -> FileNotFoundException(messageString)
else -> IOException(messageString)
}
}
internal fun Buffer.writeNullTerminated(bytes: CPointer<ByteVarOf<Byte>>): Buffer = apply {
var pos = 0 while (true) {
val byte = bytes[pos++].toInt()
if (byte == 0) {
break
} else {
writeByte(byte)
}
}
}
可以看出来它也是直接调用的窄字符 api。但问题是谁能想到 message 是本地化的文本呢…
至此,本帖正式完结

