1 /* <lambda>null2 * Copyright (C) 2023 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package okio 17 18 import kotlin.wasm.unsafe.Pointer 19 import kotlin.wasm.unsafe.withScopedMemoryAllocator 20 import okio.Path.Companion.toPath 21 import okio.internal.ErrnoException 22 import okio.internal.fdClose 23 import okio.internal.preview1.Errno 24 import okio.internal.preview1.dirnamelen 25 import okio.internal.preview1.fd 26 import okio.internal.preview1.fd_prestat_dir_name 27 import okio.internal.preview1.fd_prestat_get 28 import okio.internal.preview1.fd_readdir 29 import okio.internal.preview1.fdflags 30 import okio.internal.preview1.fdflags_append 31 import okio.internal.preview1.filetype 32 import okio.internal.preview1.filetype_directory 33 import okio.internal.preview1.filetype_regular_file 34 import okio.internal.preview1.filetype_symbolic_link 35 import okio.internal.preview1.oflag_creat 36 import okio.internal.preview1.oflag_directory 37 import okio.internal.preview1.oflag_excl 38 import okio.internal.preview1.oflag_trunc 39 import okio.internal.preview1.oflags 40 import okio.internal.preview1.path_create_directory 41 import okio.internal.preview1.path_filestat_get 42 import okio.internal.preview1.path_open 43 import okio.internal.preview1.path_readlink 44 import okio.internal.preview1.path_remove_directory 45 import okio.internal.preview1.path_rename 46 import okio.internal.preview1.path_symlink 47 import okio.internal.preview1.path_unlink_file 48 import okio.internal.preview1.right_fd_filestat_get 49 import okio.internal.preview1.right_fd_filestat_set_size 50 import okio.internal.preview1.right_fd_read 51 import okio.internal.preview1.right_fd_readdir 52 import okio.internal.preview1.right_fd_seek 53 import okio.internal.preview1.right_fd_sync 54 import okio.internal.preview1.right_fd_write 55 import okio.internal.preview1.rights 56 import okio.internal.readString 57 import okio.internal.write 58 59 /** 60 * Use [WASI] to implement the Okio file system interface. 61 * 62 * [WASI]: https://wasi.dev/ 63 */ 64 object WasiFileSystem : FileSystem() { 65 private val preopens: List<Preopen> = buildList { 66 // File descriptor of the first preopen in the `WASI` instance's configured `preopens` property. 67 // This is 3 by default, assuming `stdin` is 0, `stdout` is 1, and `stderr` is 2. Other preopens 68 // are assigned sequentially starting at this value. 69 val firstPreopen = 3 70 71 withScopedMemoryAllocator { allocator -> 72 val bufSize = 2048 73 val bufPointer = allocator.allocate(bufSize) 74 75 for (fd in firstPreopen..Int.MAX_VALUE) { 76 val getReturnPointer = allocator.allocate(12) 77 78 val getErrno = fd_prestat_get(fd, getReturnPointer.address.toInt()) 79 if (getErrno == Errno.badf.ordinal) break // No more preopens. 80 if (getErrno != 0) throw ErrnoException(getErrno.toShort()) 81 82 val size = (getReturnPointer + 4).loadInt() 83 require(size + 1 < bufSize) { "unexpected preopen size: $size" } 84 val dirNameErrno = fd_prestat_dir_name(fd, bufPointer.address.toInt(), size + 1) 85 if (dirNameErrno != 0) throw ErrnoException(dirNameErrno.toShort()) 86 val dirName = bufPointer.readString(size) 87 val dirNamePath = dirName.toPath() 88 add(Preopen(dirNamePath, dirNamePath.segmentsBytes, fd)) 89 } 90 } 91 } 92 93 private val relativePathPreopen: Preopen = preopens.firstOrNull() 94 ?: throw IllegalStateException("no preopens") 95 96 override fun canonicalize(path: Path): Path { 97 val absolutePath = when { 98 path.isAbsolute -> path 99 else -> relativePathPreopen.path.resolve(path, normalize = true) 100 } 101 102 // There's no APIs in preview1 to canonicalize a path. We give it a best effort by resolving 103 // all symlinks, but this could result in a relative path. 104 val result = resolveSymlinks(absolutePath, 0).normalized() 105 106 check(result.isAbsolute) { 107 "Canonicalize $path returned non-absolute path: $result" 108 } 109 110 return result 111 } 112 113 private fun resolveSymlinks( 114 path: Path, 115 recurseCount: Int = 0, 116 ): Path { 117 // 40 is chosen for consistency with the Linux kernel (which previously used 8). 118 if (recurseCount > 40) throw IOException("symlink cycle?") 119 120 val parent = path.parent 121 val resolvedParent = when { 122 parent != null -> resolveSymlinks(parent, recurseCount + 1) 123 else -> null 124 } 125 val pathWithResolvedParent = when { 126 resolvedParent != null -> resolvedParent.resolve(path.name) 127 else -> path 128 } 129 130 val symlinkTarget = metadata(pathWithResolvedParent).symlinkTarget 131 ?: return pathWithResolvedParent 132 133 val resolvedSymlinkTarget = when { 134 symlinkTarget.isAbsolute -> symlinkTarget 135 resolvedParent != null -> resolvedParent.resolve(symlinkTarget) 136 else -> symlinkTarget 137 } 138 139 return resolveSymlinks(resolvedSymlinkTarget, recurseCount + 1) 140 } 141 142 override fun metadataOrNull(path: Path): FileMetadata? { 143 withScopedMemoryAllocator { allocator -> 144 val returnPointer = allocator.allocate(64) 145 val preopen = preopenForPath(path) ?: return null 146 val (pathAddress, pathSize) = allocator.write(path.toString()) 147 148 val errno = path_filestat_get( 149 fd = preopen.fd, 150 flags = 0, 151 path = pathAddress.address.toInt(), 152 pathSize = pathSize, 153 returnPointer = returnPointer.address.toInt(), 154 ) 155 156 when (errno) { 157 // 'notcapable' means our preopens don't cover this path. This will happen for paths 158 // like '/' that are an ancestor of our preopens. 159 Errno.notcapable.ordinal -> return FileMetadata(isDirectory = true) 160 Errno.noent.ordinal -> return null 161 } 162 163 if (errno != 0) throw ErrnoException(errno.toShort()) 164 165 // Skip device, offset 0. 166 // Skip ino, offset 8. 167 val filetype: filetype = (returnPointer + 16).loadByte() 168 // Skip nlink, offset 24. 169 val filesize: Long = (returnPointer + 32).loadLong() 170 val atim: Long = (returnPointer + 40).loadLong() // Access time, Nanoseconds. 171 val mtim: Long = (returnPointer + 48).loadLong() // Modification time, Nanoseconds. 172 val ctim: Long = (returnPointer + 56).loadLong() // Status change time, Nanoseconds. 173 174 val symlinkTarget: Path? = when (filetype) { 175 filetype_symbolic_link -> { 176 val bufLen = filesize.toInt() + 1 177 val bufPointer = allocator.allocate(bufLen) 178 val readlinkReturnPointer = allocator.allocate(4) // `size` is u32, 4 bytes. 179 val readlinkErrno = path_readlink( 180 fd = preopen.fd, 181 path = pathAddress.address.toInt(), 182 pathSize = pathSize, 183 buf = bufPointer.address.toInt(), 184 buf_len = bufLen, 185 returnPointer = readlinkReturnPointer.address.toInt(), 186 ) 187 if (readlinkErrno != 0) throw ErrnoException(readlinkErrno.toShort()) 188 val symlinkSize = readlinkReturnPointer.loadInt() 189 val symlink = bufPointer.readString(symlinkSize) 190 symlink.toPath() 191 } 192 193 else -> null 194 } 195 196 return FileMetadata( 197 isRegularFile = filetype == filetype_regular_file, 198 isDirectory = filetype == filetype_directory, 199 symlinkTarget = symlinkTarget, 200 size = filesize, 201 createdAtMillis = ctim / 1_000_000L, // Nanos to millis. 202 lastModifiedAtMillis = mtim / 1_000_000L, // Nanos to millis. 203 lastAccessedAtMillis = atim / 1_000_000L, // Nanos to millis. 204 ) 205 } 206 } 207 208 override fun list(dir: Path): List<Path> { 209 val fd = pathOpen( 210 path = dir, 211 oflags = oflag_directory, 212 rightsBase = right_fd_readdir, 213 ) 214 try { 215 return list(dir, fd) 216 } finally { 217 fdClose(fd) 218 } 219 } 220 221 override fun listOrNull(dir: Path): List<Path>? { 222 // TODO: stop using exceptions for flow control. 223 try { 224 return list(dir) 225 } catch (e: FileNotFoundException) { 226 return null 227 } catch (e: ErrnoException) { 228 if (e.errno == Errno.notdir) return null 229 throw e 230 } 231 } 232 233 private fun list(dir: Path, fd: fd): List<Path> { 234 withScopedMemoryAllocator { allocator -> 235 // In theory, fd_readdir uses a 'cookie' field to page through results. In practice the 236 // NodeJS implementation doesn't honor the cookie and directories with large file names 237 // don't progress. Instead, just grow the buffer until the entire directory fits. 238 var bufSize = 2048 239 var bufPointer = allocator.allocate(bufSize) 240 val returnPointer = allocator.allocate(4) // `size` is u32, 4 bytes. 241 var pageSize: Int 242 while (true) { 243 val errno = fd_readdir( 244 fd = fd, 245 buf = bufPointer.address.toInt(), 246 buf_len = bufSize, 247 cookie = 0L, // Don't bother with dircookie, it doesn't work for large file names. 248 returnPointer = returnPointer.address.toInt(), 249 ) 250 251 if (errno != 0) throw ErrnoException(errno.toShort()) 252 pageSize = returnPointer.loadInt() 253 254 if (pageSize < bufSize) break 255 256 bufSize *= 4 257 bufPointer = allocator.allocate(bufSize) 258 } 259 260 // Parse dirent records from the buffer. 261 var pos = bufPointer 262 val limit = bufPointer + pageSize 263 val result = mutableListOf<Path>() 264 while (pos.address < limit.address) { 265 pos += 8 // Skip dircookie. 266 pos += 8 // Skip inode. 267 val d_namelen: dirnamelen = pos.loadInt() 268 pos += 4 // Consume d_namelen. 269 pos += 4 // Skip d_type. 270 271 val name = pos.readString(d_namelen) 272 pos += d_namelen 273 274 result += dir / name 275 } 276 277 result.sort() 278 return result 279 } 280 } 281 282 override fun openReadOnly(file: Path): FileHandle { 283 val rightsBase = right_fd_filestat_get or 284 right_fd_read or 285 right_fd_seek or 286 right_fd_sync 287 val fd = pathOpen( 288 path = file, 289 oflags = 0, 290 rightsBase = rightsBase, 291 ) 292 return WasiFileHandle(fd, readWrite = false) 293 } 294 295 override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { 296 val oflags = when { 297 mustCreate && mustExist -> { 298 throw IllegalArgumentException("Cannot require mustCreate and mustExist at the same time.") 299 } 300 mustCreate -> oflag_creat or oflag_excl 301 mustExist -> 0 302 else -> oflag_creat 303 } 304 val rightsBase = right_fd_filestat_get or 305 right_fd_filestat_set_size or 306 right_fd_read or 307 right_fd_seek or 308 right_fd_sync or 309 right_fd_write 310 val fd = pathOpen( 311 path = file, 312 oflags = oflags, 313 rightsBase = rightsBase, 314 ) 315 return WasiFileHandle(fd, readWrite = true) 316 } 317 318 override fun source(file: Path): Source { 319 return FileSource( 320 fd = pathOpen( 321 path = file, 322 oflags = 0, 323 rightsBase = right_fd_read, 324 ), 325 ) 326 } 327 328 override fun sink(file: Path, mustCreate: Boolean): Sink { 329 val oflags = when { 330 mustCreate -> oflag_creat or oflag_excl or oflag_trunc 331 else -> oflag_creat or oflag_trunc 332 } 333 334 return FileSink( 335 fd = pathOpen( 336 path = file, 337 oflags = oflags, 338 rightsBase = right_fd_write or right_fd_sync, 339 ), 340 ) 341 } 342 343 override fun appendingSink(file: Path, mustExist: Boolean): Sink { 344 val oflags = when { 345 mustExist -> 0 346 else -> oflag_creat 347 } 348 349 return FileSink( 350 fd = pathOpen( 351 path = file, 352 oflags = oflags, 353 rightsBase = right_fd_write, 354 fdflags = fdflags_append, 355 ), 356 ) 357 } 358 359 override fun createDirectory(dir: Path, mustCreate: Boolean) { 360 withScopedMemoryAllocator { allocator -> 361 val (pathAddress, pathSize) = allocator.write(dir.toString()) 362 363 val errno = path_create_directory( 364 fd = preopenForPath(dir)?.fd ?: throw FileNotFoundException("no preopen: $dir"), 365 path = pathAddress.address.toInt(), 366 pathSize = pathSize, 367 ) 368 if (errno == Errno.exist.ordinal) { 369 if (mustCreate) throw IOException("already exists: $dir") 370 return 371 } 372 373 if (errno != 0) throw ErrnoException(errno.toShort()) 374 } 375 } 376 377 override fun atomicMove(source: Path, target: Path) { 378 withScopedMemoryAllocator { allocator -> 379 val (sourcePathAddress, sourcePathSize) = allocator.write(source.toString()) 380 val (targetPathAddress, targetPathSize) = allocator.write(target.toString()) 381 382 val errno = path_rename( 383 fd = preopenForPath(source)?.fd ?: throw FileNotFoundException("no preopen: $source"), 384 old_path = sourcePathAddress.address.toInt(), 385 old_pathSize = sourcePathSize, 386 new_fd = preopenForPath(target)?.fd ?: throw FileNotFoundException("no preopen: $target"), 387 new_path = targetPathAddress.address.toInt(), 388 new_pathSize = targetPathSize, 389 ) 390 if (errno == Errno.noent.ordinal) { 391 throw FileNotFoundException("no such file: $source") 392 } 393 394 if (errno != 0) throw ErrnoException(errno.toShort()) 395 } 396 } 397 398 override fun delete(path: Path, mustExist: Boolean) { 399 withScopedMemoryAllocator { allocator -> 400 val (pathAddress, pathSize) = allocator.write(path.toString()) 401 val preopenFd = preopenForPath(path) ?: throw FileNotFoundException("no preopen: $path") 402 403 var errno = path_unlink_file( 404 fd = preopenFd.fd, 405 path = pathAddress.address.toInt(), 406 pathSize = pathSize, 407 ) 408 // If unlink failed, try remove_directory. 409 when (errno) { 410 Errno.noent.ordinal -> { 411 if (mustExist) throw FileNotFoundException("no such file: $path") 412 return // Nothing to delete. 413 } 414 415 Errno.perm.ordinal, 416 Errno.isdir.ordinal, 417 -> { 418 errno = path_remove_directory( 419 fd = preopenFd.fd, 420 path = pathAddress.address.toInt(), 421 pathSize = pathSize, 422 ) 423 } 424 } 425 if (errno != 0) throw ErrnoException(errno.toShort()) 426 } 427 } 428 429 override fun createSymlink(source: Path, target: Path) { 430 withScopedMemoryAllocator { allocator -> 431 val sourcePreopen = preopenForPath(source) 432 ?: throw FileNotFoundException("no preopen: $source") 433 434 // Always create symlinks relative to their source. Absolute symlinks are trouble because the 435 // absolute paths used by WASI are different from the absolute paths on the host file system. 436 val sourceParent = source.parent 437 ?: throw IOException("unexpected symlink source: $source") 438 val targetRelative = when { 439 target.isRelative -> target 440 else -> target.relativeTo(sourceParent) 441 } 442 443 val (sourcePathAddress, sourcePathSize) = allocator.write(source.toString()) 444 val (targetPathAddress, targetPathSize) = allocator.write(targetRelative.toString()) 445 446 val errno = path_symlink( 447 old_path = targetPathAddress.address.toInt(), 448 old_pathSize = targetPathSize, 449 fd = sourcePreopen.fd, 450 new_path = sourcePathAddress.address.toInt(), 451 new_pathSize = sourcePathSize, 452 ) 453 if (errno != 0) throw ErrnoException(errno.toShort()) 454 } 455 } 456 457 private fun pathOpen( 458 path: Path, 459 oflags: oflags, 460 rightsBase: rights, 461 fdflags: fdflags = 0, 462 ): fd { 463 withScopedMemoryAllocator { allocator -> 464 val preopenFd = preopenForPath(path) ?: throw FileNotFoundException("no preopen: $path") 465 val (pathAddress, pathSize) = allocator.write(path.toString()) 466 467 val returnPointer: Pointer = allocator.allocate(4) // fd is u32. 468 val errno = path_open( 469 fd = preopenFd.fd, 470 dirflags = 0, 471 path = pathAddress.address.toInt(), 472 pathSize = pathSize, 473 oflags = oflags, 474 fs_rights_base = rightsBase, 475 fs_rights_inheriting = 0, 476 fdflags = fdflags, 477 returnPointer = returnPointer.address.toInt(), 478 ) 479 if (errno == Errno.noent.ordinal) { 480 throw FileNotFoundException("no such file: $path") 481 } 482 if (errno != 0) throw ErrnoException(errno.toShort()) 483 return returnPointer.loadInt() 484 } 485 } 486 487 /** 488 * Returns the preopen whose path is either an ancestor of [path], or whose path [path] is an 489 * ancestor of. 490 * 491 * If [path] is an ancestor of our preopen, then operating on the path will ultimately fail with a 492 * `notcapable` errno. 493 */ 494 private fun preopenForPath(path: Path): Preopen? { 495 if (path.isRelative) return relativePathPreopen 496 497 val pathSegmentsBytes = path.segmentsBytes 498 499 return preopens.firstOrNull { preopen -> 500 val commonSize = minOf(pathSegmentsBytes.size, preopen.segmentsBytes.size) 501 preopen.segmentsBytes.subList(0, commonSize) == pathSegmentsBytes.subList(0, commonSize) 502 } 503 } 504 505 override fun toString() = "okio.WasiFileSystem" 506 507 private class Preopen( 508 val path: Path, 509 val segmentsBytes: List<ByteString>, 510 val fd: fd, 511 ) 512 } 513