diff --git a/.gitignore b/.gitignore
index 7e7d56a71..3f0dbfd57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -133,3 +133,20 @@ run/
# Ignore datagen cache.
/common/src/main/generated/resources/.cache/
+
+/fabric/run
+/fabric/runs
+/forge/run
+/forge/runs
+/neoforge/run
+/neoforge/runs
+/common/run
+/common/runs
+/quilt/run
+/quilt/runs
+/bootimage/logs
+/logs
+*.log.gz
+*.log
+*.ext2
+/bootimage/src/fs/.idea/
diff --git a/.run/Run Boot Imager.run.xml b/.run/Run Boot Imager.run.xml
new file mode 100644
index 000000000..a4de47f6f
--- /dev/null
+++ b/.run/Run Boot Imager.run.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bootimage/build.gradle b/bootimage/build.gradle
new file mode 100644
index 000000000..085010112
--- /dev/null
+++ b/bootimage/build.gradle
@@ -0,0 +1,42 @@
+plugins {
+ id 'java'
+}
+
+group = 'dev.ultreon.mods'
+version = '0.9.0'
+
+repositories {
+ mavenCentral()
+}
+
+sourceSets {
+ fs {
+ resources {
+ srcDirs = ['src/fs']
+ }
+ java {
+ srcDirs.clear()
+ srcDirs = ['src/main/java']
+ }
+ }
+}
+
+processFsResources {
+ exclude ".idea"
+ exclude ".gradle"
+ exclude "build"
+ exclude "out"
+ exclude "**/__pycache__"
+ exclude "**/*.pyc"
+}
+
+dependencies {
+ testImplementation platform('org.junit:junit-bom:5.10.0')
+ testImplementation 'org.junit.jupiter:junit-jupiter'
+
+ implementation 'com.github.Nuix:jnode-fs:v1.0.1'
+}
+
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/bootimage/src/fs/bin/echo.py b/bootimage/src/fs/bin/echo.py
new file mode 100644
index 000000000..7a802f103
--- /dev/null
+++ b/bootimage/src/fs/bin/echo.py
@@ -0,0 +1,7 @@
+from libstd import main
+
+
+@main
+def prog_main(args: list[str]) -> int:
+ print(" ".join(args))
+ return 0
diff --git a/bootimage/src/fs/bin/shell.py b/bootimage/src/fs/bin/shell.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/bootimage/src/fs/boot/main.py b/bootimage/src/fs/boot/main.py
new file mode 100644
index 000000000..2048be89b
--- /dev/null
+++ b/bootimage/src/fs/boot/main.py
@@ -0,0 +1,80 @@
+import os
+
+def create_dir(path, mode):
+ if not os.path.exists(path):
+ os.makedirs(path, exist_ok=True)
+
+
+# noinspection PyProtectedMember,PyUnresolvedReferences
+def boot():
+ import sys
+ create_dir("/bin", 0o755)
+ create_dir("/sys", 0o755)
+ create_dir("/sys/dev", 0o755)
+ create_dir("/sys/proc", 0o755)
+ create_dir("/sys/sys", 0o755)
+
+ create_dir("/dev/pts", 0o755)
+ create_dir("/dev/serial", 0o755)
+ create_dir("/dev/usb", 0o755)
+ create_dir("/dev/virtual", 0o755)
+
+ create_dir("/usr/bin", 0o755)
+ create_dir("/usr/lib", 0o755)
+ create_dir("/usr/sbin", 0o755)
+ create_dir("/usr/share", 0o755)
+ create_dir("/usr/local", 0o755)
+ create_dir("/usr/local/bin", 0o755)
+ create_dir("/usr/local/lib", 0o755)
+
+ create_dir("/var", 0o755)
+ create_dir("/var/log", 0o755)
+ create_dir("/var/lib", 0o755)
+ create_dir("/var/lock", 0o755)
+ create_dir("/var/run", 0o755)
+ create_dir("/var/tmp", 0o755)
+
+ create_dir("/etc", 0o755)
+ create_dir("/media", 0o755)
+ create_dir("/tmp", 0o755)
+ create_dir("/opt", 0o755)
+ create_dir("/home", 0o755)
+ create_dir("/home/setup", 0o755)
+ create_dir("/root", 0o755)
+ create_dir("/run", 0o755)
+ create_dir("/sbin", 0o755)
+
+ sys.path.append("/usr/bin")
+ sys.path.append("/usr/lib")
+ sys.path.append("/bin")
+ sys.path.append("/lib")
+ sys.path.append("/usr/local/bin")
+ sys.path.append("/usr/local/lib")
+ sys.path.append("/sbin")
+ sys.path.append("/usr/sbin")
+ sys.path.append("/usr/local/sbin")
+ sys.path.append("/var/lib")
+
+ if "/lib" not in sys.path:
+ raise Exception("Failed to add /lib to sys.path")
+
+ import os
+ if not os.path.exists("/lib/libsystem.py"):
+ raise Exception("Failed to find /lib/libsystem.py")
+
+ import importlib
+
+ importlib.invalidate_caches()
+ libsystem = importlib.import_module("libsystem")
+
+ # noinspection PyUnresolvedReferences
+ try:
+ libsystem._bootinit(bios)
+ except Exception as e:
+ print(e)
+
+ while True:
+ pass
+
+
+boot()
diff --git a/bootimage/src/fs/home/user/.shellrc b/bootimage/src/fs/home/user/.shellrc
new file mode 100644
index 000000000..e69de29bb
diff --git a/bootimage/src/fs/lib/libstd.py b/bootimage/src/fs/lib/libstd.py
new file mode 100644
index 000000000..5c7e0e1c4
--- /dev/null
+++ b/bootimage/src/fs/lib/libstd.py
@@ -0,0 +1 @@
+def main(self):
diff --git a/bootimage/src/fs/lib/libsystem.py b/bootimage/src/fs/lib/libsystem.py
new file mode 100644
index 000000000..684b9552e
--- /dev/null
+++ b/bootimage/src/fs/lib/libsystem.py
@@ -0,0 +1,343 @@
+import threading
+from typing import Callable
+
+
+def readonly(self):
+ __setattr__ = self.__setattr__
+
+ def is_called_from(self):
+ import inspect
+
+ if inspect.ismodule(self):
+ stack = inspect.stack()
+ return len(stack) > 1 and inspect.stack()[1][0].f_globals.get('__qualname__', None) == self.__qualname__
+ elif inspect.ismethod(self):
+ stack = inspect.stack()
+ return len(stack) > 1 and inspect.stack()[1][0].f_locals.get('self', None) is self
+ elif inspect.isfunction(self):
+ stack = inspect.stack()
+ return len(stack) > 1 and inspect.stack()[1][0].f_locals.get('__qualname__', None) == self.__qualname__
+ elif inspect.isclass(self):
+ stack = inspect.stack()
+ return len(stack) > 1 and inspect.stack()[1][0].f_locals.get('__qualname__', None) == self.__qualname__
+ else:
+ return False
+
+ def __internal_setattr__(self, key, value):
+ if key.startswith('_') and is_called_from(self):
+ raise AttributeError(f"Cannot set attribute {key}, module '{type(self).__name__}' is read-only")
+
+ __setattr__(self, key, value)
+
+ self.__setattr__ = __internal_setattr__
+
+ return self
+
+
+# noinspection PyPep8Naming
+@readonly
+class InventoryApi:
+ def __init__(self):
+ # Native API
+ raise NotImplemented()
+
+ def get(self, index: int) -> tuple[str, int]:
+ # Native API
+ raise NotImplemented()
+
+ def count(self) -> int:
+ # Native API
+ raise NotImplemented()
+
+ def find(self, name: str) -> int:
+ # Native API
+ raise NotImplemented()
+
+ def findAll(self, name: str) -> list[int]:
+ # Native API
+ raise NotImplemented()
+
+ def getName(self, index: int) -> str:
+ # Native API
+ raise NotImplemented()
+
+
+# noinspection PyPep8Naming
+@readonly
+class ButtonAPI:
+ def __init__(self):
+ # Native API
+ raise NotImplemented()
+
+ def setText(self, text: str):
+ # Native API
+ raise NotImplemented()
+
+ def setEnabled(self, enabled: bool):
+ # Native API
+ raise NotImplemented()
+
+ def setPressed(self, pressed: bool):
+ # Native API
+ raise NotImplemented()
+
+ def isPressed(self) -> bool:
+ # Native API
+ raise NotImplemented()
+
+ def isEnabled(self) -> bool:
+ # Native API
+ raise NotImplemented()
+
+ def getText(self) -> str:
+ # Native API
+ raise NotImplemented()
+
+
+# noinspection PyPep8Naming
+@readonly
+class LabelAPI:
+ def setText(self, text: str):
+ # Native API
+ raise NotImplemented()
+
+ def getText(self) -> str:
+ # Native API
+ raise NotImplemented()
+
+ def setEnabled(self, enabled: bool):
+ # Native API
+ raise NotImplemented()
+
+ def isEnabled(self) -> bool:
+ # Native API
+ raise NotImplemented()
+
+
+# noinspection PyPep8Naming
+@readonly
+class Image:
+ def __init__(self):
+ # Native API
+ raise NotImplemented()
+
+ def getWidth(self) -> int:
+ # Native API
+ raise NotImplemented()
+
+ def getHeight(self) -> int:
+ # Native API
+ raise NotImplemented()
+
+ def getData(self) -> bytes:
+ # Native API
+ raise NotImplemented()
+
+ def destroy(self):
+ # Native API
+ raise NotImplemented()
+
+
+# noinspection PyPep8Naming
+@readonly
+class UiApi:
+ def __init__(self):
+ # Native API
+ raise NotImplemented()
+
+ def createButton(self, x: int, y: int, width: int, height: int, text: str, normal: Image, pressed: Image, disabled: Image) -> ButtonAPI:
+ # Native API
+ raise NotImplemented()
+
+ def createLabel(self, x: int, y: int, width: int, height: int, text: str) -> LabelAPI:
+ # Native API
+ raise NotImplemented()
+
+ def createImage(self, x: int, y: int, width: int, height: int, data: bytes) -> Image:
+ # Native API
+ raise NotImplemented()
+
+ def getImageLimit(self) -> int:
+ # Native API
+ raise NotImplemented()
+
+ def getImageCount(self) -> int:
+ # Native API
+ raise NotImplemented()
+
+
+# noinspection PyPep8Naming
+@readonly
+class ProcessAPI:
+ def __init__(self):
+ # Native API
+ raise NotImplemented()
+
+ def setOnExit(self, callback: Callable[[int], None]):
+ # Native API
+ raise NotImplemented()
+
+ def run(self):
+ # Native API
+ raise NotImplemented()
+
+
+# noinspection PyPep8Naming
+@readonly
+class __BiosAPI:
+ def __init__(self):
+ # Native API
+ raise NotImplemented()
+
+ def getInventoryApi(self) -> InventoryApi:
+ # Native API
+ raise NotImplemented()
+
+ def getUiApi(self) -> UiApi:
+ # Native API
+ raise NotImplemented()
+
+ def spawnProcess(self, modules, command: list[str], env: dict[str, str]) -> ProcessAPI:
+ # Native API
+ raise NotImplemented()
+
+
+@readonly
+def get_inventory() -> InventoryApi:
+ # Native API
+ raise NotImplemented()
+
+
+@readonly
+def get_ui() -> UiApi:
+ # Native API
+ raise NotImplemented()
+
+
+@readonly
+def request_shutdown():
+ globals()['__shutdown_requested'] = True
+
+
+class Process:
+ def __init__(self):
+ # Native API
+ raise NotImplemented()
+
+ def wait(self):
+ # Native API
+ raise NotImplemented()
+
+ def kill(self):
+ # Native API
+ raise NotImplemented()
+
+ def update(self):
+ # Native API
+ raise NotImplemented()
+
+ def __setattr__(self, key, value):
+ if not hasattr(self, key):
+ raise AttributeError(key)
+
+
+def spawn_process(path: str):
+ import os
+ if not os.path.exists(path):
+ raise Exception(f"File {path} does not exist")
+
+ return Process(path)
+
+
+class PowerState:
+ __STATE = None
+
+ @classmethod
+ def shutdown(cls):
+ cls.__STATE = 0
+
+ @classmethod
+ def reboot(cls):
+ cls.__STATE = 1
+
+
+def allows(privilege: str):
+ return True
+
+
+def run_privileged(privilege: str, func):
+ pass
+
+
+# noinspection PyUnresolvedReferences
+def _bootinit(bios: __BiosAPI):
+ """
+ This method is inaccessible after boot setup
+ :param bios: the bios api
+ :return: None
+ """
+ import sys, os, os.path
+
+ print("Booting...")
+
+ globals()['get_inventory'] = bios.getInventoryApi
+ globals()['get_ui'] = bios.getUiApi
+ globals()['bootinit'] = None
+ globals()['__shutdown_requested'] = False
+
+ print("Locking modules")
+
+ current_process=None
+
+ privileges = threading.local()
+ privileges._privilege = None
+
+ class Privilege:
+ def __init__(self):
+ self._privilege = ""
+
+ def process_end(proc: Process | None, code: int):
+ if proc is not None:
+ proc.wait()
+ sys.exit(code)
+
+ sys.exit = lambda code: process_end(current_process, code)
+
+ def do_not_allow_setattr(self, key, value):
+ raise AttributeError(f"Cannot set attribute {key}, module '{type(self).__name__}' is read-only")
+
+ sys.modules['libsystem'].__setattr__ = do_not_allow_setattr
+ sys.modules['sys'].__setattr__ = do_not_allow_setattr
+ sys.modules['os'].__setattr__ = do_not_allow_setattr
+ sys.modules['os.path'].__setattr__ = do_not_allow_setattr
+
+ item = sys.modules.__setitem__
+
+ def do_not_allow_setitem(key, value):
+ if key == 'libsystem' or key == 'sys' or key == 'os' or key == 'os.path':
+ raise AttributeError(f"Cannot set attribute module is read-only")
+ item(key, value)
+
+ def do_not_allow_setattr_modules(self, key, value):
+ if key == "modules" or key == "__setitem__" or key == "__setattr__":
+ raise AttributeError(f"Cannot set attribute {key}, module '{type(self).__name__}' is read-only")
+
+ sys.modules.__setitem__ = do_not_allow_setitem
+ sys.modules.__setattr__ = do_not_allow_setattr
+ sys.__setattr__ = do_not_allow_setattr_modules
+
+ print("Launching processes...")
+
+ processes: list[Process] = []
+
+ while True:
+ if globals()['__shutdown_requested']:
+ print("Shutting down...")
+ for p in processes:
+ p.kill()
+ break
+
+ for p in processes:
+ p.update()
+
+ print("Shutting down...")
diff --git a/bootimage/src/fs/lib/libui/widgets.py b/bootimage/src/fs/lib/libui/widgets.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/bootimage/src/fs/root/.shellrc b/bootimage/src/fs/root/.shellrc
new file mode 100644
index 000000000..e69de29bb
diff --git a/bootimage/src/main/java/dev/ultreon/bootimager/BootImager.java b/bootimage/src/main/java/dev/ultreon/bootimager/BootImager.java
new file mode 100644
index 000000000..990d2a281
--- /dev/null
+++ b/bootimage/src/main/java/dev/ultreon/bootimager/BootImager.java
@@ -0,0 +1,42 @@
+package dev.ultreon.bootimager;
+
+import org.jnode.fs.FileSystemException;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class BootImager {
+ public static void main(String[] args) throws FileSystemException, IOException {
+ if (!Files.exists(Path.of("build"))) {
+ Files.createDirectory(Path.of("build"));
+ }
+
+ if (Files.exists(Path.of("common/src/main/resources/data/devices/filesystems/main.ext2"))) {
+ Files.delete(Path.of("common/src/main/resources/data/devices/filesystems/main.ext2"));
+ }
+
+ try (Ext2FS fs = Ext2FS.format(Path.of("common/src/main/resources/data/devices/filesystems/main.ext2"), 16L * 1024L * 1024L)) {
+ try (var walk = Files.walk(Path.of("bootimage/src/fs"))) {
+ walk.forEach(path -> {
+ try {
+ String replace = path.toString().replace("bootimage/src/fs/", "");
+ if (replace.startsWith(".")) {
+ return;
+ }
+ Path rel = Path.of("/" + replace);
+ if (Files.isDirectory(path)) {
+ fs.createDirectory(rel);
+ return;
+ }
+ byte[] data = Files.readAllBytes(path);
+ fs.createFile(rel, data);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ }
+ fs.flush();
+ }
+ }
+}
\ No newline at end of file
diff --git a/bootimage/src/main/java/dev/ultreon/bootimager/Ext2FS.java b/bootimage/src/main/java/dev/ultreon/bootimager/Ext2FS.java
new file mode 100644
index 000000000..4c2a555c8
--- /dev/null
+++ b/bootimage/src/main/java/dev/ultreon/bootimager/Ext2FS.java
@@ -0,0 +1,741 @@
+package dev.ultreon.bootimager;
+
+import com.google.common.base.Preconditions;
+import net.minecraft.util.Unit;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jnode.driver.block.BlockDeviceAPI;
+import org.jnode.driver.virtual.VirtualDevice;
+import org.jnode.fs.*;
+import org.jnode.fs.FileSystem;
+import org.jnode.fs.FileSystemException;
+import org.jnode.fs.ext2.*;
+import org.jnode.fs.spi.AbstractFileSystem;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.ref.Cleaner;
+import java.nio.ByteBuffer;
+import java.nio.file.*;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class Ext2FS implements FS {
+ private final FileSystem> fs;
+ private final ConcurrentMap locks = new ConcurrentHashMap<>();
+
+ private Ext2FS(Ext2FileSystem fs) {
+ this.fs = fs;
+
+ Cleaner cleaner = Cleaner.create();
+ cleaner.register(this, () -> {
+ try {
+ fs.close();
+ } catch (IOException e) {
+ LoggerFactory.getLogger(Ext2FS.class).error("Failed to close filesystem", e);
+ }
+ });
+ }
+
+ public static Ext2FS open(Path filePath) throws IOException, FileSystemException {
+ return open(false, filePath);
+ }
+
+ public static Ext2FS open(boolean readOnly, Path filePath) throws IOException, FileSystemException {
+ var device = new VirtualDevice("MineDisk");
+ var blockDevice = new VirtualBlockDevice(filePath.toFile().getAbsolutePath(), Files.size(filePath));
+ device.registerAPI(BlockDeviceAPI.class, blockDevice);
+
+ var type = new Ext2FileSystemType();
+ var fs = new Ext2FileSystem(device, readOnly, type);
+ fs.read();
+ return new Ext2FS(fs);
+ }
+
+ public static Ext2FS openForced(Path filePath) throws IOException, FileSystemException {
+ var device = new VirtualDevice("MineDisk");
+ var blockDevice = new VirtualBlockDevice(filePath.toFile().getAbsolutePath(), Files.size(filePath));
+ device.registerAPI(BlockDeviceAPI.class, blockDevice);
+
+ var type = new Ext2FileSystemType();
+ var fs = new Ext2FileSystem(device, false, type);
+ fs.getSuperblock().setState(Ext2Constants.EXT2_VALID_FS);
+ fs.flush();
+ fs.read();
+ return new Ext2FS(fs);
+ }
+
+ public static Ext2FS format(Path filePath, long diskSize) throws IOException, FileSystemException {
+ if (diskSize <= 16384) throw new IllegalArgumentException("Disk size must be greater than 16 KiB");
+
+ if (!Files.exists(filePath)) {
+ Files.createFile(filePath);
+ }
+
+ var device = new VirtualDevice("MineDisk");
+ device.registerAPI(BlockDeviceAPI.class, new VirtualBlockDevice(filePath.toFile().getAbsolutePath(), diskSize));
+
+ var blockDevice = new VirtualBlockDevice(filePath.toFile().getAbsolutePath(), diskSize);
+ var formatter = new Ext2FileSystemFormatter(BlockSize._1Kb);
+ var fs = formatter.format(device);
+ return new Ext2FS(fs);
+ }
+
+ @Override
+ public void close() throws IOException {
+ fs.close();
+ }
+
+ @Override
+ public InputStream read(Path path, OpenOption... options) throws IOException {
+ FSFile file = getFileAt(path);
+ if (file == null) throw new NoSuchFileException("File not found: " + path);
+ return new FSInputStream(file, options);
+ }
+
+ @Override
+ public OutputStream write(Path path, OpenOption... options) throws IOException {
+ boolean create = false;
+ boolean createNew = false;
+ for (OpenOption option : options) {
+ if (option == StandardOpenOption.CREATE) {
+ create = true;
+ } else if (option == StandardOpenOption.CREATE_NEW) {
+ createNew = true;
+ }
+ }
+
+ if (create && createNew) throw new IllegalArgumentException("Cannot create and createNew");
+
+ FSFile file;
+ if (create) {
+ Ext2Directory parentDir = getDirectoryAt(path.getParent());
+ if (parentDir == null) {
+ throw new NoSuchFileException(path.getParent().toString(), null, "parent directory not found");
+ }
+ Ext2Entry entry = (Ext2Entry) parentDir.getEntry(path.getFileName().toString());
+ if (entry == null) {
+ file = parentDir.addFile(path.getFileName().toString()).getFile();
+ } else if (entry.isFile()) {
+ file = entry.getFile();
+ } else {
+ throw new FileAlreadyExistsException("Directory already exists: " + path);
+ }
+ } else if (createNew) {
+ Ext2Directory parentDir = getDirectoryAt(path.getParent());
+ if (parentDir == null) {
+ throw new NoSuchFileException(path.getParent().toString(), null, "parent directory not found");
+ }
+ if (parentDir.getEntry(path.getFileName().toString()) != null) {
+ throw new FileAlreadyExistsException("File already exists: " + path);
+ }
+ file = parentDir.addFile(path.getFileName().toString()).getFile();
+ } else {
+ file = getFileAt(path);
+ if (file == null) throw new NoSuchFileException(path.toString(), null, "file not found");
+ }
+ return new FSOutputStream(file, options);
+ }
+
+ @Override
+ public boolean exists(Path path) {
+ try {
+ return getFsEntry(path) != null;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ private FSFile getFileAt(Path path) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return null;
+ return entry.getFile();
+ }
+
+ private Ext2Directory getDirectoryAt(Path path) throws IOException {
+ Preconditions.checkNotNull(path, "path");
+ Ext2Entry fsEntry = getFsEntry(path);
+ if (fsEntry == null) return null;
+ if (!fsEntry.isDirectory()) throw new IllegalArgumentException("Path is not a directory: " + path);
+ return (Ext2Directory) fsEntry.getDirectory();
+ }
+
+ private @Nullable Ext2Entry getFsEntry(Path path) throws IOException {
+ if (!path.toString().startsWith("/")) path = Path.of("/" + path);
+ if (!path.toString().startsWith("/")) path = Path.of("/" + path);
+ if (path.getParent() == null) return (Ext2Entry) fs.getRootEntry();
+
+ Ext2Directory root = (Ext2Directory) fs.getRootEntry().getDirectory();
+ for (Path s : path.getParent()) {
+ Ext2Entry entry = (Ext2Entry) root.getEntry(s.toString());
+ if (entry == null) return null;
+ if (!entry.isDirectory()) return null;
+ root = (Ext2Directory) entry.getDirectory();
+ if (root == null) return null;
+ }
+ return (Ext2Entry) root.getEntry(path.getFileName().toString());
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (fs instanceof AbstractFileSystem>) ((AbstractFileSystem) fs).flush();
+ }
+
+ @Override
+ public void createFile(Path path, byte[] data) throws IOException {
+ if (!path.toString().startsWith("/")) path = Path.of("/" + path);
+ if (path.toString().equals("/")) throw new IOException("Invalid path for file: " + path);
+
+ Ext2Directory parentDir = path.getParent() == null ? (Ext2Directory) fs.getRootEntry().getDirectory() : getDirectoryAt(path.getParent());
+ if (parentDir == null)
+ throw new NoSuchFileException(path.getParent().toString(), null, "parent directory not found");
+ FSFile file = parentDir.addFile(path.getFileName().toString()).getFile();
+ file.flush();
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ for (int i = 0; i < data.length; i += 1024) {
+ buffer.clear();
+ buffer.put(data, i, Math.min(1024, data.length - i));
+ buffer.flip();
+ file.write(i, buffer);
+ file.flush();
+ }
+ if (fs instanceof AbstractFileSystem> absFs) absFs.flush();
+ }
+
+ @Override
+ public void createDirectory(Path path) throws IOException {
+ if (!path.toString().startsWith("/")) path = Path.of("/" + path);
+ Ext2Directory parentDir = path.getParent() == null ? (Ext2Directory) fs.getRootEntry().getDirectory() : getDirectoryAt(path.getParent());
+ if (path.getParent() != null && parentDir != null) {
+ parentDir.addDirectory(path.getFileName().toString());
+ parentDir.flush();
+ } else {
+ FSDirectory directory = fs.getRootEntry().getDirectory();
+ directory.addDirectory(path.getFileName().toString());
+ directory.flush();
+ }
+ if (fs instanceof AbstractFileSystem> absFs) absFs.flush();
+ }
+
+ @Override
+ public Iterator listDirectory(Path of) throws IOException {
+ Ext2Directory dir = getDirectoryAt(of);
+ if (dir == null) return Collections.emptyIterator();
+ return new DirNameIterator(dir);
+ }
+
+ @Override
+ public void delete(Path path) throws IOException {
+ Ext2Entry parent = path.getParent() == null ? (Ext2Entry) fs.getRootEntry() : getFsEntry(path.getParent());
+ if (parent == null) return;
+
+ if (!parent.isDirectory()) throw new NotDirectoryException("Path is not a directory: " + path);
+
+ Ext2Entry entry = (Ext2Entry) parent.getDirectory().getEntry(path.getFileName().toString());
+ if (entry.isDirectory()) {
+ if (entry.getDirectory().iterator().hasNext()) {
+ throw new DirectoryNotEmptyException("Directory is not empty: " + path);
+ }
+
+ parent.getDirectory().remove(path.getFileName().toString());
+ return;
+ }
+
+ if (!entry.isFile()) throw new IOException("Path is not a file: " + path);
+ parent.getDirectory().remove(path.getFileName().toString());
+ }
+
+ @Override
+ public long size(Path path) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return -1;
+ if (!entry.isFile()) return -1;
+ return entry.getFile().getLength();
+ }
+
+ @Override
+ public void rename(Path from, String name) throws IOException {
+ if (name.contains("/")) throw new IOException("Invalid name: " + name);
+
+ Ext2Entry parent = from.getParent() == null ? (Ext2Entry) fs.getRootEntry() : getFsEntry(from.getParent());
+ if (parent == null) return;
+
+ if (!parent.isDirectory()) throw new NotDirectoryException("Path is not a directory: " + from);
+
+ Ext2Entry entry = (Ext2Entry) parent.getDirectory().getEntry(from.getFileName().toString());
+ if (entry == null) return;
+ entry.setName(name);
+ parent.getDirectory().flush();
+ }
+
+ @Override
+ public boolean isFolder(Path path) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ return entry != null && entry.isDirectory();
+ }
+
+ @Override
+ public boolean isFile(Path path) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ return entry != null && entry.isFile();
+ }
+
+ @Override
+ public boolean isSymbolicLink(Path path) {
+ return false;
+ }
+
+ @Override
+ public @Nullable LockKey lock(String path) throws IOException {
+ if (path == null) throw new IllegalArgumentException("Path must not be null");
+ Path pathObj = Path.of(path);
+ if (!exists(pathObj)) return null;
+ if (this.locks.containsKey(pathObj))
+ throw new IOException("Path is already locked: " + pathObj);
+ this.locks.put(pathObj, Unit.INSTANCE);
+ return new LockKey(pathObj);
+ }
+
+ @Override
+ public void unlock(String directory) {
+ if (directory == null) throw new IllegalArgumentException("Path must not be null");
+ this.locks.remove(Path.of(directory));
+ }
+
+ @Override
+ public boolean isLocked(String directory) {
+ return this.locks.containsKey(Path.of(directory));
+ }
+
+ @Override
+ public void setReadOnly(Path of, boolean b) throws IOException {
+ @Nullable Ext2Entry dir = getFsEntry(of);
+ if (dir == null) return;
+ dir.getAccessRights().setReadable(true, false);
+ dir.getAccessRights().setWritable(!b, false);
+ dir.getParent().flush();
+ }
+
+ @Override
+ public void setExecutable(Path of, boolean b) throws IOException {
+ @Nullable Ext2Entry dir = getFsEntry(of);
+ if (dir == null) return;
+ if (dir.isFile()) {
+ dir.getAccessRights().setExecutable(b, false);
+ dir.getParent().flush();
+ } else {
+ throw new IOException("Path is not a file: " + of);
+ }
+ }
+
+ @Override
+ public boolean isExecutable(Path of) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) return false;
+ return entry.isFile() && entry.getAccessRights().canExecute();
+ }
+
+ @Override
+ public boolean isWritable(Path of) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) return false;
+ return entry.isFile() && entry.getAccessRights().canWrite();
+ }
+
+ @Override
+ public boolean isReadable(Path of) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) return false;
+ return entry.isFile() && entry.getAccessRights().canRead();
+ }
+
+ @Override
+ public int getOwner(Path of) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) throw new IOException("Path does not exist: " + of);
+ return entry.getINode().getUid();
+ }
+
+ @Override
+ public int getGroup(Path of) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) throw new IOException("Path does not exist: " + of);
+ return entry.getINode().getGid();
+ }
+
+ @Override
+ public int getPermissions(Path of) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) throw new IOException("Path does not exist: " + of);
+ return entry.getINode().getMode();
+ }
+
+ @Override
+ public void setPermissions(Path of, int mode) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) throw new IOException("Path does not exist: " + of);
+ entry.getINode().setMode(mode);
+ entry.getParent().flush();
+ }
+
+ @Override
+ public void setOwner(Path of, int uid, int gid) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) throw new IOException("Path does not exist: " + of);
+ entry.getINode().setUid(uid);
+ entry.getINode().setGid(gid);
+ entry.getParent().flush();
+ }
+
+ @Override
+ public void setGroup(Path of, int gid) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) throw new IOException("Path does not exist: " + of);
+ entry.getINode().setGid(gid);
+ entry.getParent().flush();
+ }
+
+ @Override
+ public void setOwner(Path of, int uid) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) throw new IOException("Path does not exist: " + of);
+ entry.getINode().setUid(uid);
+ entry.getParent().flush();
+ }
+
+ @Override
+ public long getGeneration(Path of) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) throw new IOException("Path does not exist: " + of);
+ return entry.getINode().getGeneration();
+ }
+
+ @Override
+ public void setGeneration(Path of, long generation) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) throw new IOException("Path does not exist: " + of);
+ entry.getINode().setGeneration(generation);
+ entry.getParent().flush();
+ }
+
+ @Override
+ public boolean isReadOnly(Path of) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) throw new IOException("Path does not exist: " + of);
+ return entry.getAccessRights().canRead() && !entry.getAccessRights().canWrite();
+ }
+
+ @Override
+ public boolean canWrite(Path of) throws IOException {
+ return isWritable(of);
+ }
+
+ @Override
+ public boolean canRead(Path of) throws IOException {
+ return isReadable(of);
+ }
+
+ @Override
+ public boolean canExecute(Path of) throws IOException {
+ Ext2Entry entry = getFsEntry(of);
+ if (entry == null) return false;
+ return entry.getAccessRights().canExecute();
+ }
+
+ @Override
+ public long lastModified(Path path) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return 0;
+ return entry.getLastModified();
+ }
+
+ @Override
+ public long lastAccessed(Path path) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return 0;
+ return entry.getLastAccessed();
+ }
+
+ @Override
+ public long creationTime(Path path) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return 0;
+ return entry.getINode().getCtime();
+ }
+
+ @Override
+ public void setLastAccessed(Path path, long time) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return;
+ entry.setLastAccessed(time);
+ entry.getParent().flush();
+ }
+
+ @Override
+ public void setLastModified(Path path, long time) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return;
+ entry.setLastModified(time);
+ entry.getParent().flush();
+ }
+
+ @Override
+ public void setCreationTime(Path path, long time) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return;
+ entry.getINode().setCtime(time);
+ entry.getParent().flush();
+ }
+
+ @Override
+ public long getTotalSpace() throws IOException {
+ return fs.getTotalSpace();
+ }
+
+ @Override
+ public long getUsableSpace() throws IOException {
+ return fs.getUsableSpace();
+ }
+
+ @Override
+ public long getFreeSpace() throws IOException {
+ return fs.getFreeSpace();
+ }
+
+ @Override
+ public void move(Path source, Path destination) throws IOException {
+ Ext2Entry sourceEntry = getFsEntry(source);
+ Ext2Entry destinationEntry = getFsEntry(destination);
+ if (sourceEntry == null || destinationEntry == null) return;
+
+ try (InputStream in = read(source)) {
+ createFile(destination, in.readAllBytes());
+ }
+
+ sourceEntry.getParent().remove(sourceEntry.getName());
+ sourceEntry.getParent().flush();
+ destinationEntry.getParent().flush();
+ }
+
+ @Override
+ public void copy(Path source, Path destination) throws IOException {
+ Ext2Entry sourceEntry = getFsEntry(source);
+ Ext2Entry destinationEntry = getFsEntry(destination);
+ if (sourceEntry == null || destinationEntry == null) return;
+
+ try (InputStream in = read(source)) {
+ createFile(destination, in.readAllBytes());
+ }
+
+ sourceEntry.getParent().flush();
+ destinationEntry.getParent().flush();
+ }
+
+ @Override
+ public void write(Path path, long offset, byte[] dataBytes) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return;
+
+ if (entry.isFile()) {
+ FSFile file = entry.getFile();
+ long length = file.getLength();
+ if (offset > length) throw new IOException("Offset out of range: " + offset);
+ if (offset + dataBytes.length > length) {
+ file.setLength(offset + dataBytes.length);
+ }
+ file.write(offset, ByteBuffer.wrap(dataBytes));
+ file.flush();
+ entry.getParent().flush();
+ } else {
+ throw new IOException("Path is not a file: " + path);
+ }
+ }
+
+ @Override
+ public void write(Path path, byte[] dataBytes) throws IOException {
+ write(path, 0, dataBytes);
+ }
+
+ @Override
+ public void truncate(Path path, long size) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return;
+ if (entry.isFile()) {
+ entry.getFile().setLength(size);
+ }
+ }
+
+ @Override
+ public void read(Path path, ByteBuffer buffer, long offset) throws IOException {
+ Ext2Entry entry = getFsEntry(path);
+ if (entry == null) return;
+ if (entry.isFile()) {
+ entry.getFile().read(offset, buffer);
+ }
+ }
+
+ public FileSystem> getFileSystem() {
+ return fs;
+ }
+
+ private static class FSInputStream extends InputStream {
+ private final FSFile file;
+ private final ByteBuffer buffer = ByteBuffer.allocate(1024);
+ private long bufferOffset;
+ private long fileOffset = 0;
+
+ private int markLimit;
+
+ public FSInputStream(FSFile file, OpenOption[] options) throws IOException {
+ this.file = file;
+ for (OpenOption option : options) {
+ if (option == StandardOpenOption.TRUNCATE_EXISTING) {
+ file.setLength(0);
+ } else if (option != StandardOpenOption.READ) {
+ throw new UnsupportedOperationException("Option not supported: " + option);
+ }
+ }
+
+ bufferOffset = 0;
+ buffer.clear();
+ if (fileOffset + buffer.capacity() > file.getLength()) {
+ buffer.limit((int) (file.getLength() - fileOffset));
+ }
+ file.read(fileOffset, buffer);
+ buffer.flip();
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (bufferOffset == buffer.limit()) {
+ bufferOffset = 0;
+ buffer.clear();
+ if (fileOffset + buffer.capacity() > file.getLength()) {
+ buffer.limit((int) (file.getLength() - fileOffset));
+ }
+ file.read(fileOffset, buffer);
+ fileOffset += buffer.capacity();
+ }
+
+ return buffer.get() & 0xFF;
+ }
+
+ @Override
+ public int read(byte @NotNull [] b, int off, int len) throws IOException {
+ int bytesRead = 0;
+ if (fileOffset >= file.getLength()) {
+ return -1;
+ }
+ while (bytesRead < len) {
+ if (fileOffset >= file.getLength()) {
+ return bytesRead;
+ }
+
+ int remaining = buffer.remaining();
+ int len0 = Math.min(len - bytesRead, remaining);
+ if (len0 == 0) {
+ return -1;
+ }
+ int fileRemaining = (int) (file.getLength() - fileOffset);
+ int len1 = Math.min(len0, fileRemaining);
+ buffer.get(b, off + bytesRead, len1);
+ bufferOffset += len1;
+ bytesRead += len1;
+ fileOffset += len1;
+ if (bufferOffset == buffer.capacity()) {
+ bufferOffset = 0;
+ buffer.clear();
+ if (fileOffset + buffer.capacity() > file.getLength()) {
+ buffer.limit((int) (file.getLength() - fileOffset));
+ }
+ file.read(fileOffset, buffer);
+ }
+
+ if (len0 < remaining) {
+ break;
+ }
+ }
+ return bytesRead;
+ }
+
+ @Override
+ public int read(byte @NotNull [] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+ }
+
+ private static class DirNameIterator implements Iterator {
+ private final Iterator iterator;
+
+ public DirNameIterator(Ext2Directory dir) throws IOException {
+ iterator = dir.iterator();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public String next() {
+ return iterator.next().getName();
+ }
+ }
+
+ private class FSOutputStream extends OutputStream {
+ private final FSFile file;
+ private final ByteBuffer buffer = ByteBuffer.allocate(1024);
+ private boolean sync = false;
+ private long bufferOffset = 0;
+ private long fileOffset = 0;
+
+ public FSOutputStream(FSFile file, OpenOption[] options) throws IOException {
+ this.file = file;
+ for (OpenOption option : options) {
+ if (option == StandardOpenOption.TRUNCATE_EXISTING) {
+ file.setLength(0);
+ } else if (option == StandardOpenOption.APPEND) {
+ fileOffset = file.getLength();
+ } else if (option == StandardOpenOption.SYNC) {
+ sync = true;
+ } else if (option != StandardOpenOption.WRITE && option != StandardOpenOption.CREATE && option != StandardOpenOption.CREATE_NEW) {
+ throw new UnsupportedOperationException("Option not supported: " + option);
+ }
+ }
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ buffer.put((byte) b);
+ bufferOffset++;
+ if (bufferOffset == buffer.capacity()) {
+ buffer.flip();
+ file.write(fileOffset, buffer);
+ buffer.clear();
+ bufferOffset = 0;
+ fileOffset += buffer.capacity();
+ }
+ }
+
+ @Override
+ public void write(byte @NotNull [] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ file.flush();
+ if (fs instanceof AbstractFileSystem> absFs) absFs.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ file.flush();
+ if (fs instanceof AbstractFileSystem> absFs) absFs.flush();
+ }
+ }
+}
diff --git a/bootimage/src/main/java/dev/ultreon/bootimager/FS.java b/bootimage/src/main/java/dev/ultreon/bootimager/FS.java
new file mode 100644
index 000000000..a2b37126b
--- /dev/null
+++ b/bootimage/src/main/java/dev/ultreon/bootimager/FS.java
@@ -0,0 +1,130 @@
+package dev.ultreon.bootimager;
+
+import org.jetbrains.annotations.Nullable;
+import org.jnode.fs.FileSystemException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.util.Iterator;
+
+public interface FS extends AutoCloseable {
+ void close() throws IOException;
+
+ InputStream read(Path path, OpenOption... options) throws IOException;
+
+ OutputStream write(Path path, OpenOption... options) throws IOException;
+
+ boolean exists(Path path) throws IOException;
+
+ void flush() throws IOException;
+
+ void createFile(Path path, byte[] data) throws IOException;
+
+ void createDirectory(Path path) throws IOException;
+
+ Iterator listDirectory(Path of) throws IOException;
+
+ void delete(Path path) throws IOException;
+
+ long size(Path path) throws IOException;
+
+ void rename(Path from, String name) throws IOException;
+
+ boolean isFolder(Path path) throws IOException;
+
+ boolean isFile(Path path) throws IOException;
+
+ boolean isSymbolicLink(Path path) throws IOException;
+
+ @Nullable LockKey lock(String path) throws IOException;
+
+ void unlock(String directory);
+
+ boolean isLocked(String directory);
+
+ void setReadOnly(Path of, boolean b) throws IOException;
+
+ void setExecutable(Path of, boolean b) throws IOException;
+
+ boolean isExecutable(Path of) throws IOException;
+
+ boolean isWritable(Path of) throws IOException;
+
+ boolean isReadable(Path of) throws IOException;
+
+ int getOwner(Path of) throws IOException;
+
+ int getGroup(Path of) throws IOException;
+
+ int getPermissions(Path of) throws IOException;
+
+ void setPermissions(Path of, int mode) throws IOException;
+
+ void setOwner(Path of, int uid, int gid) throws IOException;
+
+ void setGroup(Path of, int gid) throws IOException;
+
+ void setOwner(Path of, int uid) throws IOException;
+
+ long getGeneration(Path of) throws IOException;
+
+ void setGeneration(Path of, long generation) throws IOException;
+
+ boolean isReadOnly(Path of) throws IOException;
+
+ boolean canWrite(Path of) throws IOException;
+
+ boolean canRead(Path of) throws IOException;
+
+ boolean canExecute(Path of) throws IOException;
+
+ static FS loadExt2(Path path) throws IOException, FileSystemException {
+ return Ext2FS.open(false, path);
+ }
+
+ static FS loadExt2Forced(Path path) throws IOException, FileSystemException {
+ return Ext2FS.openForced(path);
+ }
+
+ static FS loadExt2ReadOnly(Path path) throws IOException, FileSystemException {
+ return Ext2FS.open(true, path);
+ }
+
+ static FS formatExt2(Path path, long diskSize) throws IOException, FileSystemException {
+ return Ext2FS.format(path, diskSize);
+ }
+
+ long lastModified(Path path) throws IOException;
+
+ long lastAccessed(Path path) throws IOException;
+
+ long creationTime(Path path) throws IOException;
+
+ void setLastAccessed(Path path, long time) throws IOException;
+
+ void setLastModified(Path path, long time) throws IOException;
+
+ void setCreationTime(Path path, long time) throws IOException;
+
+ long getTotalSpace() throws IOException;
+
+ long getUsableSpace() throws IOException;
+
+ long getFreeSpace() throws IOException;
+
+ void move(Path source, Path destination) throws IOException;
+
+ void copy(Path source, Path destination) throws IOException;
+
+ void write(Path path, long offset, byte[] dataBytes) throws IOException;
+
+ void write(Path path, byte[] dataBytes) throws IOException;
+
+ void truncate(Path path, long size) throws IOException;
+
+ void read(Path path, ByteBuffer buffer, long offset) throws IOException;
+}
diff --git a/bootimage/src/main/java/dev/ultreon/bootimager/LockKey.java b/bootimage/src/main/java/dev/ultreon/bootimager/LockKey.java
new file mode 100644
index 000000000..ea883e7b9
--- /dev/null
+++ b/bootimage/src/main/java/dev/ultreon/bootimager/LockKey.java
@@ -0,0 +1,16 @@
+package dev.ultreon.bootimager;
+
+import java.nio.file.Path;
+
+@SuppressWarnings("ClassCanBeRecord")
+public class LockKey {
+ private final Path path;
+
+ public LockKey(Path path) {
+ this.path = path;
+ }
+
+ public Path getPath() {
+ return path;
+ }
+}
diff --git a/bootimage/src/main/java/dev/ultreon/bootimager/VirtualBlockDevice.java b/bootimage/src/main/java/dev/ultreon/bootimager/VirtualBlockDevice.java
new file mode 100644
index 000000000..7e8c2bab6
--- /dev/null
+++ b/bootimage/src/main/java/dev/ultreon/bootimager/VirtualBlockDevice.java
@@ -0,0 +1,65 @@
+package dev.ultreon.bootimager;
+
+import org.jnode.driver.block.BlockDeviceAPI;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class VirtualBlockDevice implements BlockDeviceAPI {
+ private final RandomAccessFile file;
+
+ public VirtualBlockDevice(String filePath, long size) throws IOException {
+ Path path = Path.of(filePath);
+ if (Files.notExists(path)) {
+ Files.createFile(path);
+ }
+ this.file = new RandomAccessFile(filePath, "rw");
+
+ // Ensure the file is initialized to the specified size
+ if (file.length() < size) {
+ file.setLength(size);
+ }
+ }
+
+ @Override
+ public long getLength() throws IOException {
+ return file.length();
+ }
+
+ @Override
+ public void read(long devOffset, ByteBuffer dest) throws IOException {
+ if (devOffset < 0 || devOffset >= file.length()) {
+ throw new IOException("Invalid read offset");
+ }
+ file.seek(devOffset);
+ byte[] buffer = new byte[dest.remaining()];
+ int bytesRead = file.read(buffer);
+ if (bytesRead < 0) {
+ throw new IOException("End of file reached");
+ }
+ dest.put(buffer, 0, bytesRead);
+ }
+
+ @Override
+ public void write(long devOffset, ByteBuffer src) throws IOException {
+ if (devOffset < 0 || devOffset >= file.length()) {
+ throw new IOException("Invalid write offset");
+ }
+ file.seek(devOffset);
+ byte[] buffer = new byte[src.remaining()];
+ src.get(buffer);
+ file.write(buffer);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ file.getFD().sync();
+ }
+
+ public void close() throws IOException {
+ file.close();
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index ecd7ddc8f..6594b5b8d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,6 +22,16 @@ Object getModDescription() {
return mod_description
}
+tasks.register("runBootImager", JavaExec) {
+ classpath(files(rootProject.project("bootimage").sourceSets.main.output + project(":bootimage").sourceSets.main.runtimeClasspath))
+ mainClass = "dev.ultreon.bootimager.BootImager"
+}
+
+project("common").tasks.withType(ProcessResources).configureEach {
+ dependsOn ":runBootImager"
+}
+
+
subprojects {
apply plugin: "dev.architectury.loom"
diff --git a/common/build.gradle b/common/build.gradle
index d7641c8f2..46cef1b17 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -18,6 +18,8 @@ dependencies {
// Remove the next line if you don't want to depend on the API
modApi "dev.architectury:architectury:$architectury_version"
+ implementation "org.graalvm.polyglot:polyglot:24.1.1"
+ implementation "org.graalvm.python:python-community:24.1.1"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.7.10"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10"
// modApi "com.ultreon:ultranlang:0.1.0+6"
diff --git a/common/src/main/java/com/ultreon/devices/BuiltinApps.java b/common/src/main/java/com/ultreon/devices/BuiltinApps.java
index a2815f920..43a81afc9 100644
--- a/common/src/main/java/com/ultreon/devices/BuiltinApps.java
+++ b/common/src/main/java/com/ultreon/devices/BuiltinApps.java
@@ -4,11 +4,13 @@
import com.ultreon.devices.programs.BoatRacersApp;
import com.ultreon.devices.programs.NoteStashApp;
import com.ultreon.devices.programs.PixelPainterApp;
+import com.ultreon.devices.programs.activation.ActivationApp;
import com.ultreon.devices.programs.auction.MineBayApp;
import com.ultreon.devices.programs.email.EmailApp;
import com.ultreon.devices.programs.gitweb.GitWebApp;
import com.ultreon.devices.programs.snake.SnakeApp;
import com.ultreon.devices.programs.system.*;
+import com.ultreon.devices.programs.terminal.TerminalApp;
import com.ultreon.devices.programs.themes.ThemesApp;
import dev.architectury.platform.Platform;
import net.minecraft.resources.ResourceLocation;
@@ -18,6 +20,7 @@ public static void registerBuiltinApps() {
ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "diagnostics"), () -> DiagnosticsApp::new, true);
ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "settings"), () -> SettingsApp::new, true);
ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "file_browser"), () -> FileBrowserApp::new, true);
+ ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "terminal"), () -> TerminalApp::new, false);
ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "gitweb"), () -> GitWebApp::new, false);
ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "note_stash"), () -> NoteStashApp::new, false);
ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "pixel_painter"), () -> PixelPainterApp::new, false);
diff --git a/common/src/main/java/com/ultreon/devices/Devices.java b/common/src/main/java/com/ultreon/devices/Devices.java
index 096558ccf..62c334c26 100644
--- a/common/src/main/java/com/ultreon/devices/Devices.java
+++ b/common/src/main/java/com/ultreon/devices/Devices.java
@@ -5,6 +5,7 @@
import com.google.gson.*;
import com.mojang.serialization.Lifecycle;
import com.ultreon.devices.api.ApplicationManager;
+import com.ultreon.devices.api.DeviceAPI;
import com.ultreon.devices.api.app.Application;
import com.ultreon.devices.api.print.IPrint;
import com.ultreon.devices.api.print.PrintingManager;
@@ -30,6 +31,7 @@
import com.ultreon.devices.programs.IconsApp;
import com.ultreon.devices.programs.PixelPainterApp;
import com.ultreon.devices.programs.TestApp;
+import com.ultreon.devices.programs.activation.TaskActivateMineOS;
import com.ultreon.devices.programs.auction.task.TaskAddAuction;
import com.ultreon.devices.programs.auction.task.TaskBuyItem;
import com.ultreon.devices.programs.auction.task.TaskGetAuctions;
@@ -64,13 +66,24 @@
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.RecipeType;
import net.minecraft.world.level.Level;
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.Engine;
+import org.graalvm.polyglot.Language;
+import org.graalvm.polyglot.Value;
+import org.graalvm.polyglot.io.FileSystem;
+import org.graalvm.polyglot.io.IOAccess;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.lang.reflect.Constructor;
+import java.net.URI;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.*;
+import java.nio.file.attribute.FileAttribute;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
@@ -219,6 +232,9 @@ private void registerApplications() {
TaskManager.registerTask(TaskDeleteEmail::new);
TaskManager.registerTask(TaskViewEmail::new);
+ // Activation Stuff
+ TaskManager.registerTask(TaskActivateMineOS::new);
+
if (Platform.isDevelopmentEnvironment() || Devices.EARLY_CONFIG.enableBetaApps) {
// Auction
TaskManager.registerTask(TaskAddAuction::new);
diff --git a/common/src/main/java/com/ultreon/devices/api/DeviceAPI.java b/common/src/main/java/com/ultreon/devices/api/DeviceAPI.java
new file mode 100644
index 000000000..e7f30d380
--- /dev/null
+++ b/common/src/main/java/com/ultreon/devices/api/DeviceAPI.java
@@ -0,0 +1,38 @@
+package com.ultreon.devices.api;
+
+import com.ultreon.devices.api.io.Drive;
+import com.ultreon.devices.api.io.DriveRoot;
+import com.ultreon.devices.api.io.FSResponse;
+import com.ultreon.devices.core.DataPath;
+import com.ultreon.devices.core.Laptop;
+import com.ultreon.devices.object.Result;
+
+import java.nio.file.Path;
+import java.util.function.Consumer;
+
+public class DeviceAPI {
+ private final Laptop instance;
+
+ public DeviceAPI(Laptop instance) {
+ this.instance = instance;
+ }
+
+ public void openFile(String path, Consumer> response) {
+ Drive mainDrive = Laptop.getMainDrive();
+ if (mainDrive != null) {
+ mainDrive.read(Path.of(path), fsResponse -> {
+ boolean success = fsResponse.success();
+ if (!success) {
+ response.accept(new Result<>(fsResponse.message(), null, false));
+ return;
+ }
+
+ byte[] data = fsResponse.data();
+ FileHandle handle = new FileHandle(path, data);
+ response.accept(new Result<>(null, handle, true));
+ });
+ } else {
+ response.accept(new Result<>("No main drive", null, false));
+ }
+ }
+}
diff --git a/common/src/main/java/com/ultreon/devices/api/FileHandle.java b/common/src/main/java/com/ultreon/devices/api/FileHandle.java
new file mode 100644
index 000000000..892dc9f02
--- /dev/null
+++ b/common/src/main/java/com/ultreon/devices/api/FileHandle.java
@@ -0,0 +1,48 @@
+package com.ultreon.devices.api;
+
+public class FileHandle {
+ private final String path;
+ private final byte[] data;
+
+ private int offset = 0;
+ private boolean closed;
+
+ public FileHandle(String path, byte[] data) {
+ this.path = path;
+ this.data = data;
+ }
+
+ public int tell() {
+ return offset;
+ }
+
+ public void seek(int offset) {
+ this.offset = offset;
+ }
+
+ public byte[] read(int length) {
+ if (closed) {
+ throw new IllegalStateException("File is closed");
+ }
+ byte[] result = new byte[length];
+ System.arraycopy(data, offset, result, 0, length);
+ offset += length;
+ return result;
+ }
+
+ public void write(byte[] data) {
+ if (closed) {
+ throw new IllegalStateException("File is closed");
+ }
+ System.arraycopy(data, 0, this.data, offset, data.length);
+ offset += data.length;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void close() {
+ this.closed = true;
+ }
+}
diff --git a/common/src/main/java/com/ultreon/devices/api/FileSystemAPI.java b/common/src/main/java/com/ultreon/devices/api/FileSystemAPI.java
new file mode 100644
index 000000000..8d37a7859
--- /dev/null
+++ b/common/src/main/java/com/ultreon/devices/api/FileSystemAPI.java
@@ -0,0 +1,6 @@
+package com.ultreon.devices.api;
+
+import com.ultreon.devices.core.Laptop;
+
+public class FileSystemAPI {
+}
diff --git a/common/src/main/java/com/ultreon/devices/api/app/Application.java b/common/src/main/java/com/ultreon/devices/api/app/Application.java
index c0ade7346..125018b42 100644
--- a/common/src/main/java/com/ultreon/devices/api/app/Application.java
+++ b/common/src/main/java/com/ultreon/devices/api/app/Application.java
@@ -131,8 +131,11 @@ public void render(GuiGraphics graphics, Laptop laptop, Minecraft mc, int x, int
// GL11.glEnable(GL11.GL_SCISSOR_TEST);
GLHelper.pushScissor(x, y, width, height);
- currentLayout.render(graphics, laptop, mc, x, y, mouseX, mouseY, active, partialTicks);
- GLHelper.popScissor();
+ try {
+ currentLayout.render(graphics, laptop, mc, x, y, mouseX, mouseY, active, partialTicks);
+ } finally {
+ GLHelper.popScissor();
+ }
// TODO Port this to 1.18.2 if possible
// if (!GLHelper.isScissorStackEmpty()) {
diff --git a/common/src/main/java/com/ultreon/devices/api/app/Dialog.java b/common/src/main/java/com/ultreon/devices/api/app/Dialog.java
index b3ce02ee2..4e1529b0d 100644
--- a/common/src/main/java/com/ultreon/devices/api/app/Dialog.java
+++ b/common/src/main/java/com/ultreon/devices/api/app/Dialog.java
@@ -43,8 +43,8 @@ public abstract class Dialog extends Wrappable {
protected final ColorScheme colorScheme = Laptop.getInstance().getSettings().getColorScheme();
protected final Layout defaultLayout;
private String title = "Message";
- private int width;
- private int height;
+ protected int width;
+ protected int height;
private Layout customLayout;
private boolean pendingLayoutUpdate = true;
@@ -172,6 +172,7 @@ public void updateComponents(int x, int y) {
public void close() {
this.pendingClose = true;
+ this.getWindow().close();
}
/// The response listener interface. Used for handling responses
diff --git a/common/src/main/java/com/ultreon/devices/api/io/Drive.java b/common/src/main/java/com/ultreon/devices/api/io/Drive.java
index d1d12c267..86cd7b7c2 100644
--- a/common/src/main/java/com/ultreon/devices/api/io/Drive.java
+++ b/common/src/main/java/com/ultreon/devices/api/io/Drive.java
@@ -19,6 +19,7 @@
import static com.ultreon.devices.core.io.FileSystem.request;
public class Drive {
+ public static final int BUFFER_SIZE = 1024;
private final String name;
private final UUID uuid;
private final Type type;
@@ -169,7 +170,7 @@ public void rename(Path path, String name, Consumer> callback)
}
public void write(Path path, byte[] data, Consumer> callback) {
- if (data.length < 1024) request(uuid, FileAction.Factory.makeWrite(path, -1, data), callback);
+ if (data.length < BUFFER_SIZE) request(uuid, FileAction.Factory.makeWrite(path, -1, data), callback);
else if (data.length < 4 * 1024 * 1024) writeLarge(path, data, callback);
else callback.accept(new FSResponse<>(false, FileSystem.Status.TOO_LARGE, null, "File too large"));
}
@@ -181,7 +182,7 @@ public void read(Path path, Consumer> callback) {
return;
}
- if (info.data().getSize() >= 1024) {
+ if (info.data().getSize() >= BUFFER_SIZE) {
readLarge(path, info.data().getSize(), callback);
} else {
request(uuid, FileAction.Factory.makeRead(path), callback);
@@ -191,27 +192,10 @@ public void read(Path path, Consumer> callback) {
private void readLarge(Path path, long size, Consumer> callback) {
byte[] output = new byte[(int) size];
- AtomicInteger offset = new AtomicInteger(0);
- int length = (int) Math.min(1024, size - offset.get());
- var ref = new Object() {
- void fsResponseConsumer(FSResponse r) {
- if (!r.success()) {
- callback.accept(r);
- return;
- }
-
- int len = r.data().length;
- System.arraycopy(r.data(), 0, output, offset.get(), len);
-
- if (offset.get() == length) {
- callback.accept(new FSResponse<>(true, FileSystem.Status.SUCCESSFUL, output, ""));
- return;
- }
-
- request(uuid, FileAction.Factory.makeRead(path, offset.getAndSet(offset.get() + r.data().length) + r.data().length, (int) Math.min(1024, size - offset.get())), this::fsResponseConsumer);
- }
- };
- request(uuid, FileAction.Factory.makeRead(path, offset.get(), length), ref::fsResponseConsumer);
+ int offset = 0;
+ int length = (int) Math.min(BUFFER_SIZE, size - offset);
+ var ref = new ReaderCallback(callback, output, offset, length, path, size);
+ ref.requestRead(0, BUFFER_SIZE);
}
public DataPath getRootDirectory() {
@@ -230,9 +214,9 @@ public void createDirectories(Path path, Consumer> o) {
}
private void writeLarge(Path path, byte[] data, Consumer> callback) {
- byte[] buffer = new byte[1024];
+ byte[] buffer = new byte[BUFFER_SIZE];
AtomicInteger offset = new AtomicInteger(0);
- int length = Math.min(1024, data.length - offset.get());
+ int length = Math.min(BUFFER_SIZE, data.length - offset.get());
System.arraycopy(data, 0, buffer, 0, length);
var ref = new Object() {
void fsResponseConsumer(FSResponse r) {
@@ -255,7 +239,7 @@ void fsResponseConsumer(FSResponse r) {
}
private boolean writeNext(Path path, byte[] data, int offset, Consumer> callback) {
- int length = Math.min(1024, data.length - offset);
+ int length = Math.min(BUFFER_SIZE, data.length - offset);
if (length <= 0) {
return false;
}
@@ -277,4 +261,44 @@ public static Type fromString(String type) {
return UNKNOWN;
}
}
+
+ private class ReaderCallback {
+ private final Consumer> callback;
+ private final byte[] output;
+ private final int length;
+ private final Path path;
+ private final long size;
+ private int offset;
+
+ public ReaderCallback(Consumer> callback, byte[] output, int offset, int length, Path path, long size) {
+ this.callback = callback;
+ this.output = output;
+ this.offset = offset;
+ this.length = length;
+ this.path = path;
+ this.size = size;
+ }
+
+ void fsResponseConsumer(FSResponse r) {
+ if (!r.success()) {
+ callback.accept(r);
+ return;
+ }
+
+ int len = r.data().length;
+ System.arraycopy(r.data(), 0, output, offset, len);
+
+ if (offset + len == size) {
+ callback.accept(new FSResponse<>(true, FileSystem.Status.SUCCESSFUL, output, ""));
+ return;
+ }
+
+ requestRead(offset + len, BUFFER_SIZE);
+ }
+
+ private void requestRead(long offset, int length) {
+ this.offset = (int) offset;
+ request(uuid, FileAction.Factory.makeRead(path, offset, (int) Math.min(length, size - offset)), this::fsResponseConsumer);
+ }
+ }
}
diff --git a/common/src/main/java/com/ultreon/devices/core/BiosApi.java b/common/src/main/java/com/ultreon/devices/core/BiosApi.java
new file mode 100644
index 000000000..98f1e69f5
--- /dev/null
+++ b/common/src/main/java/com/ultreon/devices/core/BiosApi.java
@@ -0,0 +1,35 @@
+package com.ultreon.devices.core;
+
+import org.graalvm.polyglot.Value;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class BiosApi {
+ private final Laptop laptop;
+ private InventoryApi inventory;
+ private UiApi uiApi;
+
+ public BiosApi(Laptop laptop) {
+ this.laptop = laptop;
+
+ this.inventory = new InventoryApi(laptop);
+ this.uiApi = new UiApi(laptop);
+ }
+
+ public boolean isWorldLess() {
+ return Laptop.isWorldLess();
+ }
+
+ public InventoryApi getInventoryApi() {
+ return inventory;
+ }
+
+ public UiApi getUiApi() {
+ return uiApi;
+ }
+
+ public PyProcess spawnProcess(Value modules, String[] command, Map env) throws IOException {
+ return laptop.spawnProcess(modules, command, env);
+ }
+}
diff --git a/common/src/main/java/com/ultreon/devices/core/BootImage.java b/common/src/main/java/com/ultreon/devices/core/BootImage.java
new file mode 100644
index 000000000..922a34331
--- /dev/null
+++ b/common/src/main/java/com/ultreon/devices/core/BootImage.java
@@ -0,0 +1,26 @@
+package com.ultreon.devices.core;
+
+import com.ultreon.devices.core.io.drive.AbstractDrive;
+import net.minecraft.world.level.storage.LevelResource;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.UUID;
+
+public class BootImage {
+ private final byte[] bytes;
+
+ public BootImage(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ public void write(UUID uuid) throws IOException {
+ if (uuid == null) return;
+ Path file = AbstractDrive.getDrivePath(uuid);
+ Files.createDirectories(file.getParent());
+ Files.write(file, bytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ }
+}
diff --git a/common/src/main/java/com/ultreon/devices/core/Ext2FS.java b/common/src/main/java/com/ultreon/devices/core/Ext2FS.java
index e99e1264e..1bd0b1cee 100644
--- a/common/src/main/java/com/ultreon/devices/core/Ext2FS.java
+++ b/common/src/main/java/com/ultreon/devices/core/Ext2FS.java
@@ -1,6 +1,11 @@
package com.ultreon.devices.core;
import com.google.common.base.Preconditions;
+import com.ultreon.devices.Reference;
+import com.ultreon.devices.core.io.drive.AbstractDrive;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.Resource;
+import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.Unit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -21,9 +26,12 @@
import java.nio.file.*;
import java.util.Collections;
import java.util.Iterator;
+import java.util.Optional;
+import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+@SuppressWarnings("t")
public class Ext2FS implements FS {
private final FileSystem> fs;
private final ConcurrentMap locks = new ConcurrentHashMap<>();
@@ -41,6 +49,17 @@ private Ext2FS(Ext2FileSystem fs) {
});
}
+ public static Ext2FS loadBootImage(ResourceManager manager, String name, UUID uuid) throws FileSystemException {
+ Optional resource = manager.getResource(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "filesystems/" + name + ".ext2"));
+ if (resource.isEmpty()) return null;
+ try (var opened = resource.get().open()) {
+ Files.copy(opened, AbstractDrive.getDrivePath(uuid));
+ return Ext2FS.open(AbstractDrive.getDrivePath(uuid));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
public static Ext2FS open(Path filePath) throws IOException, FileSystemException {
return open(false, filePath);
}
@@ -160,7 +179,8 @@ private Ext2Directory getDirectoryAt(Path path) throws IOException {
return (Ext2Directory) fsEntry.getDirectory();
}
- private @Nullable Ext2Entry getFsEntry(Path path) throws IOException {
+ @Nullable
+ public Ext2Entry getFsEntry(Path path) throws IOException {
if (!path.toString().startsWith("/")) path = Path.of("/" + path);
if (!path.toString().startsWith("/")) path = Path.of("/" + path);
if (path.getParent() == null) return (Ext2Entry) fs.getRootEntry();
@@ -168,6 +188,7 @@ private Ext2Directory getDirectoryAt(Path path) throws IOException {
Ext2Directory root = (Ext2Directory) fs.getRootEntry().getDirectory();
for (Path s : path.getParent()) {
Ext2Entry entry = (Ext2Entry) root.getEntry(s.toString());
+ if (entry == null) return null;
if (!entry.isDirectory()) return null;
root = (Ext2Directory) entry.getDirectory();
if (root == null) return null;
diff --git a/common/src/main/java/com/ultreon/devices/core/InventoryApi.java b/common/src/main/java/com/ultreon/devices/core/InventoryApi.java
new file mode 100644
index 000000000..c80bfef4e
--- /dev/null
+++ b/common/src/main/java/com/ultreon/devices/core/InventoryApi.java
@@ -0,0 +1,9 @@
+package com.ultreon.devices.core;
+
+public class InventoryApi {
+ private final Laptop laptop;
+
+ public InventoryApi(Laptop laptop) {
+ this.laptop = laptop;
+ }
+}
diff --git a/common/src/main/java/com/ultreon/devices/core/Laptop.java b/common/src/main/java/com/ultreon/devices/core/Laptop.java
index 9089780a1..a1cbabbd4 100644
--- a/common/src/main/java/com/ultreon/devices/core/Laptop.java
+++ b/common/src/main/java/com/ultreon/devices/core/Laptop.java
@@ -5,11 +5,13 @@
import com.mojang.blaze3d.vertex.PoseStack;
import com.ultreon.devices.Devices;
import com.ultreon.devices.api.ApplicationManager;
+import com.ultreon.devices.api.DeviceAPI;
+import com.ultreon.devices.api.app.*;
import com.ultreon.devices.api.app.Dialog;
import com.ultreon.devices.api.app.System;
-import com.ultreon.devices.api.app.*;
import com.ultreon.devices.api.app.component.Image;
import com.ultreon.devices.api.io.Drive;
+import com.ultreon.devices.api.io.FSResponse;
import com.ultreon.devices.api.task.Callback;
import com.ultreon.devices.api.task.Task;
import com.ultreon.devices.api.task.TaskManager;
@@ -19,6 +21,7 @@
import com.ultreon.devices.core.io.task.TaskGetMainDrive;
import com.ultreon.devices.core.task.TaskInstallApp;
import com.ultreon.devices.object.AppInfo;
+import com.ultreon.devices.programs.activation.ActivationApp;
import com.ultreon.devices.programs.system.DiagnosticsApp;
import com.ultreon.devices.programs.system.DisplayResolution;
import com.ultreon.devices.programs.system.PredefinedResolution;
@@ -47,19 +50,35 @@
import net.minecraft.network.chat.Style;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvents;
+import net.minecraft.util.Unit;
+import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
+import org.graalvm.polyglot.*;
+import org.graalvm.polyglot.io.FileSystem;
+import org.graalvm.polyglot.io.IOAccess;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.io.PrintStream;
-import java.nio.file.AccessDeniedException;
-import java.nio.file.Path;
-import java.util.List;
+import java.net.URI;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.UserPrincipal;
import java.util.*;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
+import static java.lang.System.currentTimeMillis;
+
/// Laptop GUI class.
///
/// @author MrCrayfish, Qboi123
@@ -71,8 +90,16 @@ public class Laptop extends Screen implements System {
private static Font font;
private static final ResourceLocation LAPTOP_GUI = ResourceLocation.fromNamespaceAndPath(Devices.MOD_ID, "textures/gui/laptop.png");
private static final List APPLICATIONS = new ArrayList<>();
+ private static final int ACTIVATE_RETRY = 60;
private static boolean worldLess;
private static Laptop instance;
+ private final Context codeContext;
+ private final CompletableFuture codeThread;
+ private final LaptopFileSystem fileSystem;
+ private boolean registered;
+ private UUID license = null;
+ private int retryActivate = seconds2ticks(ACTIVATE_RETRY);
+ private ActivationApp registerApp;
private Double dragWindowFromX;
private Double dragWindowFromY;
private final VideoInfo videoInfo;
@@ -81,6 +108,7 @@ public class Laptop extends Screen implements System {
private Window