Skip to content

Commit b2863bb

Browse files
Implement path_rename
1 parent fa86bf4 commit b2863bb

File tree

7 files changed

+123
-3
lines changed

7 files changed

+123
-3
lines changed

Sources/SystemExtras/FileAtOperations.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,57 @@ extension FileDescriptor {
319319
}
320320
#endif
321321
}
322+
323+
/// Rename a file or directory relative to directory file descriptors
324+
///
325+
/// - Parameters:
326+
/// - oldPath: The relative location of the file or directory to rename.
327+
/// - newDirFd: The directory file descriptor for the new path.
328+
/// - newPath: The relative destination path to which to rename the file or directory.
329+
///
330+
/// The corresponding C function is `renameat`.
331+
@_alwaysEmitIntoClient
332+
public func rename(
333+
at oldPath: FilePath,
334+
to newDirFd: FileDescriptor,
335+
at newPath: FilePath
336+
) throws {
337+
try oldPath.withPlatformString { cOldPath in
338+
try newPath.withPlatformString { cNewPath in
339+
try _rename(at: cOldPath, to: newDirFd, at: cNewPath).get()
340+
}
341+
}
342+
}
343+
344+
/// Rename a file or directory relative to directory file descriptors
345+
///
346+
/// - Parameters:
347+
/// - oldPath: The relative location of the file or directory to rename.
348+
/// - newDirFd: The directory file descriptor for the new path.
349+
/// - newPath: The relative destination path to which to rename the file or directory.
350+
///
351+
/// The corresponding C function is `renameat`.
352+
@_alwaysEmitIntoClient
353+
public func rename(
354+
at oldPath: UnsafePointer<CInterop.PlatformChar>,
355+
to newDirFd: FileDescriptor,
356+
at newPath: UnsafePointer<CInterop.PlatformChar>
357+
) throws {
358+
try _rename(at: oldPath, to: newDirFd, at: newPath).get()
359+
}
360+
361+
@usableFromInline
362+
internal func _rename(
363+
at oldPath: UnsafePointer<CInterop.PlatformChar>,
364+
to newDirFd: FileDescriptor,
365+
at newPath: UnsafePointer<CInterop.PlatformChar>
366+
) -> Result<(), Errno> {
367+
#if os(Windows)
368+
return .failure(Errno(rawValue: ERROR_NOT_SUPPORTED))
369+
#else
370+
return nothingOrErrno(retryOnInterrupt: false) {
371+
system_renameat(self.rawValue, oldPath, newDirFd.rawValue, newPath)
372+
}
373+
#endif
374+
}
322375
}

Sources/SystemExtras/Syscalls.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ internal func system_unlinkat(
9797
return unlinkat(fd, path, flags)
9898
}
9999

