Skip to content

Commit fe1b938

Browse files
committed
in-memory file system for WASI with support for directories, files, and character devices
1 parent 6edd77e commit fe1b938

File tree

11 files changed

+2394
-54
lines changed

11 files changed

+2394
-54
lines changed

Sources/WASI/CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@ add_wasmkit_library(WASI
66
Platform/File.swift
77
Platform/PlatformTypes.swift
88
Platform/SandboxPrimitives.swift
9+
Platform/HostFileSystem.swift
10+
MemoryFileSystem/MemoryFileSystem.swift
11+
MemoryFileSystem/MemoryFSNodes.swift
12+
MemoryFileSystem/MemoryDirEntry.swift
13+
MemoryFileSystem/MemoryFileEntry.swift
14+
MemoryFileSystem/MemoryStdioFile.swift
915
FileSystem.swift
1016
GuestMemorySupport.swift
1117
Clock.swift
1218
RandomBufferGenerator.swift
1319
WASI.swift
20+
WASIBridgeToMemory.swift
1421
)
1522

1623
target_link_wasmkit_libraries(WASI PUBLIC
17-
WasmTypes SystemExtras)
24+
WasmTypes SystemExtras)

Sources/WASI/FileSystem.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ enum FdEntry {
8383
return directory
8484
}
8585
}
86+
87+
func asFile() -> (any WASIFile)? {
88+
if case .file(let entry) = self {
89+
return entry
90+
}
91+
return nil
92+
}
8693
}
8794

