65
65
import java .util .HashSet ;
66
66
import java .util .Iterator ;
67
67
import java .util .List ;
68
+ import java .util .Locale ;
68
69
import java .util .Map ;
69
70
import java .util .Set ;
70
71
import java .util .TreeMap ;
73
74
public final class VirtualFileSystem implements FileSystem {
74
75
75
76
/*
76
- * Virtual filesystem root
77
+ * Root of the virtual filesystem in the resources.
77
78
*/
78
- static final String VFS_PREFIX = "/{ vfs-prefix} " ;
79
+ private static final String VFS_PREFIX = "/vfs" ;
79
80
80
81
/*
81
- * Index of all files and directories available in the filessytem at runtime.
82
- * - paths are relative to the filesystem root
82
+ * Index of all files and directories available in the resources at runtime.
83
+ * - paths are absolute
83
84
* - directory paths end with a '/'
84
- * Used to determine directy entries, if an entry is a file or a directory, etc.
85
+ * - uses '/' separator regardless of platform.
86
+ * Used to determine directory entries, if an entry is a file or a directory, etc.
85
87
*/
86
88
private static final String FILES_LIST_PATH = VFS_PREFIX + "/{files-list-name}" ;
87
89
88
- private static final TreeMap <Path , Entry > VFS_ENTRIES = new TreeMap <>();
90
+ /*
91
+ * Maps platform-specific paths to entries.
92
+ */
93
+ private static final TreeMap <String , Entry > VFS_ENTRIES = new TreeMap <>();
89
94
95
+ /*
96
+ * These use '/' as the separator and start with VFS_PREFIX, no trailing slashes.
97
+ */
90
98
private static Set <String > filesList ;
91
99
private static Set <String > dirsList ;
100
+ private static Map <String , String > lowercaseToResourceMap ;
92
101
93
102
private final FileSystem delegate = FileSystem .newDefaultFileSystem ();
94
103
95
- static final record Entry (boolean isFile , Object data ) {};
104
+ private static final String PLATFORM_SEPARATOR = Paths .get ("" ).getFileSystem ().getSeparator ();
105
+ private static final char RESOURCE_SEPARATOR_CHAR = '/' ;
106
+ private static final String RESOURCE_SEPARATOR = String .valueOf (RESOURCE_SEPARATOR_CHAR );
107
+
108
+ /*
109
+ * For files, `data` is a byte[], for directories it is a Path[] which
110
+ * contains platform-specific paths.
111
+ */
112
+ private static final record Entry (boolean isFile , Object data ) {};
113
+
114
+ /*
115
+ * Determines where the virtual filesystem lives in the real filesystem,
116
+ * e.g. if set to "X:\graalpy_vfs", then a resource with path /vfx/xyz/abc
117
+ * is visible as "X:\graalpy_vfs\xyz\abc". This needs to be an absolute path
118
+ * with platform-specific separators without any trailing separator.
119
+ * If that file or directory actually exists, it will not be accessible.
120
+ */
121
+ private final String mountPoint ;
122
+ private static final boolean caseInsensitive = isWindows ();
96
123
97
- private static void putVFSEntry (Path p , Entry e ) throws IOException {
98
- VFS_ENTRIES .put (toRealPathStatic (toAbsolutePathStatic (p )), e );
124
+ public VirtualFileSystem () {
125
+ String mp = System .getenv ("GRAALPY_VFS_MOUNT_POINT" );
126
+ if (mp == null ) {
127
+ mp = isWindows () ? "X:\\ graalpy_vfs" : "/graalpy_vfs" ;
128
+ }
129
+ if (mp .endsWith (PLATFORM_SEPARATOR ) || !Path .of (mp ).isAbsolute ()) {
130
+ throw new IllegalArgumentException ("GRAALPY_VFS_MOUNT_POINT must be set to an absolute path without a trailing separator" );
131
+ }
132
+ this .mountPoint = mp ;
99
133
}
100
134
135
+ public static boolean isWindows () {
136
+ return System .getProperty ("os.name" ).toLowerCase (Locale .ROOT ).contains ("windows" );
137
+ }
138
+
139
+ public String resourcePathToPlatformPath (String path ) {
140
+ assert path .startsWith (VFS_PREFIX );
141
+ path = path .substring (VFS_PREFIX .length ());
142
+ if (!PLATFORM_SEPARATOR .equals (RESOURCE_SEPARATOR )) {
143
+ path = path .replace (RESOURCE_SEPARATOR , PLATFORM_SEPARATOR );
144
+ }
145
+ return mountPoint + path ;
146
+ }
147
+
148
+ private String platformPathToResourcePath (String path ) throws IOException {
149
+ assert path .startsWith (mountPoint );
150
+
151
+ path = path .substring (mountPoint .length ());
152
+ if (!PLATFORM_SEPARATOR .equals (RESOURCE_SEPARATOR )) {
153
+ path = path .replace (PLATFORM_SEPARATOR , RESOURCE_SEPARATOR );
154
+ }
155
+ if (path .endsWith (RESOURCE_SEPARATOR )) {
156
+ path = path .substring (0 , path .length () - RESOURCE_SEPARATOR .length ());
157
+ }
158
+ path = VFS_PREFIX + path ;
159
+ if (caseInsensitive ) {
160
+ path = getLowercaseToResourceMap ().get (path );
161
+ }
162
+ return path ;
163
+ }
164
+
101
165
private static Set <String > getFilesList () throws IOException {
102
- if (filesList == null ) {
166
+ if (filesList == null ) {
103
167
initFilesAndDirsList ();
104
168
}
105
169
return filesList ;
106
170
}
107
171
108
172
private static Set <String > getDirsList () throws IOException {
109
- if (dirsList == null ) {
173
+ if (dirsList == null ) {
110
174
initFilesAndDirsList ();
111
175
}
112
176
return dirsList ;
113
- }
177
+ }
178
+
179
+ private static Map <String , String > getLowercaseToResourceMap () throws IOException {
180
+ assert caseInsensitive ;
181
+ if (lowercaseToResourceMap == null ) {
182
+ initFilesAndDirsList ();
183
+ }
184
+ return lowercaseToResourceMap ;
185
+ }
114
186
115
187
private static void initFilesAndDirsList () throws IOException {
116
188
filesList = new HashSet <>();
117
189
dirsList = new HashSet <>();
190
+ if (caseInsensitive ) {
191
+ lowercaseToResourceMap = new HashMap <>();
192
+ }
118
193
try (InputStream stream = VirtualFileSystem .class .getResourceAsStream (FILES_LIST_PATH )) {
119
194
if (stream == null ) {
120
195
return ;
121
196
}
122
197
BufferedReader br = new BufferedReader (new InputStreamReader (stream ));
123
198
String line ;
124
199
while ((line = br .readLine ()) != null ) {
125
- if (line .endsWith ("/" )) {
200
+ if (line .endsWith (RESOURCE_SEPARATOR )) {
126
201
line = line .substring (0 , line .length () - 1 );
127
202
dirsList .add (line );
128
203
} else {
129
204
filesList .add (line );
130
- }
205
+ }
206
+ if (caseInsensitive ) {
207
+ lowercaseToResourceMap .put (line .toLowerCase (Locale .ROOT ), line );
208
+ }
131
209
}
132
210
}
133
211
}
134
212
135
- private static Entry readDirEntry (String parentDir ) throws IOException {
213
+ private Entry readDirEntry (String parentDir ) throws IOException {
136
214
List <String > l = new ArrayList <>();
137
215
138
216
// find all files in parent dir
@@ -151,14 +229,14 @@ private static Entry readDirEntry(String parentDir) throws IOException {
151
229
152
230
Path [] paths = new Path [l .size ()];
153
231
for (int i = 0 ; i < paths .length ; i ++) {
154
- paths [i ] = Paths .get (l .get (i ));
232
+ paths [i ] = Paths .get (resourcePathToPlatformPath ( l .get (i ) ));
155
233
}
156
234
return new Entry (false , paths );
157
235
}
158
236
159
237
private static boolean isParent (String parentDir , String file ) {
160
238
return file .length () > parentDir .length () && file .startsWith (parentDir ) &&
161
- file .indexOf ("/" , parentDir .length () + 1 ) < 0 ;
239
+ file .indexOf (RESOURCE_SEPARATOR_CHAR , parentDir .length () + 1 ) < 0 ;
162
240
}
163
241
164
242
private static Entry readFileEntry (String file ) throws IOException {
@@ -183,22 +261,20 @@ static byte[] readResource(String path) throws IOException {
183
261
}
184
262
185
263
private Entry file (Path path ) throws IOException {
186
- Entry e = VFS_ENTRIES .get (toRealPath (toAbsolutePath (path )));
264
+ path = toRealPath (toAbsolutePath (path ));
265
+ String pathString = path .toString ();
266
+ String entryKey = caseInsensitive ? pathString .toLowerCase (Locale .ROOT ) : pathString ;
267
+ Entry e = VFS_ENTRIES .get (entryKey );
187
268
if (e == null ) {
188
- String pathString = path .toString ();
189
- if (pathString .endsWith ("/" )) {
190
- pathString = pathString .substring (0 , pathString .length () - 1 );
191
- }
192
-
193
- URL uri = VirtualFileSystem .class .getResource (pathString );
269
+ pathString = platformPathToResourcePath (pathString );
270
+ URL uri = pathString == null ? null : VirtualFileSystem .class .getResource (pathString );
194
271
if (uri != null ) {
195
272
if (getDirsList ().contains (pathString )) {
196
273
e = readDirEntry (pathString );
197
274
} else {
198
275
e = readFileEntry (pathString );
199
- getFilesList ().remove (pathString );
200
276
}
201
- putVFSEntry ( path , e );
277
+ VFS_ENTRIES . put ( entryKey , e );
202
278
}
203
279
}
204
280
return e ;
@@ -220,7 +296,7 @@ public Path parsePath(String path) {
220
296
221
297
@ Override
222
298
public void checkAccess (Path path , Set <? extends AccessMode > modes , LinkOption ... linkOptions ) throws IOException {
223
- if (path .normalize ().startsWith (VFS_PREFIX )) {
299
+ if (path .normalize ().startsWith (mountPoint )) {
224
300
if (modes .contains (AccessMode .WRITE )) {
225
301
throw new IOException ("read-only filesystem" );
226
302
}
@@ -234,7 +310,7 @@ public void checkAccess(Path path, Set<? extends AccessMode> modes, LinkOption..
234
310
235
311
@ Override
236
312
public void createDirectory (Path dir , FileAttribute <?>... attrs ) throws IOException {
237
- if (dir .normalize ().startsWith (VFS_PREFIX )) {
313
+ if (dir .normalize ().startsWith (mountPoint )) {
238
314
throw new SecurityException ("read-only filesystem" );
239
315
} else {
240
316
delegate .createDirectory (dir , attrs );
@@ -243,7 +319,7 @@ public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOExcept
243
319
244
320
@ Override
245
321
public void delete (Path path ) throws IOException {
246
- if (path .normalize ().startsWith (VFS_PREFIX )) {
322
+ if (path .normalize ().startsWith (mountPoint )) {
247
323
throw new SecurityException ("read-only filesystem" );
248
324
} else {
249
325
delegate .delete (path );
@@ -252,7 +328,7 @@ public void delete(Path path) throws IOException {
252
328
253
329
@ Override
254
330
public SeekableByteChannel newByteChannel (Path path , Set <? extends OpenOption > options , FileAttribute <?>... attrs ) throws IOException {
255
- if (!path .normalize ().startsWith (VFS_PREFIX )) {
331
+ if (!path .normalize ().startsWith (mountPoint )) {
256
332
return delegate .newByteChannel (path , options , attrs );
257
333
}
258
334
@@ -328,7 +404,7 @@ public void close() throws IOException {
328
404
329
405
@ Override
330
406
public DirectoryStream <Path > newDirectoryStream (Path dir , DirectoryStream .Filter <? super Path > filter ) throws IOException {
331
- if (!dir .normalize ().startsWith (VFS_PREFIX )) {
407
+ if (!dir .normalize ().startsWith (mountPoint )) {
332
408
return delegate .newDirectoryStream (dir , filter );
333
409
}
334
410
Entry e = file (dir );
@@ -354,29 +430,21 @@ public Iterator<Path> iterator() {
354
430
355
431
@ Override
356
432
public Path toAbsolutePath (Path path ) {
357
- return toAbsolutePathStatic (path );
358
- }
359
-
360
- private static Path toAbsolutePathStatic (Path path ) {
361
- if (path .startsWith ("/" )) {
433
+ if (path .startsWith (mountPoint )) {
362
434
return path ;
363
435
} else {
364
- return Paths .get ("/" , path .toString ());
436
+ return Paths .get (mountPoint , path .toString ());
365
437
}
366
438
}
367
439
368
440
@ Override
369
441
public Path toRealPath (Path path , LinkOption ... linkOptions ) throws IOException {
370
- return toRealPathStatic (path , linkOptions );
371
- }
372
-
373
- private static Path toRealPathStatic (Path path , LinkOption ... linkOptions ) throws IOException {
374
442
return path .normalize ();
375
443
}
376
444
377
445
@ Override
378
446
public Map <String , Object > readAttributes (Path path , String attributes , LinkOption ... options ) throws IOException {
379
- if (!path .normalize ().startsWith (VFS_PREFIX )) {
447
+ if (!path .normalize ().startsWith (mountPoint )) {
380
448
return delegate .readAttributes (path , attributes , options );
381
449
}
382
450
Entry e = file (path );
0 commit comments