diff --git a/.gitignore b/.gitignore index d76fe94..364fc59 100644 --- a/.gitignore +++ b/.gitignore @@ -809,4 +809,6 @@ FodyWeavers.xsd # End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,eclipse,intellij,webstorm,node .idea -*.iso \ No newline at end of file +*.iso + +**/rootfs/* \ No newline at end of file diff --git a/etc/fs2json.py b/etc/fs2json.py new file mode 100644 index 0000000..8d8b4a2 --- /dev/null +++ b/etc/fs2json.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 + +# Note: +# - Hardlinks are copied +# - The size of symlinks and directories is meaningless, it depends on whatever +# the filesystem/tar file reports + +import argparse +import json +import os +import stat +import sys +import itertools +import logging +import hashlib +import tarfile + +VERSION = 3 + +IDX_NAME = 0 +IDX_SIZE = 1 +IDX_MTIME = 2 +IDX_MODE = 3 +IDX_UID = 4 +IDX_GID = 5 + +# target for symbolic links +# child nodes for directories +# filename for files +IDX_TARGET = 6 +IDX_FILENAME = 6 + +HASH_LENGTH = 8 + +S_IFLNK = 0xA000 +S_IFREG = 0x8000 +S_IFDIR = 0x4000 + +def hash_file(filename) -> str: + with open(filename, "rb", buffering=0) as f: + return hash_fileobj(f) + +def hash_fileobj(f) -> str: + h = hashlib.sha256() + for b in iter(lambda: f.read(128*1024), b""): + h.update(b) + return h.hexdigest() + +def main(): + logging.basicConfig(format="%(message)s") + logger = logging.getLogger("fs2json") + logger.setLevel(logging.DEBUG) + + args = argparse.ArgumentParser(description="Create filesystem JSON. Example:\n" + " ./fs2json.py --exclude /boot/ --out fs.json /mnt/", + formatter_class=argparse.RawTextHelpFormatter + ) + args.add_argument("--exclude", + action="append", + metavar="path", + help="Path to exclude (relative to base path). Can be specified multiple times.") + args.add_argument("--out", + metavar="out", + nargs="?", + type=argparse.FileType("w"), + help="File to write to (defaults to stdout)", + default=sys.stdout) + args.add_argument("path", + metavar="path-or-tar", + help="Base path or tar file to include in JSON") + + args = args.parse_args() + + path = os.path.normpath(args.path) + + if os.path.isfile(path): + tar = tarfile.open(path, "r") + else: + tar = None + + if tar: + (root, total_size) = handle_tar(logger, tar) + else: + (root, total_size) = handle_dir(logger, path, args.exclude) + + if False: + # normalize the order of children, useful to debug differences between + # the tar and filesystem reader + def sort_children(children): + for c in children: + if isinstance(c[IDX_TARGET], list): + sort_children(c[IDX_TARGET]) + children.sort() + + sort_children(root) + + result = { + "fsroot": root, + "version": VERSION, + "size": total_size, + } + + logger.info("Creating json ...") + json.dump(result, args.out, check_circular=False, separators=(',', ':')) + +def handle_dir(logger, path, exclude): + path = path + "/" + exclude = exclude or [] + exclude = [os.path.join("/", os.path.normpath(p)) for p in exclude] + exclude = set(exclude) + + def onerror(oserror): + logger.warning(oserror) + + rootdepth = path.count("/") + files = os.walk(path, onerror=onerror) + prevpath = [] + + mainroot = [] + filename_to_hash = {} + total_size = 0 + rootstack = [mainroot] + + def make_node(st, name): + obj = [None] * 7 + + obj[IDX_NAME] = name + obj[IDX_SIZE] = st.st_size + obj[IDX_MTIME] = int(st.st_mtime) + obj[IDX_MODE] = int(st.st_mode) + + obj[IDX_UID] = st.st_uid + obj[IDX_GID] = st.st_gid + + nonlocal total_size + total_size += st.st_size + + # Missing: + # int(st.st_atime), + # int(st.st_ctime), + + return obj + + logger.info("Creating file tree ...") + + for f in files: + dirpath, dirnames, filenames = f + pathparts = dirpath.split("/") + pathparts = pathparts[rootdepth:] + fullpath = os.path.join("/", *pathparts) + + if fullpath in exclude: + dirnames[:] = [] + continue + + depth = 0 + for this, prev in zip(pathparts, prevpath): + if this != prev: + break + depth += 1 + + for _name in prevpath[depth:]: + rootstack.pop() + + oldroot = rootstack[-1] + + assert len(pathparts[depth:]) == 1 + openname = pathparts[-1] + + if openname == "": + root = mainroot + else: + root = [] + st = os.stat(dirpath) + rootobj = make_node(st, openname) + rootobj[IDX_TARGET] = root + oldroot.append(rootobj) + + rootstack.append(root) + + for filename in itertools.chain(filenames, dirnames): + absname = os.path.join(dirpath, filename) + + st = os.lstat(absname) + isdir = stat.S_ISDIR(st.st_mode) + islink = stat.S_ISLNK(st.st_mode) + + isfile = stat.S_ISREG(st.st_mode) + + if isdir and not islink: + continue + + obj = make_node(st, filename) + + if islink: + target = os.readlink(absname) + obj[IDX_TARGET] = target + elif isfile: + file_hash = hash_file(absname) + filename = file_hash[0:HASH_LENGTH] + ".bin" + existing = filename_to_hash.get(filename) + assert existing is None or existing == file_hash, "Collision in short hash (%s and %s)" % (existing, file_hash) + filename_to_hash[filename] = file_hash + obj[IDX_FILENAME] = filename + + while obj[-1] is None: + obj.pop() + + root.append(obj) + + prevpath = pathparts + + return (mainroot, total_size) + +def handle_tar(logger, tar): + mainroot = [] + filename_to_hash = {} + total_size = 0 + + for member in tar.getmembers(): + parts = member.name.split("/") + name = parts.pop() + + dir = mainroot + + for p in parts: + for c in dir: + if c[IDX_NAME] == p: + dir = c[IDX_TARGET] + + obj = [None] * 7 + obj[IDX_NAME] = name + obj[IDX_SIZE] = member.size + obj[IDX_MTIME] = member.mtime + obj[IDX_MODE] = member.mode + obj[IDX_UID] = member.uid + obj[IDX_GID] = member.gid + + if member.isfile() or member.islnk(): + obj[IDX_MODE] |= S_IFREG + f = tar.extractfile(member) + file_hash = hash_fileobj(f) + filename = file_hash[0:HASH_LENGTH] + ".bin" + existing = filename_to_hash.get(filename) + assert existing is None or existing == file_hash, "Collision in short hash (%s and %s)" % (existing, file_hash) + filename_to_hash[filename] = file_hash + obj[IDX_FILENAME] = filename + if member.islnk(): + # fix size for hard links + f.seek(0, os.SEEK_END) + obj[IDX_SIZE] = int(f.tell()) + elif member.isdir(): + obj[IDX_MODE] |= S_IFDIR + obj[IDX_TARGET] = [] + elif member.issym(): + obj[IDX_MODE] |= S_IFLNK + obj[IDX_TARGET] = member.linkname + else: + logger.error("Unsupported type: {} ({})".format(member.type, name)) + + total_size += obj[IDX_SIZE] + + while obj[-1] is None: + obj.pop() + + dir.append(obj) + + return mainroot, total_size + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/public/contents.json b/public/contents.json index 9306609..fd9bef6 100644 --- a/public/contents.json +++ b/public/contents.json @@ -1 +1 @@ -{"fsroot":[["daiplg",4096,1743524547,16877,0,0,[]]],"version":3,"size":4096} \ No newline at end of file +{"fsroot":[["daiplg",4096,1751908871,16895,1000,1000,[[".profile",1201,1751910405,33279,1000,1000,"e12b6fea.bin"],["README.md",585,1751910617,33279,1000,1000,"a4432dd3.bin"],["selfctl",5068875,1751907891,33279,1000,1000,"d34941b9.bin"]]]],"version":3,"size":5074757} \ No newline at end of file diff --git a/public/flat/a4432dd3.bin b/public/flat/a4432dd3.bin new file mode 100644 index 0000000..151dde1 --- /dev/null +++ b/public/flat/a4432dd3.bin @@ -0,0 +1,20 @@ +Welcome to BitBox! + +This is a custom buildroot running inside the V86 emulator. +It has been tweaked to be as fast as possible and has the following packages installed: + +busybox utilities +sudo +ash (default shell) +ascii_invaders (shooting game) +pfetch (recommended) +static-get (package manager - not recommended for larger packages) +... + +You CAN LOAD your own files into the /home/daiplg directory by using the "glowing" +folder icon in bottom left corner of the screen. + +Credits: +- V86 emulator: github.com/copy/v86 +- Busybox: busybox.net +- Buildroot: buildroot.org diff --git a/public/flat/d34941b9.bin b/public/flat/d34941b9.bin new file mode 100644 index 0000000..a02cf4a Binary files /dev/null and b/public/flat/d34941b9.bin differ diff --git a/public/flat/e12b6fea.bin b/public/flat/e12b6fea.bin new file mode 100644 index 0000000..0168270 --- /dev/null +++ b/public/flat/e12b6fea.bin @@ -0,0 +1,36 @@ +#!/bin/ash + +# check if interfaces has ip address +# filter out loopback and virtual interfaces +# if not, run udhcpc on it +_config_interfaces() { + local interfaces=$(ip -o link show | awk -F': ' '{print $2}') + local filtered_interfaces=$(echo "$interfaces" | grep -vE '^(lo|virbr|docker|sit)') + for interface in $filtered_interfaces; do + if ! ip addr show "$interface" | grep -q 'inet '; then + echo "No IP address found for interface: $interface. Running udhcpc..." + sudo udhcpc -i "$interface" 2>/dev/null || true + fi + done +} + +_config_bundled_scrips() { + sudo chown -R daiplg /home/daiplg + sudo chmod +x /home/daiplg/selfctl + sudo mv /home/daiplg/selfctl /usr/bin/selfctl +} + +_config_interfaces +_config_bundled_scrips + +# tell user to run the selfctl command to know more about the system +echo " +This is a minimal system for running the V86 emulator. +You can run the command \`selfctl\` to know more about the system. +It will show you the available commands and their usage. + +Check the README.md file in the /home/daiplg directory for more information. + +The repository for this system is available at: +https://github.com/daipham3213/bitbox.git +" diff --git a/src/main.js b/src/main.js index c31f820..022e9ec 100644 --- a/src/main.js +++ b/src/main.js @@ -145,6 +145,7 @@ const entry = () => { }, filesystem: { basefs: '/contents.json', + baseurl: '/flat/', }, autostart: true, disable_keyboard: true, diff --git a/src/terminal.js b/src/terminal.js index 067adbb..70e6cc6 100644 --- a/src/terminal.js +++ b/src/terminal.js @@ -16,9 +16,6 @@ const override = () => { foreground: '#ABB2BF', // Default text color cursor: '#ABB2BF', // Cursor color selectionBackground: '#61AFEF', // Highlight selection - blue: '#61AFEF', // For prompts if needed - green: '#98C379', // For prompts if needed - yellow: '#E5C07B', // For commands if needed }, }; super({ ...defaultOptions, ...options });