Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -809,4 +809,6 @@ FodyWeavers.xsd

# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,eclipse,intellij,webstorm,node
.idea
*.iso
*.iso

**/rootfs/*
272 changes: 272 additions & 0 deletions etc/fs2json.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion public/contents.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"fsroot":[["daiplg",4096,1743524547,16877,0,0,[]]],"version":3,"size":4096}
{"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}
20 changes: 20 additions & 0 deletions public/flat/a4432dd3.bin
Original file line number Diff line number Diff line change
@@ -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
Binary file added public/flat/d34941b9.bin
Binary file not shown.
36 changes: 36 additions & 0 deletions public/flat/e12b6fea.bin
Original file line number Diff line number Diff line change
@@ -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
"
1 change: 1 addition & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ const entry = () => {
},
filesystem: {
basefs: '/contents.json',
baseurl: '/flat/',
},
autostart: true,
disable_keyboard: true,
Expand Down
3 changes: 0 additions & 3 deletions src/terminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down