From bce099ef324148d69277509cd0bba5fd81d54ec0 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 3 Apr 2024 21:27:18 +0100 Subject: [PATCH] Allow mounting folders in the standalone emulator This theoretically allows you to use the emulator to run the test suite (via --mount-ro projects/core/src/test/resources/test-rom/:test-rom), but not sure how useful this is in practice. --- .../main/java/cc/tweaked/standalone/Main.java | 124 ++++++++++++++---- 1 file changed, 98 insertions(+), 26 deletions(-) diff --git a/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java b/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java index 49b26a6f3..157ef7e24 100644 --- a/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java +++ b/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java @@ -5,11 +5,16 @@ package cc.tweaked.standalone; +import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.core.ComputerContext; import dan200.computercraft.core.CoreConfig; +import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.AddressRule; import dan200.computercraft.core.computer.Computer; +import dan200.computercraft.core.filesystem.FileMount; +import dan200.computercraft.core.filesystem.FileSystemException; +import dan200.computercraft.core.filesystem.WritableFileMount; import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.core.terminal.TextBuffer; import dan200.computercraft.core.util.Colour; @@ -31,6 +36,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.OptionalInt; @@ -55,37 +61,58 @@ public class Main { private static final Logger LOG = LoggerFactory.getLogger(Main.class); private static final boolean DEBUG = Checks.DEBUG; - private record TermSize(int width, int height) { - public static final TermSize DEFAULT = new TermSize(51, 19); - public static final Pattern PATTERN = Pattern.compile("^(\\d+)x(\\d+)$"); - } - - private static T getParsedOptionValue(CommandLine cli, Option opt, Class klass) throws ParseException { - var res = cli.getOptionValue(opt); - if (klass == Path.class) { - try { - return klass.cast(Path.of(res)); - } catch (InvalidPathException e) { - throw new ParseException("'" + res + "' is not a valid path (" + e.getReason() + ")"); - } - } else if (klass == TermSize.class) { - var matcher = TermSize.PATTERN.matcher(res); - if (!matcher.matches()) throw new ParseException("'" + res + "' is not a valid terminal size."); - - return klass.cast(new TermSize(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)))); - } else { - return klass.cast(TypeHandler.createValue(res, klass)); + private static Path parsePath(String path) throws ParseException { + try { + return Path.of(path); + } catch (InvalidPathException e) { + throw new ParseException("'" + path + "' is not a valid path (" + e.getReason() + ")"); } } + private record TermSize(int width, int height) { + public static final TermSize DEFAULT = new TermSize(51, 19); + public static final Pattern PATTERN = Pattern.compile("^(\\d+)x(\\d+)$"); + + public static TermSize parse(String value) throws ParseException { + var matcher = TermSize.PATTERN.matcher(value); + if (!matcher.matches()) throw new ParseException("'" + value + "' is not a valid terminal size."); + + return new TermSize(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2))); + } + } + + private record MountPaths(Path src, String dest) { + public static final Pattern PATTERN = Pattern.compile("^([^:]+):([^:]+)$"); + + public static MountPaths parse(String value) throws ParseException { + var matcher = MountPaths.PATTERN.matcher(value); + if (!matcher.matches()) throw new ParseException("'" + value + "' is not a mount spec."); + + return new MountPaths(parsePath(matcher.group(1)), matcher.group(2)); + } + } + + private interface ValueParser { + T parse(String path) throws ParseException; + } + @Contract("_, _, _, !null -> !null") - private static @Nullable T getParsedOptionValue(CommandLine cli, Option opt, Class klass, @Nullable T defaultValue) throws ParseException { - return cli.hasOption(opt) ? getParsedOptionValue(cli, opt, klass) : defaultValue; + private static @Nullable T getParsedOptionValue(CommandLine cli, Option opt, ValueParser parser, @Nullable T defaultValue) throws ParseException { + return cli.hasOption(opt) ? parser.parse(cli.getOptionValue(opt)) : defaultValue; + } + + private static List getParsedOptionValues(CommandLine cli, Option opt, ValueParser parser) throws ParseException { + var values = cli.getOptionValues(opt); + if (values == null) return List.of(); + + List parsedValues = new ArrayList<>(values.length); + for (var value : values) parsedValues.add(parser.parse(value)); + return List.copyOf(parsedValues); } public static void main(String[] args) throws InterruptedException { var options = new Options(); - Option resourceOpt, computerOpt, termSizeOpt, allowLocalDomainsOpt, helpOpt; + Option resourceOpt, computerOpt, termSizeOpt, allowLocalDomainsOpt, helpOpt, mountOpt, mountRoOpt; options.addOption(resourceOpt = Option.builder("r").argName("PATH").longOpt("resources").hasArg() .desc("The path to the resources directory") .build()); @@ -98,6 +125,12 @@ public static void main(String[] args) throws InterruptedException { options.addOption(allowLocalDomainsOpt = Option.builder("L").longOpt("allow-local-domains") .desc("Allow accessing local domains with the HTTP API.") .build()); + options.addOption(mountOpt = Option.builder().longOpt("mount").hasArg().argName("SRC:DEST") + .desc("Mount a folder SRC at directory DEST on the computer.") + .build()); + options.addOption(mountRoOpt = Option.builder().longOpt("mount-ro").hasArg().argName("SRC:DEST") + .desc("Mount a read-only folder SRC at directory DEST on the computer.") + .build()); options.addOption(helpOpt = Option.builder("h").longOpt("help") .desc("Print help message") @@ -107,6 +140,7 @@ public static void main(String[] args) throws InterruptedException { Path computerDirectory; TermSize termSize; boolean allowLocalDomains; + List mounts, readOnlyMounts; try { var cli = new DefaultParser().parse(options, args); if (cli.hasOption(helpOpt)) { @@ -115,10 +149,12 @@ public static void main(String[] args) throws InterruptedException { } if (!cli.hasOption(resourceOpt)) throw new ParseException("--resources directory is required"); - resourcesDirectory = getParsedOptionValue(cli, resourceOpt, Path.class); - computerDirectory = getParsedOptionValue(cli, computerOpt, Path.class, null); - termSize = getParsedOptionValue(cli, termSizeOpt, TermSize.class, TermSize.DEFAULT); + resourcesDirectory = parsePath(cli.getOptionValue(resourceOpt)); + computerDirectory = getParsedOptionValue(cli, computerOpt, Main::parsePath, null); + termSize = getParsedOptionValue(cli, termSizeOpt, TermSize::parse, TermSize.DEFAULT); allowLocalDomains = cli.hasOption(allowLocalDomainsOpt); + mounts = getParsedOptionValues(cli, mountOpt, MountPaths::parse); + readOnlyMounts = getParsedOptionValues(cli, mountRoOpt, MountPaths::parse); } catch (ParseException e) { System.err.println(e.getLocalizedMessage()); @@ -143,6 +179,7 @@ public static void main(String[] args) throws InterruptedException { new Terminal(termSize.width(), termSize.height(), true, () -> isDirty.set(true)), 0 ); + computer.addApi(new FileMounter(computer.getAPIEnvironment(), readOnlyMounts, mounts)); computer.turnOn(); runAndInit(gl, computer, isDirty); @@ -154,6 +191,41 @@ public static void main(String[] args) throws InterruptedException { } } + /** + * An {@link ILuaAPI} which is used to mount additional files, but does not expose any new globals/methods. + */ + private static final class FileMounter implements ILuaAPI { + private final IAPIEnvironment environment; + private final List readOnlyMounts; + private final List mounts; + + FileMounter(IAPIEnvironment environment, List readOnlyMounts, List mounts) { + this.environment = environment; + this.readOnlyMounts = readOnlyMounts; + this.mounts = mounts; + } + + @Override + public String[] getNames() { + return new String[0]; + } + + @Override + public void startup() { + try { + var fs = environment.getFileSystem(); + for (var mount : readOnlyMounts) { + fs.mount(mount.dest(), mount.dest(), new FileMount(mount.src())); + } + for (var mount : mounts) { + fs.mount(mount.dest(), mount.dest(), new WritableFileMount(mount.src().toFile(), 1_000_000)); + } + } catch (FileSystemException e) { + throw new IllegalStateException(e); + } + } + } + private static final int SCALE = 2; private static final int MARGIN = 2; private static final int PIXEL_WIDTH = 6;