diff --git a/src/main/java/dev/exhq/ajarc/computer/AjarDirectory.java b/src/main/java/dev/exhq/ajarc/computer/AjarDirectory.java index 3d71486..e38c1f8 100644 --- a/src/main/java/dev/exhq/ajarc/computer/AjarDirectory.java +++ b/src/main/java/dev/exhq/ajarc/computer/AjarDirectory.java @@ -14,7 +14,7 @@ public record AjarDirectory( builder -> builder.group(Codec.unboundedMap(Codec.STRING, Codec.lazyInitialized(() -> AjarFile.CODEC)) .fieldOf("listing").forGetter(AjarDirectory::listing)) - .apply(builder, AjarDirectory::new)); + .apply(builder, map -> new AjarDirectory(new HashMap<>(map)))); public static AjarDirectory ofEmpty() { return new AjarDirectory(new HashMap<>()); diff --git a/src/main/java/dev/exhq/ajarc/computer/ComputerBlockEntity.java b/src/main/java/dev/exhq/ajarc/computer/ComputerBlockEntity.java index 3a756c5..46de15b 100644 --- a/src/main/java/dev/exhq/ajarc/computer/ComputerBlockEntity.java +++ b/src/main/java/dev/exhq/ajarc/computer/ComputerBlockEntity.java @@ -6,6 +6,7 @@ import dev.exhq.ajarc.network.ComputerScreenUpdate; import dev.exhq.ajarc.register.Register; import dev.exhq.ajarc.vm.JsVm; //import dev.exhq.ajarc.vm.WasmVm; +import net.minecraft.client.Minecraft; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; @@ -15,8 +16,10 @@ import net.minecraft.world.SimpleMenuProvider; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; +import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.NotNull; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -50,6 +53,10 @@ public class ComputerBlockEntity extends BlockEntity { .getOrThrow(); } + public AjarFileSystem getFileSystem() { + return fileSystem; + } + @Override protected void saveAdditional(@NotNull CompoundTag pTag, HolderLookup.@NotNull Provider pRegistries) { super.saveAdditional(pTag, pRegistries); @@ -83,10 +90,31 @@ public class ComputerBlockEntity extends BlockEntity { } else if (line.equals("big")) { screen = ComputerTerminal.ofSize(20, 30); lines.add("Made big!"); - } else if (line.startsWith("add")) { - lines.add(jsVm.add(69, 42) + ""); + } else if (line.startsWith("resetfs")) { + var root = fileSystem.root().listing(); + var directory = (AjarDirectory) root.compute("bin", (ignored, old) -> + old instanceof AjarDirectory ? old : AjarDirectory.ofEmpty()); + Minecraft.getInstance().getResourceManager() + .listResources("bin", pred -> pred.getNamespace().equals(Ajar.MODID) && pred.getPath().endsWith(".js")) + .forEach( + (resourceLocation, resource) -> + { + try { + directory.listing().put( + resourceLocation.getPath().replace("bin/", ""), + new AjarRegularFile(IOUtils.toByteArray(resource.open())) + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + ); } else { - lines.add("Made unknown!"); + var bin = (AjarDirectory) fileSystem.root().listing().get("bin"); + var parts = line.split(" "); + var commandFile =(AjarRegularFile) bin.listing().get(parts[0] + ".js"); + var commandText = commandFile.getTextContent(); + jsVm.executeScript(commandText, Arrays.copyOfRange(parts, 1, parts.length)); } } @@ -96,4 +124,8 @@ public class ComputerBlockEntity extends BlockEntity { screen.dimensions().rows(), screen.dimensions().columns()); } + + public void addLine(String line) { + lines.add(line); + } } diff --git a/src/main/java/dev/exhq/ajarc/vm/JsVm.java b/src/main/java/dev/exhq/ajarc/vm/JsVm.java index 749a63b..226db82 100644 --- a/src/main/java/dev/exhq/ajarc/vm/JsVm.java +++ b/src/main/java/dev/exhq/ajarc/vm/JsVm.java @@ -2,52 +2,151 @@ package dev.exhq.ajarc.vm; import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandboxes; -import delight.nashornsandbox.internal.JsSanitizer; 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 net.minecraft.client.Minecraft; -import org.apache.commons.io.IOUtils; import javax.script.ScriptException; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Map; +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) { - sandbox = NashornSandboxes.create(); - sandbox.inject("screen", new ScreenHelper(entity)); + this.sandbox = NashornSandboxes.create(); + this.entity = entity; + this.sandbox.inject("screen", new ScreenHelper(entity)); + this.sandbox.inject("fs", new FileSystemHelper(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 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 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 == null) return null; + 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 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 int add(int left, int right) { - String text; + public void executeScript(String source, String[] args) { try { - var stream = Minecraft.getInstance() - .getResourceManager() - .getResourceOrThrow(Ajar.identifier("script.js")) - .open(); - text = IOUtils.toString(stream, StandardCharsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException(e); - } - var bindings = sandbox.createBindings(); - bindings.put("left", left); - bindings.put("right", right); - try { - sandbox.eval(text, bindings); + // 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) { - throw new RuntimeException(e); + Ajar.LOGGER.error("Could not execute script", e); + entity.addLine("Could not execute script: " + e.getMessage()); } - - return (int) (double) bindings.get("result"); } }