CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-graalvm-polyglot--polyglot

GraalVM Polyglot API for embedding multiple programming languages in Java applications with secure language interoperability

Pending
Overview
Eval results
Files

io-abstractions.mddocs/

I/O Abstractions

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.

Capabilities

File System Abstraction

Custom file system implementations for controlled file access.

public interface FileSystem {
    Path parsePath(URI uri);
    Path parsePath(String path);
    void checkAccess(Path path, Set<AccessMode> modes, LinkOption... linkOptions);
    void createDirectory(Path dir, FileAttribute<?>... attrs);
    void delete(Path path);
    SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs);
    DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter);
    Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options);
    void setAttribute(Path path, String attribute, Object value, LinkOption... options);
    void copy(Path source, Path target, CopyOption... options);
    void move(Path source, Path target, CopyOption... options);
    Path toAbsolutePath(Path path);
    Path toRealPath(Path path, LinkOption... linkOptions);
    String getSeparator();
    String getPathSeparator();
}

Usage:

public class RestrictedFileSystem implements FileSystem {
    private final Path allowedRoot;
    private final FileSystem delegate;
    
    public RestrictedFileSystem(Path allowedRoot) {
        this.allowedRoot = allowedRoot.toAbsolutePath();
        this.delegate = FileSystems.getDefault();
    }
    
    @Override
    public Path parsePath(String path) {
        Path parsed = delegate.getPath(path);
        if (!isAllowed(parsed)) {
            throw new SecurityException("Access denied: " + path);
        }
        return parsed;
    }
    
    @Override
    public void checkAccess(Path path, Set<AccessMode> modes, LinkOption... linkOptions) {
        if (!isAllowed(path)) {
            throw new AccessDeniedException(path.toString());
        }
        delegate.checkAccess(path, modes, linkOptions);
    }
    
    @Override
    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
        if (!isAllowed(path)) {
            throw new AccessDeniedException(path.toString());
        }
        return delegate.newByteChannel(path, options, attrs);
    }
    
    private boolean isAllowed(Path path) {
        try {
            Path absolute = path.toAbsolutePath();
            return absolute.startsWith(allowedRoot);
        } catch (Exception e) {
            return false;
        }
    }
    
    // ... implement other methods with similar access control
}

// Usage with context
FileSystem restrictedFS = new RestrictedFileSystem(Paths.get("/safe/directory"));

IOAccess ioAccess = IOAccess.newBuilder()
    .fileSystem(restrictedFS)
    .allowHostFileAccess(false)
    .build();

Context context = Context.newBuilder("js")
    .allowIO(ioAccess)
    .build();

// JavaScript file operations are now restricted to /safe/directory
context.eval("js", "const fs = require('fs'); fs.readFileSync('/safe/directory/file.txt')"); // Works
// context.eval("js", "fs.readFileSync('/etc/passwd')"); // Throws SecurityException

Virtual File System

Create in-memory file systems for testing or sandboxing:

public class MemoryFileSystem implements FileSystem {
    private final Map<String, byte[]> files = new ConcurrentHashMap<>();
    private final Map<String, Set<String>> directories = new ConcurrentHashMap<>();
    
    public MemoryFileSystem() {
        directories.put("/", new ConcurrentSkipListSet<>());
    }
    
    @Override
    public Path parsePath(String path) {
        return Paths.get(path);
    }
    
    @Override
    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
        String pathStr = path.toString();
        byte[] content = files.getOrDefault(pathStr, new byte[0]);
        
        if (options.contains(StandardOpenOption.WRITE) || options.contains(StandardOpenOption.CREATE)) {
            return new WritableMemoryChannel(pathStr, content, this::writeFile);
        } else {
            return new ReadOnlyMemoryChannel(content);
        }
    }
    
    @Override
    public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) {
        String dirStr = dir.toString();
        Set<String> children = directories.getOrDefault(dirStr, Collections.emptySet());
        
        List<Path> paths = children.stream()
            .map(child -> Paths.get(dirStr, child))
            .filter(path -> {
                try {
                    return filter == null || filter.accept(path);
                } catch (IOException e) {
                    return false;
                }
            })
            .collect(Collectors.toList());
            
        return new MemoryDirectoryStream(paths);
    }
    
    private void writeFile(String path, byte[] content) {
        files.put(path, content);
        
        // Update parent directory
        Path parent = Paths.get(path).getParent();
        if (parent != null) {
            String parentStr = parent.toString();
            directories.computeIfAbsent(parentStr, k -> new ConcurrentSkipListSet<>())
                .add(Paths.get(path).getFileName().toString());
        }
    }
    
    // ... implement other methods
}

Process Handler

Custom process execution for controlled subprocess management.

public interface ProcessHandler {
    Process start(ProcessCommand command) throws IOException;
}

