51
51
import java .nio .channels .SeekableByteChannel ;
52
52
import java .nio .file .AccessMode ;
53
53
import java .nio .file .DirectoryStream ;
54
+ import java .nio .file .Files ;
54
55
import java .nio .file .LinkOption ;
55
56
import java .nio .file .NotDirectoryException ;
56
57
import java .nio .file .OpenOption ;
@@ -76,9 +77,9 @@ public final class VirtualFileSystem implements FileSystem {
76
77
/*
77
78
* Root of the virtual filesystem in the resources.
78
79
*/
79
- private static final String VFS_PREFIX = "/vfs" ;
80
-
81
- /*
80
+ private static final Path VFS_PREFIX = Path . of ( "/vfs" ) ;
81
+
82
+ /*
82
83
* Index of all files and directories available in the resources at runtime.
83
84
* - paths are absolute
84
85
* - directory paths end with a '/'
@@ -113,42 +114,72 @@ private static final record Entry(boolean isFile, Object data) {};
113
114
114
115
/*
115
116
* 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
+ * e.g. if set to "X:\graalpy_vfs", then a resource with path /vfs /xyz/abc
117
118
* is visible as "X:\graalpy_vfs\xyz\abc". This needs to be an absolute path
118
119
* with platform-specific separators without any trailing separator.
119
120
* If that file or directory actually exists, it will not be accessible.
120
121
*/
121
- private final String mountPoint ;
122
+ private final Path mountPoint ;
123
+ private final Path extractDir ;
124
+ private final DirectoryStream .Filter <Path > extractFilter ;
122
125
private static final boolean caseInsensitive = isWindows ();
123
-
126
+
124
127
public VirtualFileSystem () {
128
+ this (null );
129
+ }
130
+
131
+ /**
132
+ * If an extract filter is given, the virtual file system will lazily extract files and
133
+ * directories matching the filter to a temporary directory. This happens if the
134
+ * {@link #toAbsolutePath(Path) absolute path} is computed. This argument may be {@code null}
135
+ * causing that no extraction will happen.
136
+ */
137
+ public VirtualFileSystem (DirectoryStream .Filter <Path > extractFilter ) {
125
138
String mp = System .getenv ("GRAALPY_VFS_MOUNT_POINT" );
126
139
if (mp == null ) {
127
140
mp = isWindows () ? "X:\\ graalpy_vfs" : "/graalpy_vfs" ;
128
141
}
129
- if (mp .endsWith (PLATFORM_SEPARATOR ) || !Path .of (mp ).isAbsolute ()) {
142
+ this .mountPoint = Path .of (mp );
143
+ if (mp .endsWith (PLATFORM_SEPARATOR ) || !mountPoint .isAbsolute ()) {
130
144
throw new IllegalArgumentException ("GRAALPY_VFS_MOUNT_POINT must be set to an absolute path without a trailing separator" );
131
145
}
132
- this .mountPoint = mp ;
146
+ this .extractFilter = extractFilter ;
147
+ if (extractFilter != null ) {
148
+ try {
149
+ this .extractDir = Files .createTempDirectory ("vfsx" );
150
+ } catch (IOException e ) {
151
+ throw new IllegalStateException (e );
152
+ }
153
+ } else {
154
+ this .extractDir = null ;
155
+ }
133
156
}
134
157
135
158
public static boolean isWindows () {
136
159
return System .getProperty ("os.name" ).toLowerCase (Locale .ROOT ).contains ("windows" );
137
160
}
138
-
139
- public String resourcePathToPlatformPath (String path ) {
161
+
162
+ public String resourcePathToPlatformPath (String spath ) {
163
+ Path path = Path .of (spath );
140
164
assert path .startsWith (VFS_PREFIX );
141
- path = path .substring (VFS_PREFIX .length ());
165
+ Path mountPoint = this .mountPoint ;
166
+ if (path .startsWith (VFS_PREFIX )) {
167
+ path = VFS_PREFIX .relativize (path );
168
+ }
169
+ String result = mountPoint .resolve (path ).toString ();
142
170
if (!PLATFORM_SEPARATOR .equals (RESOURCE_SEPARATOR )) {
143
- path = path .replace (RESOURCE_SEPARATOR , PLATFORM_SEPARATOR );
171
+ result = result .replace (RESOURCE_SEPARATOR , PLATFORM_SEPARATOR );
144
172
}
145
- return mountPoint + path ;
173
+ return result ;
146
174
}
147
175
148
176
private String platformPathToResourcePath (String path ) throws IOException {
177
+ String mountPoint = this .mountPoint .toString ();
149
178
assert path .startsWith (mountPoint );
150
-
151
- path = path .substring (mountPoint .length ());
179
+
180
+ if (path .startsWith (mountPoint )) {
181
+ path = path .substring (mountPoint .length ());
182
+ }
152
183
if (!PLATFORM_SEPARATOR .equals (RESOURCE_SEPARATOR )) {
153
184
path = path .replace (PLATFORM_SEPARATOR , RESOURCE_SEPARATOR );
154
185
}
@@ -161,7 +192,7 @@ private String platformPathToResourcePath(String path) throws IOException {
161
192
}
162
193
return path ;
163
194
}
164
-
195
+
165
196
private static Set <String > getFilesList () throws IOException {
166
197
if (filesList == null ) {
167
198
initFilesAndDirsList ();
@@ -260,8 +291,15 @@ static byte[] readResource(String path) throws IOException {
260
291
}
261
292
}
262
293
294
+ private Path toAbsolutePathInternal (Path path ) {
295
+ if (path .startsWith (mountPoint )) {
296
+ return path ;
297
+ }
298
+ return mountPoint .resolve (path );
299
+ }
300
+
263
301
private Entry file (Path path ) throws IOException {
264
- path = toRealPath ( toAbsolutePath ( path ));
302
+ path = toAbsolutePathInternal ( path ). normalize ( );
265
303
String pathString = path .toString ();
266
304
String entryKey = caseInsensitive ? pathString .toLowerCase (Locale .ROOT ) : pathString ;
267
305
Entry e = VFS_ENTRIES .get (entryKey );
@@ -284,6 +322,64 @@ private Entry file(Path path) throws IOException {
284
322
return e ;
285
323
}
286
324
325
+ /**
326
+ * Determines if the given platform path should be extracted to a temp directory. This is
327
+ * determined by the provided filter accepts the path.
328
+ */
329
+ private boolean shouldExtract (Path path ) {
330
+ try {
331
+ return extractFilter != null && extractFilter .accept (path );
332
+ } catch (IOException e ) {
333
+ throw new IllegalStateException (e );
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Extracts a file or directory from the resource to the temporary directory and returns the
339
+ * path to the extracted file. Inexisting parent directories will also be created (recursively).
340
+ * If the extracted file or directory already exists, nothing will be done.
341
+ */
342
+ private Path getExtractedPath (Path path ) {
343
+ assert extractDir != null ;
344
+ assert shouldExtract (path );
345
+ try {
346
+ /*
347
+ * Remove the mountPoint(X) (e.g. "graalpy_vfs(x)") prefix if given. Method 'file' is
348
+ * able to handle relative paths and we need it to compute the extract path.
349
+ */
350
+ Path relPath ;
351
+ if (path .startsWith (mountPoint )) {
352
+ relPath = mountPoint .relativize (path );
353
+ } else {
354
+ relPath = path ;
355
+ }
356
+
357
+ // create target path
358
+ Path xPath = extractDir .resolve (relPath );
359
+ if (!Files .exists (xPath )) {
360
+ Entry e = file (relPath );
361
+ if (e == null ) {
362
+ return path ;
363
+ }
364
+ if (e .isFile ()) {
365
+ // first create parent dirs
366
+ Path parent = xPath .getParent ();
367
+ assert parent == null || Files .isDirectory (parent );
368
+ Files .createDirectories (parent );
369
+
370
+ // write data extracted file
371
+ Files .write (xPath , (byte []) e .data ());
372
+ } else {
373
+ Files .createDirectories (xPath );
374
+ }
375
+ }
376
+
377
+ return xPath ;
378
+ } catch (IOException e ) {
379
+ throw new IllegalStateException (e );
380
+ }
381
+ }
382
+
287
383
@ Override
288
384
public Path parsePath (URI uri ) {
289
385
if (uri .getScheme ().equals ("file" )) {
@@ -434,16 +530,24 @@ public Iterator<Path> iterator() {
434
530
435
531
@ Override
436
532
public Path toAbsolutePath (Path path ) {
437
- if (path .startsWith (mountPoint )) {
438
- return path ;
533
+ Path result ;
534
+ if (shouldExtract (path )) {
535
+ result = getExtractedPath (path );
439
536
} else {
440
- return Paths . get ( mountPoint , path . toString ()) ;
537
+ result = path ;
441
538
}
539
+ return toAbsolutePathInternal (result );
442
540
}
443
541
444
542
@ Override
445
543
public Path toRealPath (Path path , LinkOption ... linkOptions ) throws IOException {
446
- return path .normalize ();
544
+ Path result ;
545
+ if (shouldExtract (path )) {
546
+ result = getExtractedPath (path );
547
+ } else {
548
+ result = path ;
549
+ }
550
+ return result .normalize ();
447
551
}
448
552
449
553
@ Override
0 commit comments