100+
// renameat
101+
internal func system_renameat(
102+
_ oldfd: Int32, _ oldpath: UnsafePointer<CInterop.PlatformChar>,
103+
_ newfd: Int32, _ newpath: UnsafePointer<CInterop.PlatformChar>
104+
) -> CInt {
105+
return renameat(oldfd, oldpath, newfd, newpath)
106+
}
107+
100108
// ftruncate
101109
internal func system_ftruncate(_ fd: Int32, _ size: off_t) -> CInt {
102110
return ftruncate(fd, size)

Sources/WASI/FileSystem.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ protocol WASIDir: WASIEntry {
6161
func removeDirectory(atPath path: String) throws
6262
func removeFile(atPath path: String) throws
6363
func symlink(from sourcePath: String, to destPath: String) throws
64+
func rename(from sourcePath: String, toDir newDir: any WASIDir, to destPath: String) throws
6465
func readEntries(cookie: WASIAbi.DirCookie) throws -> AnyIterator<Result<ReaddirElement, any Error>>
6566
func attributes(path: String, symlinkFollow: Bool) throws -> WASIAbi.Filestat
6667
func setFilestatTimes(

Sources/WASI/Platform/Directory.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,44 @@ extension DirEntry: WASIDir, FdWASIEntry {
117117
}
118118
}
119119

120+
func rename(from sourcePath: String, toDir newDir: any WASIDir, to destPath: String) throws {
121+
#if os(Windows)
122+
throw WASIAbi.Errno.ENOSYS
123+
#else
124+
guard let newDir = newDir as? Self else {
125+
throw WASIAbi.Errno.EBADF
126+
}
127+
128+
// As a special case, rename ignores a trailing slash rather than treating
129+
// it as equivalent to a trailing slash-dot, so strip any trailing slashes
130+
// for the purposes of openParent.
131+
let oldHasTrailingSlash = SandboxPrimitives.pathHasTrailingSlash(sourcePath)
132+
let newHasTrailingSlash = SandboxPrimitives.pathHasTrailingSlash(destPath)
133+
134+
let oldPath = SandboxPrimitives.stripDirSuffix(sourcePath)
135+
let newPath = SandboxPrimitives.stripDirSuffix(destPath)
136+
137+
let (sourceDir, sourceBasename) = try SandboxPrimitives.openParent(
138+
start: fd, path: oldPath
139+
)
140+
let (destDir, destBasename) = try SandboxPrimitives.openParent(
141+
start: newDir.fd, path: newPath
142+
)
143+
144+
// Re-append a slash if the original path had one
145+
let finalSourceBasename = oldHasTrailingSlash ? sourceBasename + "/" : sourceBasename
146+
let finalDestBasename = newHasTrailingSlash ? destBasename + "/" : destBasename
147+
148+
try WASIAbi.Errno.translatingPlatformErrno {
149+
try sourceDir.rename(
150+
at: FilePath(finalSourceBasename),
151+
to: destDir,
152+
at: FilePath(finalDestBasename)
153+
)
154+
}
155+
#endif
156+
}
157+
120158
func readEntries(
121159
cookie: WASIAbi.DirCookie
122160
) throws -> AnyIterator<Result<ReaddirElement, any Error>> {

Sources/WASI/Platform/SandboxPrimitives/OpenParent.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ internal func splitParent(path: String) -> (FilePath, FilePath.Component)? {
3131
}
3232

3333
extension SandboxPrimitives {
34+
/// Strip trailing slashes from a path, unless this reduces the path to "/" itself.
35+
/// This is used by rename operations to prevent paths like "foo/" from canonicalizing
36+
/// to "foo/." since these syscalls treat these differently.
37+
static func stripDirSuffix(_ path: String) -> String {
38+
var path = path
39+
while path.count > 1 && path.hasSuffix("/") {
40+
path = String(path.dropLast())
41+
}
42+
return path
43+
}
44+
45+
/// Check if a path has trailing slashes
46+
static func pathHasTrailingSlash(_ path: String) -> Bool {
47+
return path.hasSuffix("/")
48+
}
49+
3450
static func openParent(start: FileDescriptor, path: String) throws -> (FileDescriptor, String) {
3551
guard let (dirName, basename) = splitParent(path: path) else {
3652
throw WASIAbi.Errno.ENOENT

Sources/WASI/WASI.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1824,7 +1824,13 @@ public class WASIBridgeToHost: WASI {
18241824
oldFd: WASIAbi.Fd, oldPath: String,
18251825
newFd: WASIAbi.Fd, newPath: String
18261826
) throws {
1827-
throw WASIAbi.Errno.ENOTSUP
1827+
guard case let .directory(oldDirEntry) = fdTable[oldFd] else {
1828+
throw WASIAbi.Errno.ENOTDIR
1829+
}
1830+
guard case let .directory(newDirEntry) = fdTable[newFd] else {
1831+
throw WASIAbi.Errno.ENOTDIR
1832+
}
1833+
try oldDirEntry.rename(from: oldPath, toDir: newDirEntry, to: newPath)
18281834
}
18291835

18301836
func path_symlink(oldPath: String, dirFd: WASIAbi.Fd, newPath: String) throws {

Tests/WASITests/IntegrationTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,6 @@ final class IntegrationTests: XCTestCase {
110110
"WASI Rust tests": [
111111
"path_link",
112112
"dir_fd_op_failures",
113-
"path_rename_dir_trailing_slashes",
114-
"path_rename",
115113
"pwrite-with-append",
116114
"poll_oneoff_stdio",
117115
"overwrite_preopen",

0 commit comments

Comments
 (0)