public final class ProcessCommand {
    public static ProcessCommand create(List<String> command);
    public List<String> getCommand();
    public String getDirectory();
    public Map<String, String> getEnvironment();
    public boolean isRedirectErrorStream();
}

Usage:

public class RestrictedProcessHandler implements ProcessHandler {
    private final Set<String> allowedCommands;
    private final Path allowedWorkingDir;
    
    public RestrictedProcessHandler(Set<String> allowedCommands, Path allowedWorkingDir) {
        this.allowedCommands = allowedCommands;
        this.allowedWorkingDir = allowedWorkingDir;
    }
    
    @Override
    public Process start(ProcessCommand command) throws IOException {
        List<String> cmd = command.getCommand();
        if (cmd.isEmpty()) {
            throw new IOException("Empty command");
        }
        
        String executable = cmd.get(0);
        if (!allowedCommands.contains(executable)) {
            throw new SecurityException("Command not allowed: " + executable);
        }
        
        String workingDir = command.getDirectory();
        if (workingDir != null && !Paths.get(workingDir).startsWith(allowedWorkingDir)) {
            throw new SecurityException("Working directory not allowed: " + workingDir);
        }
        
        ProcessBuilder pb = new ProcessBuilder(cmd);
        if (workingDir != null) {
            pb.directory(new File(workingDir));
        }
        
        // Filter environment variables
        Map<String, String> env = command.getEnvironment();
        if (env != null) {
            pb.environment().clear();
            env.entrySet().stream()
                .filter(entry -> isSafeEnvironmentVariable(entry.getKey()))
                .forEach(entry -> pb.environment().put(entry.getKey(), entry.getValue()));
        }
        
        return pb.start();
    }
    
    private boolean isSafeEnvironmentVariable(String name) {
        // Only allow safe environment variables
        return !name.startsWith("SECRET_") && !name.contains("PASSWORD");
    }
}

// Usage
Set<String> allowedCommands = Set.of("echo", "cat", "grep", "sed");
Path allowedDir = Paths.get("/tmp/sandbox");

ProcessHandler processHandler = new RestrictedProcessHandler(allowedCommands, allowedDir);

IOAccess ioAccess = IOAccess.newBuilder()
    .processHandler(processHandler)
    .allowHostSocketAccess(false)
    .build();

Context context = Context.newBuilder("js")
    .allowIO(ioAccess)
    .build();

// JavaScript can only execute allowed commands in allowed directories

Message Transport

Custom message transport for inter-language communication.

public interface MessageTransport {
    MessageEndpoint open(URI uri, MessageEndpoint endpoint) throws IOException, MessageTransport.VetoException;
    
    public static final class VetoException extends Exception {
        public VetoException(String message);
    }
}

public interface MessageEndpoint {
    void sendText(String text) throws IOException;
    void sendBinary(ByteBuffer data) throws IOException;
    void sendPing(ByteBuffer data) throws IOException;
    void sendPong(ByteBuffer data) throws IOException;
    void sendClose() throws IOException;
}

Usage:

public class CustomMessageTransport implements MessageTransport {
    private final Map<String, MessageHandler> handlers = new HashMap<>();
    
    public CustomMessageTransport() {
        registerHandler("echo", new EchoHandler());
        registerHandler("log", new LogHandler());
    }
    
    public void registerHandler(String protocol, MessageHandler handler) {
        handlers.put(protocol, handler);
    }
    
    @Override
    public MessageEndpoint open(URI uri, MessageEndpoint endpoint) throws IOException, VetoException {
        String scheme = uri.getScheme();
        MessageHandler handler = handlers.get(scheme);
        
        if (handler == null) {
            throw new VetoException("Unsupported protocol: " + scheme);
        }
        
        return handler.createEndpoint(uri, endpoint);
    }
}

public interface MessageHandler {
    MessageEndpoint createEndpoint(URI uri, MessageEndpoint clientEndpoint) throws IOException;
}

public class EchoHandler implements MessageHandler {
    @Override
    public MessageEndpoint createEndpoint(URI uri, MessageEndpoint clientEndpoint) {
        return new EchoEndpoint(clientEndpoint);
    }
}

public class EchoEndpoint implements MessageEndpoint {
    private final MessageEndpoint client;
    
    public EchoEndpoint(MessageEndpoint client) {
        this.client = client;
    }
    
    @Override
    public void sendText(String text) throws IOException {
        // Echo back the text
        client.sendText("Echo: " + text);
    }
    
    @Override
    public void sendBinary(ByteBuffer data) throws IOException {
        // Echo back the binary data
        client.sendBinary(data);
    }
    
    // ... implement other methods
}

// Usage
MessageTransport transport = new CustomMessageTransport();

IOAccess ioAccess = IOAccess.newBuilder()
    .messageTransport(transport)
    .build();

Context context = Context.newBuilder("js")
    .allowIO(ioAccess)
    .build();

