package dev.exhq.ajarc.vm; import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandboxes; import dev.exhq.ajarc.Ajar; import dev.exhq.ajarc.computer.AjarDirectory; import dev.exhq.ajarc.computer.AjarFile; import dev.exhq.ajarc.computer.AjarRegularFile; import dev.exhq.ajarc.computer.AjarSymlinkFile; import dev.exhq.ajarc.computer.ComputerBlockEntity; import dev.exhq.ajarc.computer.ComputerTerminal; import javax.script.ScriptException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class JsVm { private final NashornSandbox sandbox; private final ComputerBlockEntity entity; public JsVm(ComputerBlockEntity entity) { this.sandbox = NashornSandboxes.create(); this.entity = entity; this.sandbox.inject("screen", new ScreenHelper(entity)); this.sandbox.inject("fs", new FileSystemHelper(entity)); this.sandbox.inject("js", new Js(entity)); this.sandbox.allow(ProcessHelper.class); } public record ProcessHelper(String[] args) { } private record BoundFile(AjarDirectory parent, String name) { public boolean exists() { return parent.listing().containsKey(name); } public AjarFile resolve() { return parent.listing().get(name); } public void set(AjarFile file) { parent.listing().put(name, file); } public void unlink() { parent.listing().remove(name); } } public record FileSystemHelper(ComputerBlockEntity entity) { private List<String> splitPath(String path) { var array = new ArrayList<>(Arrays.asList(path.split("[/\\\\]"))); for (int i = 0; i < array.size(); i++) { var str = array.get(i); if ("..".equals(str) && i > 1) { array.remove(i); array.remove(i - 1); i--; } else if (".".equals(str) || "".equals(str)) { array.remove(i); i--; } } return array; } private BoundFile resolveFile( String path ) { AjarDirectory node = entity.getFileSystem().root(); List<String> splitPath = splitPath(path); if (splitPath.isEmpty()) return null; for (int i = 0; i < splitPath.size(); i++) { var segment = splitPath.get(i); var child = node.listing().get(segment); if (child instanceof AjarSymlinkFile symlink) { // TODO: symlink infinite recursion protection if (symlink.target().startsWith("/")) { return resolveFile(symlink.target() + "/" + String.join("/", splitPath.subList(i + 1, splitPath.size()))); } else { return resolveFile(String.join("/", splitPath.subList(0, i)) + "/" + symlink.target() + splitPath.subList(i + 1, splitPath.size())); } } if (i == splitPath.size() - 1) break; if (child == null) return null; if (!(child instanceof AjarDirectory directory)) { throw new RuntimeException("No such file or directory " + path); } node = directory; } return new BoundFile(node, splitPath.getLast()); } public boolean unlink(String path) { var boundFile = resolveFile(path); if (boundFile == null || !boundFile.exists()) return false; boundFile.unlink(); return true; } public String readFile(String path) { // TODO: resolve relative to working directory var boundFile = resolveFile(path); if (boundFile == null || !boundFile.exists()) throw new RuntimeException("No such file: " + path); var file = boundFile.resolve(); if (file instanceof AjarRegularFile regularFile) { return regularFile.getTextContent(); } throw new RuntimeException("File is not a text file"); } public void writeFile(String path, String content) { var boundFile = resolveFile(path); if (boundFile == null) { throw new RuntimeException("Could not resolve file (parent directory does not exist): " + path); } var existingFile = boundFile.resolve(); if (existingFile != null && !(existingFile instanceof AjarRegularFile)) { throw new RuntimeException("Cannot overwrite non text file: " + path); } boundFile.set(new AjarRegularFile(content.getBytes(StandardCharsets.UTF_8))); } } public record ScreenHelper(ComputerBlockEntity entity) { public void setScreenSize(int rows, int cols) { entity.setTerminal(ComputerTerminal.ofSize(rows, cols)); } public void print(String line) { entity.addLine(line); } } public class Js { private final ComputerBlockEntity entity; public Js(ComputerBlockEntity entity) {this.entity = entity;} public void run(String path){ var fs = new FileSystemHelper(entity); entity.getVm().executeScript(fs.readFile(path), new String[0]); } } public void executeScript(String source, String[] args) { try { // TODO: provide an import facility of some sort var bindings = sandbox.createBindings(); bindings.put("process", new ProcessHelper(args)); sandbox.eval(source, bindings); } catch (ScriptException e) { Ajar.LOGGER.error("Could not execute script", e); entity.addLine("Could not execute script: " + e.getMessage()); } } }