1
1
package vfstest
2
2
3
3
import (
4
+ "errors"
4
5
"fmt"
5
6
"io/fs"
6
7
"maps"
@@ -23,11 +24,14 @@ type mapFS struct {
23
24
m fstest.MapFS
24
25
25
26
useCaseSensitiveFileNames bool
27
+
28
+ symlinks map [canonicalPath ]canonicalPath
26
29
}
27
30
28
31
var (
29
32
_ iovfs.RealpathFS = (* mapFS )(nil )
30
33
_ iovfs.WritableFS = (* mapFS )(nil )
34
+ _ fs.ReadFileFS = (* mapFS )(nil )
31
35
)
32
36
33
37
type sys struct {
@@ -45,7 +49,7 @@ func FromMap[File any](m map[string]File, useCaseSensitiveFileNames bool) vfs.FS
45
49
posix := false
46
50
windows := false
47
51
48
- for p := range m {
52
+ checkPath := func ( p string ) {
49
53
if ! tspath .IsRootedDiskPath (p ) {
50
54
panic (fmt .Sprintf ("non-rooted path %q" , p ))
51
55
}
@@ -61,12 +65,10 @@ func FromMap[File any](m map[string]File, useCaseSensitiveFileNames bool) vfs.FS
61
65
}
62
66
}
63
67
64
- if posix && windows {
65
- panic ("mixed posix and windows paths" )
66
- }
67
-
68
68
mfs := make (fstest.MapFS , len (m ))
69
69
for p , f := range m {
70
+ checkPath (p )
71
+
70
72
var file * fstest.MapFile
71
73
switch f := any (f ).(type ) {
72
74
case string :
@@ -79,10 +81,24 @@ func FromMap[File any](m map[string]File, useCaseSensitiveFileNames bool) vfs.FS
79
81
panic (fmt .Sprintf ("invalid file type %T" , f ))
80
82
}
81
83
84
+ if file .Mode & fs .ModeSymlink != 0 {
85
+ target := string (file .Data )
86
+ checkPath (target )
87
+
88
+ target , _ = strings .CutPrefix (target , "/" )
89
+ fileCopy := * file
90
+ fileCopy .Data = []byte (target )
91
+ file = & fileCopy
92
+ }
93
+
82
94
p , _ = strings .CutPrefix (p , "/" )
83
95
mfs [p ] = file
84
96
}
85
97
98
+ if posix && windows {
99
+ panic ("mixed posix and windows paths" )
100
+ }
101
+
86
102
return iovfs .From (convertMapFS (mfs , useCaseSensitiveFileNames ), useCaseSensitiveFileNames )
87
103
}
88
104
@@ -115,8 +131,10 @@ func convertMapFS(input fstest.MapFS, useCaseSensitiveFileNames bool) *mapFS {
115
131
// Create all missing intermediate directories so we can attach the realpath to each of them.
116
132
// fstest.MapFS doesn't require this as it synthesizes directories on the fly, but it's a lot
117
133
// harder to reapply a realpath onto those when we're deep in some FileInfo method.
118
- if err := m .mkdirAll (dirName (p ), 0o777 ); err != nil {
119
- panic (fmt .Sprintf ("failed to create intermediate directories for %q: %v" , p , err ))
134
+ if dir := dirName (p ); dir != "" {
135
+ if err := m .mkdirAll (dir , 0o777 ); err != nil {
136
+ panic (fmt .Sprintf ("failed to create intermediate directories for %q: %v" , p , err ))
137
+ }
120
138
}
121
139
m .setEntry (p , m .getCanonicalPath (p ), * file )
122
140
}
@@ -151,44 +169,137 @@ func (m *mapFS) open(p canonicalPath) (fs.File, error) {
151
169
return m .m .Open (string (p ))
152
170
}
153
171
154
- func (m * mapFS ) get (p canonicalPath ) (* fstest.MapFile , bool ) {
155
- file , ok := m .m [string (p )]
156
- return file , ok
172
+ func Symlink (target string ) * fstest.MapFile {
173
+ return & fstest.MapFile {
174
+ Data : []byte (target ),
175
+ Mode : fs .ModeSymlink ,
176
+ }
177
+ }
178
+
179
+ func (m * mapFS ) getFollowingSymlinks (p canonicalPath ) (* fstest.MapFile , canonicalPath , error ) {
180
+ return m .getFollowingSymlinksWorker (p , "" , "" )
181
+ }
182
+
183
+ type brokenSymlinkError struct {
184
+ from , to canonicalPath
185
+ }
186
+
187
+ func (e * brokenSymlinkError ) Error () string {
188
+ return fmt .Sprintf ("broken symlink %q -> %q" , e .from , e .to )
189
+ }
190
+
191
+ func (m * mapFS ) getFollowingSymlinksWorker (p canonicalPath , symlinkFrom , symlinkTo canonicalPath ) (* fstest.MapFile , canonicalPath , error ) {
192
+ if file , ok := m .m [string (p )]; ok && file .Mode & fs .ModeSymlink == 0 {
193
+ return file , p , nil
194
+ }
195
+
196
+ if target , ok := m .symlinks [p ]; ok {
197
+ return m .getFollowingSymlinksWorker (target , p , target )
198
+ }
199
+
200
+ // This could be a path underneath a symlinked directory.
201
+ for other , target := range m .symlinks {
202
+ if len (other ) < len (p ) && other == p [:len (other )] && p [len (other )] == '/' {
203
+ return m .getFollowingSymlinksWorker (target + p [len (other ):], other , target )
204
+ }
205
+ }
206
+
207
+ err := fs .ErrNotExist
208
+ if symlinkFrom != "" {
209
+ err = & brokenSymlinkError {symlinkFrom , symlinkTo }
210
+ }
211
+ return nil , p , err
157
212
}
158
213
159
214
func (m * mapFS ) set (p canonicalPath , file * fstest.MapFile ) {
160
215
m .m [string (p )] = file
161
216
}
162
217
163
218
func (m * mapFS ) setEntry (realpath string , canonical canonicalPath , file fstest.MapFile ) {
219
+ if realpath == "" || canonical == "" {
220
+ panic ("empty path" )
221
+ }
222
+
164
223
file .Sys = & sys {
165
224
original : file .Sys ,
166
225
realpath : realpath ,
167
226
}
168
227
m .set (canonical , & file )
228
+
229
+ if file .Mode & fs .ModeSymlink != 0 {
230
+ if m .symlinks == nil {
231
+ m .symlinks = make (map [canonicalPath ]canonicalPath )
232
+ }
233
+ m .symlinks [canonical ] = m .getCanonicalPath (string (file .Data ))
234
+ }
169
235
}
170
236
171
- func dirName ( p string ) string {
172
- dir := path . Dir ( p )
173
- if dir == "." {
174
- return ""
237
+ func splitPath ( s string , offset int ) ( before , after string ) {
238
+ idx := strings . IndexByte ( s [ offset :], '/' )
239
+ if idx < 0 {
240
+ return s , ""
175
241
}
176
- return dir
242
+ return s [:idx + offset ], s [idx + 1 + offset :]
243
+ }
244
+
245
+ func dirName (p string ) string {
246
+ dir , _ := path .Split (p )
247
+ return strings .TrimSuffix (dir , "/" )
248
+ }
249
+
250
+ func baseName (p string ) string {
251
+ _ , file := path .Split (p )
252
+ return file
177
253
}
178
254
179
255
func (m * mapFS ) mkdirAll (p string , perm fs.FileMode ) error {
180
- for ; p != "" ; p = dirName (p ) {
181
- canonical := m .getCanonicalPath (p )
182
- if other , ok := m .get (canonical ); ok {
183
- if other .Mode .IsDir () {
184
- break
185
- }
256
+ if p == "" {
257
+ panic ("empty path" )
258
+ }
259
+
260
+ // Fast path; already exists.
261
+ if other , _ , err := m .getFollowingSymlinks (m .getCanonicalPath (p )); err == nil {
262
+ if ! other .Mode .IsDir () {
186
263
return fmt .Errorf ("mkdir %q: path exists but is not a directory" , p )
187
264
}
188
- m .setEntry (p , canonical , fstest.MapFile {
265
+ return nil
266
+ }
267
+
268
+ var toCreate []string
269
+ offset := 0
270
+ for {
271
+ dir , rest := splitPath (p , offset )
272
+ canonical := m .getCanonicalPath (dir )
273
+ other , otherPath , err := m .getFollowingSymlinks (canonical )
274
+ if err != nil {
275
+ if ! errors .Is (err , fs .ErrNotExist ) {
276
+ return err
277
+ }
278
+ toCreate = append (toCreate , dir )
279
+ } else {
280
+ if ! other .Mode .IsDir () {
281
+ return fmt .Errorf ("mkdir %q: path exists but is not a directory" , otherPath )
282
+ }
283
+ if canonical != otherPath {
284
+ // We have a symlinked parent, reset and start again.
285
+ p = other .Sys .(* sys ).realpath + "/" + rest
286
+ toCreate = toCreate [:0 ]
287
+ offset = 0
288
+ continue
289
+ }
290
+ }
291
+ if rest == "" {
292
+ break
293
+ }
294
+ offset = len (dir ) + 1
295
+ }
296
+
297
+ for _ , dir := range toCreate {
298
+ m .setEntry (dir , m .getCanonicalPath (dir ), fstest.MapFile {
189
299
Mode : fs .ModeDir | perm &^umask ,
190
300
})
191
301
}
302
+
192
303
return nil
193
304
}
194
305
@@ -199,7 +310,7 @@ type fileInfo struct {
199
310
}
200
311
201
312
func (fi * fileInfo ) Name () string {
202
- return path . Base (fi .realpath )
313
+ return baseName (fi .realpath )
203
314
}
204
315
205
316
func (fi * fileInfo ) Sys () any {
@@ -284,15 +395,24 @@ func (m *mapFS) Open(name string) (fs.File, error) {
284
395
}, nil
285
396
}
286
397
398
+ func (m * mapFS ) ReadFile (name string ) ([]byte , error ) {
399
+ m .mu .RLock ()
400
+ defer m .mu .RUnlock ()
401
+
402
+ file , _ , err := m .getFollowingSymlinks (m .getCanonicalPath (name ))
403
+ if err != nil {
404
+ return nil , err
405
+ }
406
+ return file .Data , nil
407
+ }
408
+
287
409
func (m * mapFS ) Realpath (name string ) (string , error ) {
288
410
m .mu .RLock ()
289
411
defer m .mu .RUnlock ()
290
412
291
- // TODO: handle symlinks after https://go.dev/cl/385534 is available
292
- // Don't bother going through fs.Stat.
293
- file , ok := m .get (m .getCanonicalPath (name ))
294
- if ! ok {
295
- return "" , fs .ErrNotExist
413
+ file , _ , err := m .getFollowingSymlinks (m .getCanonicalPath (name ))
414
+ if err != nil {
415
+ return "" , err
296
416
}
297
417
return file .Sys .(* sys ).realpath , nil
298
418
}
@@ -324,16 +444,29 @@ func (m *mapFS) WriteFile(path string, data []byte, perm fs.FileMode) error {
324
444
325
445
if parent := dirName (path ); parent != "" {
326
446
canonical := m .getCanonicalPath (parent )
327
- parentFile , ok := m .get (canonical )
328
- if ! ok {
329
- return fmt .Errorf ("write %q: parent directory does not exist " , path )
447
+ parentFile , _ , err := m .getFollowingSymlinks (canonical )
448
+ if err != nil {
449
+ return fmt .Errorf ("write %q: %w " , path , err )
330
450
}
331
451
if ! parentFile .Mode .IsDir () {
332
452
return fmt .Errorf ("write %q: parent path exists but is not a directory" , path )
333
453
}
334
454
}
335
455
336
- m .setEntry (path , m .getCanonicalPath (path ), fstest.MapFile {
456
+ file , cp , err := m .getFollowingSymlinks (m .getCanonicalPath (path ))
457
+ if err != nil {
458
+ var brokenSymlinkError * brokenSymlinkError
459
+ if ! errors .Is (err , fs .ErrNotExist ) && ! errors .As (err , & brokenSymlinkError ) {
460
+ // No other errors are possible.
461
+ panic (err )
462
+ }
463
+ } else {
464
+ if ! file .Mode .IsRegular () {
465
+ return fmt .Errorf ("write %q: path exists but is not a regular file" , path )
466
+ }
467
+ }
468
+
469
+ m .setEntry (path , cp , fstest.MapFile {
337
470
Data : data ,
338
471
ModTime : time .Now (),
339
472
Mode : perm &^ umask ,
0 commit comments