0
# I/O Abstractions
1
2
I/O abstractions provide pluggable interfaces for controlled file system access, process execution, and message communication. These abstractions enable sandboxed environments with custom I/O behavior while maintaining security boundaries and enabling advanced use cases like virtual file systems and custom process handlers.
3
4
## Capabilities
5
6
### File System Abstraction
7
8
Custom file system implementations for controlled file access.
9
10
```java { .api }
11
public interface FileSystem {
12
Path parsePath(URI uri);
13
Path parsePath(String path);
14
void checkAccess(Path path, Set<AccessMode> modes, LinkOption... linkOptions);
15
void createDirectory(Path dir, FileAttribute<?>... attrs);
16
void delete(Path path);
17
SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs);
18
DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter);
19
Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options);
20
void setAttribute(Path path, String attribute, Object value, LinkOption... options);
21
void copy(Path source, Path target, CopyOption... options);
22
void move(Path source, Path target, CopyOption... options);
23
Path toAbsolutePath(Path path);
24
Path toRealPath(Path path, LinkOption... linkOptions);
25
String getSeparator();
26
String getPathSeparator();
27
}
28
```
29
30
**Usage:**
31
32
```java
33
public class RestrictedFileSystem implements FileSystem {
34
private final Path allowedRoot;
35
private final FileSystem delegate;
36
37
public RestrictedFileSystem(Path allowedRoot) {
38
this.allowedRoot = allowedRoot.toAbsolutePath();
39
this.delegate = FileSystems.getDefault();
40
}
41
42
@Override
43
public Path parsePath(String path) {
44
Path parsed = delegate.getPath(path);
45
if (!isAllowed(parsed)) {
46
throw new SecurityException("Access denied: " + path);
47
}
48
return parsed;
49
}
50
51
@Override
52
public void checkAccess(Path path, Set<AccessMode> modes, LinkOption... linkOptions) {
53
if (!isAllowed(path)) {
54
throw new AccessDeniedException(path.toString());
55
}
56
delegate.checkAccess(path, modes, linkOptions);
57
}
58
59
@Override
60
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
61
if (!isAllowed(path)) {
62
throw new AccessDeniedException(path.toString());
63
}
64
return delegate.newByteChannel(path, options, attrs);
65
}
66
67
private boolean isAllowed(Path path) {
68
try {
69
Path absolute = path.toAbsolutePath();
70
return absolute.startsWith(allowedRoot);
71
} catch (Exception e) {
72
return false;
73
}
74
}
75
76
// ... implement other methods with similar access control
77
}
78
79
// Usage with context
80
FileSystem restrictedFS = new RestrictedFileSystem(Paths.get("/safe/directory"));
81
82
IOAccess ioAccess = IOAccess.newBuilder()
83
.fileSystem(restrictedFS)
84
.allowHostFileAccess(false)
85
.build();
86
87
Context context = Context.newBuilder("js")
88
.allowIO(ioAccess)
89
.build();
90
91
// JavaScript file operations are now restricted to /safe/directory
92
context.eval("js", "const fs = require('fs'); fs.readFileSync('/safe/directory/file.txt')"); // Works
93
// context.eval("js", "fs.readFileSync('/etc/passwd')"); // Throws SecurityException
94
```
95
96
### Virtual File System
97
98
Create in-memory file systems for testing or sandboxing:
99
100
```java
101
public class MemoryFileSystem implements FileSystem {
102
private final Map<String, byte[]> files = new ConcurrentHashMap<>();
103
private final Map<String, Set<String>> directories = new ConcurrentHashMap<>();
104
105
public MemoryFileSystem() {
106
directories.put("/", new ConcurrentSkipListSet<>());
107
}
108
109
@Override
110
public Path parsePath(String path) {
111
return Paths.get(path);
112
}
113
114
@Override
115
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
116
String pathStr = path.toString();
117
byte[] content = files.getOrDefault(pathStr, new byte[0]);
118
119
if (options.contains(StandardOpenOption.WRITE) || options.contains(StandardOpenOption.CREATE)) {
120
return new WritableMemoryChannel(pathStr, content, this::writeFile);
121
} else {
122
return new ReadOnlyMemoryChannel(content);
123
}
124
}
125
126
@Override
127
public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) {
128
String dirStr = dir.toString();
129
Set<String> children = directories.getOrDefault(dirStr, Collections.emptySet());
130
131
List<Path> paths = children.stream()
132
.map(child -> Paths.get(dirStr, child))
133
.filter(path -> {
134
try {
135
return filter == null || filter.accept(path);
136
} catch (IOException e) {
137
return false;
138
}
139
})
140
.collect(Collectors.toList());
141
142
return new MemoryDirectoryStream(paths);
143
}
144
145
private void writeFile(String path, byte[] content) {
146
files.put(path, content);
147
148
// Update parent directory
149
Path parent = Paths.get(path).getParent();
150
if (parent != null) {
151
String parentStr = parent.toString();
152
directories.computeIfAbsent(parentStr, k -> new ConcurrentSkipListSet<>())
153
.add(Paths.get(path).getFileName().toString());
154
}
155
}
156
157
// ... implement other methods
158
}
159
```
160
161
### Process Handler
162
163
Custom process execution for controlled subprocess management.
164
165
```java { .api }
166
public interface ProcessHandler {
167
Process start(ProcessCommand command) throws IOException;
168
}
169
170
public final class ProcessCommand {
171
public static ProcessCommand create(List<String> command);
172
public List<String> getCommand();
173
public String getDirectory();
174
public Map<String, String> getEnvironment();
175
public boolean isRedirectErrorStream();
176
}
177
```
178
179
**Usage:**
180
181
```java
182
public class RestrictedProcessHandler implements ProcessHandler {
183
private final Set<String> allowedCommands;
184
private final Path allowedWorkingDir;
185
186
public RestrictedProcessHandler(Set<String> allowedCommands, Path allowedWorkingDir) {
187
this.allowedCommands = allowedCommands;
188
this.allowedWorkingDir = allowedWorkingDir;
189
}
190
191
@Override
192
public Process start(ProcessCommand command) throws IOException {
193
List<String> cmd = command.getCommand();
194
if (cmd.isEmpty()) {
195
throw new IOException("Empty command");
196
}
197
198
String executable = cmd.get(0);
199
if (!allowedCommands.contains(executable)) {
200
throw new SecurityException("Command not allowed: " + executable);
201
}
202
203
String workingDir = command.getDirectory();
204
if (workingDir != null && !Paths.get(workingDir).startsWith(allowedWorkingDir)) {
205
throw new SecurityException("Working directory not allowed: " + workingDir);
206
}
207
208
ProcessBuilder pb = new ProcessBuilder(cmd);
209
if (workingDir != null) {
210
pb.directory(new File(workingDir));
211
}
212
213
// Filter environment variables
214
Map<String, String> env = command.getEnvironment();
215
if (env != null) {
216
pb.environment().clear();
217
env.entrySet().stream()
218
.filter(entry -> isSafeEnvironmentVariable(entry.getKey()))
219
.forEach(entry -> pb.environment().put(entry.getKey(), entry.getValue()));
220
}
221
222
return pb.start();
223
}
224
225
private boolean isSafeEnvironmentVariable(String name) {
226
// Only allow safe environment variables
227
return !name.startsWith("SECRET_") && !name.contains("PASSWORD");
228
}
229
}
230
231
// Usage
232
Set<String> allowedCommands = Set.of("echo", "cat", "grep", "sed");
233
Path allowedDir = Paths.get("/tmp/sandbox");
234
235
ProcessHandler processHandler = new RestrictedProcessHandler(allowedCommands, allowedDir);
236
237
IOAccess ioAccess = IOAccess.newBuilder()
238
.processHandler(processHandler)
239
.allowHostSocketAccess(false)
240
.build();
241
242
Context context = Context.newBuilder("js")
243
.allowIO(ioAccess)
244
.build();
245
246
// JavaScript can only execute allowed commands in allowed directories
247
```
248
249
### Message Transport
250
251
Custom message transport for inter-language communication.
252
253
```java { .api }
254
public interface MessageTransport {
255
MessageEndpoint open(URI uri, MessageEndpoint endpoint) throws IOException, MessageTransport.VetoException;
256
257
public static final class VetoException extends Exception {
258
public VetoException(String message);
259
}
260
}
261
262
public interface MessageEndpoint {
263
void sendText(String text) throws IOException;
264
void sendBinary(ByteBuffer data) throws IOException;
265
void sendPing(ByteBuffer data) throws IOException;
266
void sendPong(ByteBuffer data) throws IOException;
267
void sendClose() throws IOException;
268
}
269
```
270
271
**Usage:**
272
273
```java
274
public class CustomMessageTransport implements MessageTransport {
275
private final Map<String, MessageHandler> handlers = new HashMap<>();
276
277
public CustomMessageTransport() {
278
registerHandler("echo", new EchoHandler());
279
registerHandler("log", new LogHandler());
280
}
281
282
public void registerHandler(String protocol, MessageHandler handler) {
283
handlers.put(protocol, handler);
284
}
285
286
@Override
287
public MessageEndpoint open(URI uri, MessageEndpoint endpoint) throws IOException, VetoException {
288
String scheme = uri.getScheme();
289
MessageHandler handler = handlers.get(scheme);
290
291
if (handler == null) {
292
throw new VetoException("Unsupported protocol: " + scheme);
293
}
294
295
return handler.createEndpoint(uri, endpoint);
296
}
297
}
298
299
public interface MessageHandler {
300
MessageEndpoint createEndpoint(URI uri, MessageEndpoint clientEndpoint) throws IOException;
301
}
302
303
public class EchoHandler implements MessageHandler {
304
@Override
305
public MessageEndpoint createEndpoint(URI uri, MessageEndpoint clientEndpoint) {
306
return new EchoEndpoint(clientEndpoint);
307
}
308
}
309
310
public class EchoEndpoint implements MessageEndpoint {
311
private final MessageEndpoint client;
312
313
public EchoEndpoint(MessageEndpoint client) {
314
this.client = client;
315
}
316
317
@Override
318
public void sendText(String text) throws IOException {
319
// Echo back the text
320
client.sendText("Echo: " + text);
321
}
322
323
@Override
324
public void sendBinary(ByteBuffer data) throws IOException {
325
// Echo back the binary data
326
client.sendBinary(data);
327
}
328
329
// ... implement other methods
330
}
331
332
// Usage
333
MessageTransport transport = new CustomMessageTransport();
334
335
IOAccess ioAccess = IOAccess.newBuilder()
336
.messageTransport(transport)
337
.build();
338
339
Context context = Context.newBuilder("js")
340
.allowIO(ioAccess)
341
.build();
342
343
// JavaScript can now use custom message protocols
344
```
345
346
### Byte Sequence Abstraction
347
348
Work with binary data in a language-neutral way.
349
350
```java { .api }
351
public interface ByteSequence {
352
int length();
353
byte byteAt(int index);
354
ByteSequence subSequence(int start, int end);
355
byte[] toByteArray();
356
}
357
```
358
359
**Usage:**
360
361
```java
362
public class FileByteSequence implements ByteSequence {
363
private final RandomAccessFile file;
364
private final long offset;
365
private final int length;
366
367
public FileByteSequence(RandomAccessFile file, long offset, int length) {
368
this.file = file;
369
this.offset = offset;
370
this.length = length;
371
}
372
373
@Override
374
public int length() {
375
return length;
376
}
377
378
@Override
379
public byte byteAt(int index) {
380
if (index < 0 || index >= length) {
381
throw new IndexOutOfBoundsException();
382
}
383
try {
384
file.seek(offset + index);
385
return file.readByte();
386
} catch (IOException e) {
387
throw new RuntimeException(e);
388
}
389
}
390
391
@Override
392
public ByteSequence subSequence(int start, int end) {
393
if (start < 0 || end > length || start > end) {
394
throw new IndexOutOfBoundsException();
395
}
396
return new FileByteSequence(file, offset + start, end - start);
397
}
398
399
@Override
400
public byte[] toByteArray() {
401
byte[] result = new byte[length];
402
try {
403
file.seek(offset);
404
file.readFully(result);
405
return result;
406
} catch (IOException e) {
407
throw new RuntimeException(e);
408
}
409
}
410
}
411
```
412
413
## Advanced I/O Patterns
414
415
### Layered File Systems
416
417
Combine multiple file systems for complex access patterns:
418
419
```java
420
public class LayeredFileSystem implements FileSystem {
421
private final List<FileSystem> layers;
422
423
public LayeredFileSystem(FileSystem... layers) {
424
this.layers = Arrays.asList(layers);
425
}
426
427
@Override
428
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
429
// Try each layer in order
430
for (FileSystem fs : layers) {
431
try {
432
return fs.newByteChannel(path, options, attrs);
433
} catch (NoSuchFileException e) {
434
continue; // Try next layer
435
}
436
}
437
throw new NoSuchFileException(path.toString());
438
}
439
440
// ... implement other methods with similar layering logic
441
}
442
443
// Usage: Create a file system that checks memory first, then disk
444
FileSystem layered = new LayeredFileSystem(
445
new MemoryFileSystem(),
446
new RestrictedFileSystem(Paths.get("/allowed"))
447
);
448
```
449
450
### Audit File System
451
452
Wrap file systems to log all access:
453
454
```java
455
public class AuditFileSystem implements FileSystem {
456
private final FileSystem delegate;
457
private final Logger logger;
458
459
public AuditFileSystem(FileSystem delegate, Logger logger) {
460
this.delegate = delegate;
461
this.logger = logger;
462
}
463
464
@Override
465
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
466
logger.info("File access: {} with options: {}", path, options);
467
return delegate.newByteChannel(path, options, attrs);
468
}
469
470
@Override
471
public void delete(Path path) {
472
logger.warn("File deletion: {}", path);
473
delegate.delete(path);
474
}
475
476
// ... wrap other methods with logging
477
}
478
```
479
480
### Process Pool Handler
481
482
Manage process execution with resource pooling:
483
484
```java
485
public class PooledProcessHandler implements ProcessHandler {
486
private final ExecutorService executor;
487
private final Semaphore processLimit;
488
489
public PooledProcessHandler(int maxConcurrentProcesses) {
490
this.executor = Executors.newCachedThreadPool();
491
this.processLimit = new Semaphore(maxConcurrentProcesses);
492
}
493
494
@Override
495
public Process start(ProcessCommand command) throws IOException {
496
try {
497
processLimit.acquire();
498
} catch (InterruptedException e) {
499
throw new IOException("Process limit acquisition interrupted", e);
500
}
501
502
ProcessBuilder pb = new ProcessBuilder(command.getCommand());
503
Process process = pb.start();
504
505
// Release permit when process completes
506
executor.submit(() -> {
507
try {
508
process.waitFor();
509
} catch (InterruptedException e) {
510
Thread.currentThread().interrupt();
511
} finally {
512
processLimit.release();
513
}
514
});
515
516
return process;
517
}
518
}
519
```
520
521
## Performance Considerations
522
523
- **Caching**: Implement caching in file systems for frequently accessed files
524
- **Lazy Loading**: Use lazy loading for directory listings and file metadata
525
- **Resource Management**: Properly close channels, streams, and processes
526
- **Thread Safety**: Ensure I/O implementations are thread-safe if used across contexts
527
- **Memory Usage**: Be mindful of memory usage in virtual file systems and byte sequences
528
529
## Error Handling
530
531
I/O operations should handle errors appropriately:
532
533
```java
534
public class SafeFileSystem implements FileSystem {
535
private final FileSystem delegate;
536
537
@Override
538
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
539
try {
540
return delegate.newByteChannel(path, options, attrs);
541
} catch (AccessDeniedException e) {
542
throw new PolyglotException("File access denied: " + path, e);
543
} catch (NoSuchFileException e) {
544
throw new PolyglotException("File not found: " + path, e);
545
} catch (IOException e) {
546
throw new PolyglotException("I/O error accessing file: " + path, e);
547
}
548
}
549
550
// ... similar error handling for other methods
551
}
552
```