Skip to content

Commit da9d099

Browse files
committed
feat: timestamp handling in MemoryFileSystem
1 parent 39b78a9 commit da9d099

File tree

4 files changed

+371
-10
lines changed

4 files changed

+371
-10
lines changed

Sources/WASI/MemoryFileSystem/MemoryDirEntry.swift

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import SystemExtras
12
import SystemPackage
23

34
/// A WASIDir implementation backed by an in-memory directory node.
@@ -8,10 +9,13 @@ struct MemoryDirEntry: WASIDir {
89
let fileSystem: MemoryFileSystem
910

1011
func attributes() throws -> WASIAbi.Filestat {
12+
let timestamps = dirNode.timestamps
1113
return WASIAbi.Filestat(
1214
dev: 0, ino: 0, filetype: .DIRECTORY,
1315
nlink: 1, size: 0,
14-
atim: 0, mtim: 0, ctim: 0
16+
atim: timestamps.atim,
17+
mtim: timestamps.mtim,
18+
ctim: timestamps.ctim
1519
)
1620
}
1721

@@ -27,7 +31,26 @@ struct MemoryDirEntry: WASIDir {
2731
atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp,
2832
fstFlags: WASIAbi.FstFlags
2933
) throws {
30-
// No-op for memory filesystem - timestamps not tracked
34+
let now = currentTimestamp()
35+
let newAtim: WASIAbi.Timestamp?
36+
if fstFlags.contains(.ATIM) {
37+
newAtim = atim
38+
} else if fstFlags.contains(.ATIM_NOW) {
39+
newAtim = now
40+
} else {
41+
newAtim = nil
42+
}
43+
44+
let newMtim: WASIAbi.Timestamp?
45+
if fstFlags.contains(.MTIM) {
46+
newMtim = mtim
47+
} else if fstFlags.contains(.MTIM_NOW) {
48+
newMtim = now
49+
} else {
50+
newMtim = nil
51+
}
52+
53+
dirNode.setTimes(atim: newAtim, mtim: newMtim)
3154
}
3255

3356
func advise(
@@ -123,14 +146,27 @@ struct MemoryDirEntry: WASIDir {
123146

124147
let fileType: WASIAbi.FileType
125148
var size: WASIAbi.FileSize = 0
149+
var atim: WASIAbi.Timestamp = 0
150+
var mtim: WASIAbi.Timestamp = 0
151+
var ctim: WASIAbi.Timestamp = 0
126152

127153
switch node.type {
128154
case .directory:
129155
fileType = .DIRECTORY
156+
if let dirNode = node as? MemoryDirectoryNode {
157+
let timestamps = dirNode.timestamps
158+
atim = timestamps.atim
159+
mtim = timestamps.mtim
160+
ctim = timestamps.ctim
161+
}
130162
case .file:
131163
fileType = .REGULAR_FILE
132164
if let fileNode = node as? MemoryFileNode {
133165
size = WASIAbi.FileSize(fileNode.size)
166+
let timestamps = fileNode.timestamps
167+
atim = timestamps.atim
168+
mtim = timestamps.mtim
169+
ctim = timestamps.ctim
134170
}
135171
case .characterDevice:
136172
fileType = .CHARACTER_DEVICE
@@ -139,7 +175,7 @@ struct MemoryDirEntry: WASIDir {
139175
return WASIAbi.Filestat(
140176
dev: 0, ino: 0, filetype: fileType,
141177
nlink: 1, size: size,
142-
atim: 0, mtim: 0, ctim: 0
178+
atim: atim, mtim: mtim, ctim: ctim
143179
)
144180
}
145181

@@ -148,6 +184,69 @@ struct MemoryDirEntry: WASIDir {
148184
atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp,
149185
fstFlags: WASIAbi.FstFlags, symlinkFollow: Bool
150186
) throws {
151-
// No-op for memory filesystem - timestamps not tracked
187+
let fullPath = self.path.hasSuffix("/") ? self.path + path : self.path + "/" + path
188+
guard let node = fileSystem.lookup(at: fullPath) else {
189+
throw WASIAbi.Errno.ENOENT
190+
}
191+
192+
let now = currentTimestamp()
193+
let newAtim: WASIAbi.Timestamp?
194+
if fstFlags.contains(.ATIM) {
195+
newAtim = atim
196+
} else if fstFlags.contains(.ATIM_NOW) {
197+
newAtim = now
198+
} else {
199+
newAtim = nil
200+
}
201+
202+
let newMtim: WASIAbi.Timestamp?
203+
if fstFlags.contains(.MTIM) {
204+
newMtim = mtim
205+
} else if fstFlags.contains(.MTIM_NOW) {
206+
newMtim = now
207+
} else {
208+
newMtim = nil
209+
}
210+
211+
if let dirNode = node as? MemoryDirectoryNode {
212+
dirNode.setTimes(atim: newAtim, mtim: newMtim)
213+
return
214+
}
215+
216+
guard let fileNode = node as? MemoryFileNode else {
217+
return
218+
}
219+
220+
switch fileNode.content {
221+
case .bytes:
222+
fileNode.setTimes(atim: newAtim, mtim: newMtim)
223+
224+
case .handle(let handle):
225+
let accessTime: FileTime
226+
if fstFlags.contains(.ATIM) {
227+
accessTime = FileTime(
228+
seconds: Int(atim / 1_000_000_000),
229+
nanoseconds: Int(atim % 1_000_000_000)
230+
)
231+
} else if fstFlags.contains(.ATIM_NOW) {
232+
accessTime = .now
233+
} else {
234+
accessTime = .omit
235+
}
236+
237+
let modTime: FileTime
238+
if fstFlags.contains(.MTIM) {
239+
modTime = FileTime(
240+
seconds: Int(mtim / 1_000_000_000),
241+
nanoseconds: Int(mtim % 1_000_000_000)
242+
)
243+
} else if fstFlags.contains(.MTIM_NOW) {
244+
modTime = .now
245+
} else {
246+
modTime = .omit
247+
}
248+
249+
try handle.setTimes(access: accessTime, modification: modTime)
250+
}
152251
}
153252
}

Sources/WASI/MemoryFileSystem/MemoryFSNodes.swift

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
import SystemPackage
22

3+
#if canImport(Darwin)
4+
import Darwin
5+
#elseif canImport(Glibc)
6+
import Glibc
7+
#elseif canImport(Musl)
8+
import Musl
9+
#elseif os(Windows)
10+
import ucrt
11+
import WinSDK
12+
#endif
13+
14+
func currentTimestamp() -> WASIAbi.Timestamp {
15+
#if os(Windows)
16+
var ft = FILETIME()
17+
GetSystemTimeAsFileTime(&ft)
18+
let intervals = (Int64(ft.dwHighDateTime) << 32) | Int64(ft.dwLowDateTime)
19+
let unixIntervals = intervals - 116_444_736_000_000_000
20+
return WASIAbi.Timestamp(unixIntervals * 100)
21+
#else
22+
var ts = timespec()
23+
clock_gettime(CLOCK_REALTIME, &ts)
24+
return WASIAbi.Timestamp(ts.tv_sec) * 1_000_000_000 + WASIAbi.Timestamp(ts.tv_nsec)
25+
#endif
26+
}
27+
328
/// Base protocol for all file system nodes in memory.
429
protocol MemFSNode: AnyObject {
530
var type: MemFSNodeType { get }
@@ -17,22 +42,62 @@ final class MemoryDirectoryNode: MemFSNode {
1742
let type: MemFSNodeType = .directory
1843
private var children: [String: MemFSNode] = [:]
1944

20-
init() {}
45+
private var _atim: WASIAbi.Timestamp
46+
private var _mtim: WASIAbi.Timestamp
47+
private var _ctim: WASIAbi.Timestamp
48+
49+
init() {
50+
let now = currentTimestamp()
51+
self._atim = now
52+
self._mtim = now
53+
self._ctim = now
54+
}
55+
56+
var timestamps: (atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp, ctim: WASIAbi.Timestamp) {
57+
return (_atim, _mtim, _ctim)
58+
}
59+
60+
func touchAccessTime() {
61+
_atim = currentTimestamp()
62+
}
63+
64+
func touchModificationTime() {
65+
let now = currentTimestamp()
66+
_mtim = now
67+
_ctim = now
68+
}
69+
70+
func setTimes(atim: WASIAbi.Timestamp?, mtim: WASIAbi.Timestamp?) {
71+
let now = currentTimestamp()
72+
if let atim = atim {
73+
_atim = atim
74+
}
75+
if let mtim = mtim {
76+
_mtim = mtim
77+
}
78+
_ctim = now
79+
}
2180

2281
func getChild(name: String) -> MemFSNode? {
2382
return children[name]
2483
}
2584

2685
func setChild(name: String, node: MemFSNode) {
2786
children[name] = node
87+
touchModificationTime()
2888
}
2989

3090
@discardableResult
3191
func removeChild(name: String) -> Bool {
32-
return children.removeValue(forKey: name) != nil
92+
let removed = children.removeValue(forKey: name) != nil
93+
if removed {
94+
touchModificationTime()
95+
}
96+
return removed
3397
}
3498

3599
func listChildren() -> [String] {
100+
touchAccessTime()
36101
return Array(children.keys).sorted()
37102
}
38103

@@ -46,8 +111,16 @@ final class MemoryFileNode: MemFSNode {
46111
let type: MemFSNodeType = .file
47112
var content: FileContent
48113

114+
private var _atim: WASIAbi.Timestamp
115+
private var _mtim: WASIAbi.Timestamp
116+
private var _ctim: WASIAbi.Timestamp
117+
49118
init(content: FileContent) {
50119
self.content = content
120+
let now = currentTimestamp()
121+
self._atim = now
122+
self._mtim = now
123+
self._ctim = now
51124
}
52125

53126
convenience init(bytes: some Sequence<UInt8>) {
@@ -71,6 +144,56 @@ final class MemoryFileNode: MemFSNode {
71144
}
72145
}
73146
}
147+
148+
var timestamps: (atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp, ctim: WASIAbi.Timestamp) {
149+
switch content {
150+
case .bytes:
151+
return (_atim, _mtim, _ctim)
152+
case .handle(let fd):
153+
do {
154+
let attrs = try fd.attributes()
155+
let atim =
156+
WASIAbi.Timestamp(attrs.accessTime.seconds) * 1_000_000_000
157+
+ WASIAbi.Timestamp(attrs.accessTime.nanoseconds)
158+
let mtim =
159+
WASIAbi.Timestamp(attrs.modificationTime.seconds) * 1_000_000_000
160+
+ WASIAbi.Timestamp(attrs.modificationTime.nanoseconds)
161+
let ctim =
162+
WASIAbi.Timestamp(attrs.creationTime.seconds) * 1_000_000_000
163+
+ WASIAbi.Timestamp(attrs.creationTime.nanoseconds)
164+
return (atim, mtim, ctim)
165+
} catch {
166+
return (0, 0, 0)
167+
}
168+
}
169+
}
170+
171+
func touchAccessTime() {
172+
if case .bytes = content {
173+
_atim = currentTimestamp()
174+
}
175+
}
176+
177+
func touchModificationTime() {
178+
if case .bytes = content {
179+
let now = currentTimestamp()
180+
_mtim = now
181+
_ctim = now
182+
}
183+
}
184+
185+
func setTimes(atim: WASIAbi.Timestamp?, mtim: WASIAbi.Timestamp?) {
186+
if case .bytes = content {
187+
let now = currentTimestamp()
188+
if let atim = atim {
189+
_atim = atim
190+
}
191+
if let mtim = mtim {
192+
_mtim = mtim
193+
}
194+
_ctim = now
195+
}
196+
}
74197
}
75198

76199
/// A character device node in the memory file system.
@@ -183,7 +306,6 @@ final class MemoryCharacterDeviceEntry: WASIFile {
183306

184307
switch deviceNode.kind {
185308
case .null:
186-
// /dev/null discards all writes but reports them as successful
187309
var totalBytes: UInt32 = 0
188310
for iovec in buffer {
189311
iovec.withHostBufferPointer { bufferPtr in
@@ -205,7 +327,6 @@ final class MemoryCharacterDeviceEntry: WASIFile {
205327

206328
switch deviceNode.kind {
207329
case .null:
208-
// /dev/null always returns EOF (0 bytes read)
209330
return 0
210331
}
211332
}

0 commit comments

Comments
 (0)