// JavaScript can now use custom message protocols

Byte Sequence Abstraction

Work with binary data in a language-neutral way.

public interface ByteSequence {
    int length();
    byte byteAt(int index);
    ByteSequence subSequence(int start, int end);
    byte[] toByteArray();
}

Usage:

public class FileByteSequence implements ByteSequence {
    private final RandomAccessFile file;
    private final long offset;
    private final int length;
    
    public FileByteSequence(RandomAccessFile file, long offset, int length) {
        this.file = file;
        this.offset = offset;
        this.length = length;
    }
    
    @Override
    public int length() {
        return length;
    }
    
    @Override
    public byte byteAt(int index) {
        if (index < 0 || index >= length) {
            throw new IndexOutOfBoundsException();
        }
        try {
            file.seek(offset + index);
            return file.readByte();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    @Override
    public ByteSequence subSequence(int start, int end) {
        if (start < 0 || end > length || start > end) {
            throw new IndexOutOfBoundsException();
        }
        return new FileByteSequence(file, offset + start, end - start);
    }
    
    @Override
    public byte[] toByteArray() {
        byte[] result = new byte[length];
        try {
            file.seek(offset);
            file.readFully(result);
            return result;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Advanced I/O Patterns

Layered File Systems

Combine multiple file systems for complex access patterns:

public class LayeredFileSystem implements FileSystem {
    private final List<FileSystem> layers;
    
    public LayeredFileSystem(FileSystem... layers) {
        this.layers = Arrays.asList(layers);
    }
    
    @Override
    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
        // Try each layer in order
        for (FileSystem fs : layers) {
            try {
                return fs.newByteChannel(path, options, attrs);
            } catch (NoSuchFileException e) {
                continue; // Try next layer
            }
        }
        throw new NoSuchFileException(path.toString());
    }
    
    // ... implement other methods with similar layering logic
}

// Usage: Create a file system that checks memory first, then disk
FileSystem layered = new LayeredFileSystem(
    new MemoryFileSystem(),
    new RestrictedFileSystem(Paths.get("/allowed"))
);

Audit File System

Wrap file systems to log all access:

public class AuditFileSystem implements FileSystem {
    private final FileSystem delegate;
    private final Logger logger;
    
    public AuditFileSystem(FileSystem delegate, Logger logger) {
        this.delegate = delegate;
        this.logger = logger;
    }
    
    @Override
    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
        logger.info("File access: {} with options: {}", path, options);
        return delegate.newByteChannel(path, options, attrs);
    }
    
    @Override
    public void delete(Path path) {
        logger.warn("File deletion: {}", path);
        delegate.delete(path);
    }
    
    // ... wrap other methods with logging
}

Process Pool Handler

Manage process execution with resource pooling:

public class PooledProcessHandler implements ProcessHandler {
    private final ExecutorService executor;
    private final Semaphore processLimit;
    
    public PooledProcessHandler(int maxConcurrentProcesses) {
        this.executor = Executors.newCachedThreadPool();
        this.processLimit = new Semaphore(maxConcurrentProcesses);
    }
    
    @Override
    public Process start(ProcessCommand command) throws IOException {
        try {
            processLimit.acquire();
        } catch (InterruptedException e) {
            throw new IOException("Process limit acquisition interrupted", e);
        }
        
        ProcessBuilder pb = new ProcessBuilder(command.getCommand());
        Process process = pb.start();
        
        // Release permit when process completes
        executor.submit(() -> {
            try {
                process.waitFor();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                processLimit.release();
            }
        });
        
        return process;
    }
}

Performance Considerations

  • Caching: Implement caching in file systems for frequently accessed files
  • Lazy Loading: Use lazy loading for directory listings and file metadata
  • Resource Management: Properly close channels, streams, and processes
  • Thread Safety: Ensure I/O implementations are thread-safe if used across contexts
  • Memory Usage: Be mindful of memory usage in virtual file systems and byte sequences

Error Handling

I/O operations should handle errors appropriately:

public class SafeFileSystem implements FileSystem {
    private final FileSystem delegate;
    
    @Override
    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) {
        try {
            return delegate.newByteChannel(path, options, attrs);
        } catch (AccessDeniedException e) {
            throw new PolyglotException("File access denied: " + path, e);
        } catch (NoSuchFileException e) {
            throw new PolyglotException("File not found: " + path, e);
        } catch (IOException e) {
            throw new PolyglotException("I/O error accessing file: " + path, e);
        }
    }
    
    // ... similar error handling for other methods
}

Install with Tessl CLI

npx tessl i tessl/maven-org-graalvm-polyglot--polyglot

docs

context-management.md

engine-management.md

execution-monitoring.md

index.md

io-abstractions.md

proxy-system.md

security-configuration.md

source-management.md

value-operations.md

tile.json