8895
/// A table that maps file descriptor to actual resource in host environment
@@ -120,3 +127,55 @@ struct FdTable {
120127
}
121128
}
122129
}
130+
131+
/// Content of a file that can be retrieved from the file system.
132+
public enum FileContent {
133+
case bytes([UInt8])
134+
case handle(FileDescriptor)
135+
}
136+
137+
/// Public protocol for file system providers that users interact with.
138+
///
139+
/// This protocol exposes only user-facing methods for managing files and directories.
140+
public protocol FileSystemProvider {
141+
/// Adds a file to the file system with the given byte content.
142+
func addFile(at path: String, content: [UInt8]) throws
143+
144+
/// Adds a file to the file system with the given string content.
145+
func addFile(at path: String, content: String) throws
146+
147+
/// Adds a file to the file system backed by a file descriptor handle.
148+
func addFile(at path: String, handle: FileDescriptor) throws
149+
150+
/// Gets the content of a file at the specified path.
151+
func getFile(at path: String) throws -> FileContent
152+
153+
/// Removes a file from the file system.
154+
func removeFile(at path: String) throws
155+
}
156+
157+
/// Internal protocol for file system implementations used by WASI.
158+
///
159+
/// This protocol contains WASI-specific implementation details that should not
160+
/// be exposed to library users.
161+
internal protocol FileSystem {
162+
/// Returns the list of pre-opened directory paths.
163+
func getPreopenPaths() -> [String]
164+
165+
/// Opens a directory and returns a WASIDir implementation.
166+
func openDirectory(at path: String) throws -> any WASIDir
167+
168+
/// Opens a file or directory from a directory file descriptor.
169+
func openAt(
170+
dirFd: any WASIDir,
171+
path: String,
172+
oflags: WASIAbi.Oflags,
173+
fsRightsBase: WASIAbi.Rights,
174+
fsRightsInheriting: WASIAbi.Rights,
175+
fdflags: WASIAbi.Fdflags,
176+
symlinkFollow: Bool
177+
) throws -> FdEntry
178+
179+
/// Creates a standard I/O file entry for stdin/stdout/stderr.
180+
func createStdioFile(fd: FileDescriptor, accessMode: FileAccessMode) -> any WASIFile
181+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import SystemPackage
2+
3+
/// A WASIDir implementation backed by an in-memory directory node.
4+
internal struct MemoryDirEntry: WASIDir {
5+
let preopenPath: String?
6+
let dirNode: MemoryDirectoryNode
7+
let path: String
8+
let fileSystem: MemoryFileSystem
9+
10+
func attributes() throws -> WASIAbi.Filestat {
11+
return WASIAbi.Filestat(
12+
dev: 0, ino: 0, filetype: .DIRECTORY,
13+
nlink: 1, size: 0,
14+
atim: 0, mtim: 0, ctim: 0
15+
)
16+
}
17+
18+
func fileType() throws -> WASIAbi.FileType {
19+
return .DIRECTORY
20+
}
21+
22+
func status() throws -> WASIAbi.Fdflags {
23+
return []
24+
}
25+
26+
func setTimes(
27+
atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp,
28+
fstFlags: WASIAbi.FstFlags
29+
) throws {
30+
// No-op for memory filesystem - timestamps not tracked
31+
}
32+
33+
func advise(
34+
offset: WASIAbi.FileSize, length: WASIAbi.FileSize, advice: WASIAbi.Advice
35+
) throws {
36+
// No-op for memory filesystem
37+
}
38+
39+
func close() throws {
40+
// No-op for memory filesystem - no resources to release
41+
}
42+
43+
func openFile(
44+
symlinkFollow: Bool,
45+
path: String,
46+
oflags: WASIAbi.Oflags,
47+
accessMode: FileAccessMode,
48+
fdflags: WASIAbi.Fdflags
49+
) throws -> FileDescriptor {
50+
// Memory filesystem doesn't return real file descriptors for this method
51+
// File opening is handled through the WASI bridge's path_open implementation
52+
throw WASIAbi.Errno.ENOTSUP
53+
}
54+
55+
func createDirectory(atPath path: String) throws {
56+
let fullPath = self.path.hasSuffix("/") ? self.path + path : self.path + "/" + path
57+
try fileSystem.ensureDirectory(at: fullPath)
58+
}
59+
60+
func removeDirectory(atPath path: String) throws {
61+
try fileSystem.removeNode(in: dirNode, at: path, mustBeDirectory: true)
62+
}
63+
64+
func removeFile(atPath path: String) throws {
65+
try fileSystem.removeNode(in: dirNode, at: path, mustBeDirectory: false)
66+
}
67+
68+
func symlink(from sourcePath: String, to destPath: String) throws {
69+
// Symlinks not supported in memory filesystem
70+
throw WASIAbi.Errno.ENOTSUP
71+
}
72+
73+
func rename(from sourcePath: String, toDir newDir: any WASIDir, to destPath: String) throws {
74+
guard let newMemoryDir = newDir as? MemoryDirEntry else {
75+
throw WASIAbi.Errno.EXDEV
76+
}
77+
78+
try fileSystem.rename(
79+
from: sourcePath, in: dirNode,
80+
to: destPath, in: newMemoryDir.dirNode
81+
)
82+
}
83+
84+
func readEntries(cookie: WASIAbi.DirCookie) throws -> AnyIterator<Result<ReaddirElement, any Error>> {
85+
let children = dirNode.listChildren()
86+
87+
let iterator = children.enumerated()
88+
.dropFirst(Int(cookie))
89+
.map { (index, name) -> Result<ReaddirElement, any Error> in
90+
return Result(catching: {
91+
let childPath = self.path.hasSuffix("/") ? self.path + name : self.path + "/" + name
92+
guard let childNode = fileSystem.lookup(at: childPath) else {
93+
throw WASIAbi.Errno.ENOENT
94+
}
95+
96+
let fileType: WASIAbi.FileType
97+
switch childNode.type {
98+
case .directory: fileType = .DIRECTORY
99+
case .file: fileType = .REGULAR_FILE
100+
case .characterDevice: fileType = .CHARACTER_DEVICE
101+
}
102+
103+
let dirent = WASIAbi.Dirent(
104+
dNext: WASIAbi.DirCookie(index + 1),
105+
dIno: 0,
106+
dirNameLen: WASIAbi.DirNameLen(name.utf8.count),
107+
dType: fileType
108+
)
109+
110+
return (dirent, name)
111+
})
112+
}
113+
.makeIterator()
114+
115+
return AnyIterator(iterator)
116+
}
117+
118+
func attributes(path: String, symlinkFollow: Bool) throws -> WASIAbi.Filestat {
119+
let fullPath = self.path.hasSuffix("/") ? self.path + path : self.path + "/" + path
120+
guard let node = fileSystem.lookup(at: fullPath) else {
121+
throw WASIAbi.Errno.ENOENT
122+
}
123+
124+
let fileType: WASIAbi.FileType
125+
var size: WASIAbi.FileSize = 0
126+
127+
switch node.type {
128+
case .directory:
129+
fileType = .DIRECTORY
130+
case .file:
131+
fileType = .REGULAR_FILE
132+
if let fileNode = node as? MemoryFileNode {
133+
size = WASIAbi.FileSize(fileNode.size)
134+
}
135+
case .characterDevice:
136+
fileType = .CHARACTER_DEVICE
137+
}
138+
139+
return WASIAbi.Filestat(
140+
dev: 0, ino: 0, filetype: fileType,
141+
nlink: 1, size: size,
142+
atim: 0, mtim: 0, ctim: 0
143+
)
144+
}
145+
146+
func setFilestatTimes(
147+
path: String,
148+
atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp,
149+
fstFlags: WASIAbi.FstFlags, symlinkFollow: Bool
150+
) throws {
151+
// No-op for memory filesystem - timestamps not tracked
152+
}
153+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import SystemPackage
2+
3+
/// Base protocol for all file system nodes in memory.
4+
internal protocol MemFSNode: AnyObject {
5+
var type: MemFSNodeType { get }
6+
}
7+
8+
/// Types of file system nodes.
9+
internal enum MemFSNodeType {
10+
case directory
11+
case file
12+
case characterDevice
13+
}
14+
15+
/// A directory node in the memory file system.
16+
internal final class MemoryDirectoryNode: MemFSNode {
17+
let type: MemFSNodeType = .directory
18+
private var children: [String: MemFSNode] = [:]
19+
20+
init() {}
21+
22+
func getChild(name: String) -> MemFSNode? {
23+
return children[name]
24+
}
25+
26+
func setChild(name: String, node: MemFSNode) {
27+
children[name] = node
28+
}
29+
30+
@discardableResult
31+
func removeChild(name: String) -> Bool {
32+
return children.removeValue(forKey: name) != nil
33+
}
34+
35+
func listChildren() -> [String] {
36+
return Array(children.keys).sorted()
37+
}
38+
39+
func childCount() -> Int {
40+
return children.count
41+
}
42+
}
43+
44+
/// A regular file node in the memory file system.
45+
internal final class MemoryFileNode: MemFSNode {
46+
let type: MemFSNodeType = .file
47+
var content: FileContent
48+
49+
init(content: FileContent) {
50+
self.content = content
51+
}
52+
53+
convenience init(bytes: [UInt8]) {
54+
self.init(content: .bytes(bytes))
55+
}
56+
57+
convenience init(handle: FileDescriptor) {
58+
self.init(content: .handle(handle))
59+
}
60+
61+
var size: Int {
62+
switch content {
63+
case .bytes(let bytes):
64+
return bytes.count
65+
case .handle(let fd):
66+
do {
67+
let attrs = try fd.attributes()
68+
return Int(attrs.size)
69+
} catch {
70+
return 0
71+
}
72+
}
73+
}
74+
}
75+
76+
/// A character device node in the memory file system.
77+
internal final class MemoryCharacterDeviceNode: MemFSNode {
78+
let type: MemFSNodeType = .characterDevice
79+
80+
enum Kind {
81+
case null
82+
}
83+
84+
let kind: Kind
85+
86+
init(kind: Kind) {
87+
self.kind = kind
88+
}
89+
}

0 commit comments

Comments
 (0)