diff --git a/.gitignore b/.gitignore
index 4921e39..c865263 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,9 @@ CTestTestfile.cmake
compile_commands.json
_deps/
+# Generated header (from applet_config.hpp.in via cmake configure_file)
+include/cfbox/applet_config.hpp
+
# Compiled objects
*.o
*.obj
@@ -61,3 +64,6 @@ third_party/linux/scripts/mod/
third_party/linux/tools/objtool/objtool
third_party/linux/source
third_party/linux/*.cpio
+
+# Demo source video (large, not tracked)
+*.mp4
diff --git a/.gitmodules b/.gitmodules
index bcd9634..d6eb9e7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "third_party/linux"]
path = third_party/linux
url = https://github.com/torvalds/linux.git
+[submodule "projects/imx-forge-demo"]
+ path = projects/imx-forge-demo
+ url = https://github.com/Awesome-Embedded-Learning-Studio/imx-forge
diff --git a/README.md b/README.md
index 691fb4e..c616aa4 100644
--- a/README.md
+++ b/README.md
@@ -2,45 +2,88 @@
**[中文](README.md)** | [English](README.en.md)
-用现代 C++23 实现的极简 BusyBox 替代品。
+用现代 C++23 实现的 BusyBox 替代品 —— 单二进制、123 applet、399 测试。可在 i.MX6ULL 上作为 PID 1 运行,替代 BusyBox。
+
+
+
+
+
+> 上图:cfbox 作为 PID 1 启动 imx-forge rootfs —— 跑 `rcS`(mount/mdev),打印 `Please press Enter to activate this console.`(askfirst),回车进入 cfbox `sh`。
[](https://github.com/Awesome-Embedded-Learning-Studio/CFBox/actions/workflows/ci.yml)
[](https://opensource.org/licenses/MIT)
[](https://en.cppreference.com/w/cpp/23)
[](https://cmake.org/)
-[](tests/)
-[](src/applets/)
+[](tests/)
+[](src/applets/)
+[](cmake/toolchain/Toolchain-armhf.cmake)
+
+## 这是什么?
+
+CFBox 是一个单一可执行文件的 Unix 工具集,通过符号链接分发。**123 个 applet** 已实现并通过测试,CI 流水线覆盖原生构建、交叉编译(armhf/aarch64)、QEMU 用户/系统模式测试。支持 CMake 配置化构建(per-applet 开关)、GNU 风格长选项、彩色帮助输出。并且希望在后续,可以慢慢追赶甚至超越常见的 coreutils。
+
+**设计理念:** 简洁优先 — 现代 C++(`std::expected`,无异常/无 RTTI)— 嵌入式友好(交叉编译、静态链接、当 PID 1 init)。
+
+## 实测:i.MX6ULL
+
+cfbox 在 NXP i.MX6ULL(armhf,Cortex-A7)上**替代 BusyBox**,作为 [imx-forge](projects/imx-forge-demo) rootfs 的 PID 1 + 工具集,撑起完整启动闭环:
+
+| 启动阶段 | cfbox 承担 |
+|---------|-----------|
+| PID 1 | `init` —— 解析 busybox 格式 `/etc/inittab`,支持 `sysinit`/`askfirst`/`respawn`/`ctrlaltdel`/`shutdown` |
+| rcS | `mount -a` / `mount -t devpts devpts /dev/pts` / `mdev -s`(冷启动扫 /sys 建 /dev 节点) |
+| console | `askfirst` → `Please press Enter to activate this console.` → 回车 → cfbox `sh` |
+| 关机 | `umount -a -r` / `swapoff -a` / `reboot` |
+
+
+
+
-## 概述
+实测:`/proc/cpuinfo` 显示 `ARMv7 Processor rev 5 (v7l)`(i.MX6ULL),cfbox `sh` 交互,`ls`/`cat`/`df`/`ps`/`uname`/`free` 等正常 dispatch。
-CFBox 是一个单一可执行文件的 Unix 工具集,通过符号链接分发。115 个 applet 已实现并通过测试,CI 流水线覆盖原生构建、交叉编译、QEMU 用户/系统模式测试。支持 CMake 配置化构建(per-applet 开关)、GNU 风格长选项、彩色帮助输出。
+
+
+
-**设计理念:** 简洁优先 — 现代C++(`std::expected`) — 嵌入式友好(交叉编译、静态链接)
+> armhf 静态构建(自包含,直接当 PID 1 跑):
+> ```bash
+> cmake -B build-armhf-static \
+> -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain/Toolchain-armhf.cmake \
+> -DCMAKE_BUILD_TYPE=Release \
+> -DCFBOX_OPTIMIZE_FOR_SIZE=ON \
+> -DCFBOX_STATIC_LINK=ON
+> cmake --build build-armhf-static -j$(nproc) # 产物 ~1.2 MB
+> ```
## 体积对比
+我们简单地统计了一下,以下表格由项目内的 `scripts/gen_size_table.sh` 自动生成。
+
| 项目 | 语言 | 体积 | Applets | 体积/Applet |
|------|------|------|---------|-------------|
-| **CFBox (size-opt)** | **C++23** | **406 KB** | **115** | **~3.5 KB** |
+| **CFBox (size-opt)** | **C++23** | **418 KB** | **123** | **~3.4 KB** |
+| CFBox (armhf static) | C++23 | ~1.2 MB | 123 | — |
| Toybox | C | ~500 KB | 238 | ~2.1 KB |
| BusyBox (full) | C | ~1.7 MB | 274 | ~9 KB |
| uutils/coreutils | Rust | ~11 MB | ~100 | ~110 KB |
-> CFBox 比 BusyBox 小 **3-4x**,在相似体积下提供了完整 awk 解释器、归档工具集(tar/cpio/ar/unzip/gzip)、diff/patch(Myers O(ND) 算法)、进程工具集(ps/top/pstree/pgrep/pmap)以及内置 TUI 框架。
+> CFBox 比 BusyBox 小 **3-4x**,在相似体积下提供了完整 awk 解释器、归档工具集(tar/cpio/ar/unzip/gzip)、diff/patch(Myers O(ND) 算法)、进程工具集(ps/top/pstree/pgrep/pmap)以及内置 TUI 框架。
## 性能
+我们仍然在尝试使用 C++ 逼近主流 box 工具(BusyBox/Toybox)的性能。
+
| 操作 | 数据规模 | 耗时 |
|------|---------|------|
| grep -c | 10 MB | 54 ms |
| cat | 10 MB | 63 ms |
| wc | 10 MB | 17 ms |
| sort | 100K 行 | 32 ms |
-| diff | 100K 行(相似文件) | 79 ms |
+| diff | 100K 行(相似文件) | 79 ms |
-- grep/cat/wc 均为流式处理,读取 `/dev/urandom` 不会内存爆炸
-- diff 使用 Myers O(ND) 算法,sort 预计算排序 key 避免重复分配
-- 零外部依赖:手写轻量 deflate/inflate 替代 zlib
+- grep/cat/wc 均为流式处理,读取 `/dev/urandom` 不会内存爆炸
+- diff 使用 Myers O(ND) 算法,sort 预计算排序 key 避免重复分配
+- 零外部依赖:手写轻量 deflate/inflate 替代 zlib
## 快速开始
@@ -50,7 +93,7 @@ cmake -B build
cmake --build build
# 测试
-ctest --test-dir build --output-on-failure # 379 个 GTest 单元测试
+ctest --test-dir build --output-on-failure # 399 个 GTest 单元测试
bash tests/integration/run_all.sh # 54 套集成测试脚本
# 通过子命令运行
@@ -61,43 +104,47 @@ bash tests/integration/run_all.sh # 54 套集成测试脚本
echo "Hello, World!" # 通过符号链接调用 cfbox
```
-## 支持的命令(115 个)
+## 支持的命令(123 个)
-### 文本处理(31 个)
+### 文本处理(31 个)
`echo`, `printf`, `cat`, `head`, `tail`, `wc`, `sort`, `uniq`, `grep`, `sed`, `fold`, `expand`, `cut`, `paste`, `nl`, `comm`, `tr`, `tac`, `rev`, `shuf`, `factor`, `od`, `split`, `seq`, `tsort`, `expr`, `awk`, `diff`, `patch`, `cmp`, `ed`
-### 文件操作(22 个)
+### 文件操作(22 个)
`mkdir`, `rm`, `cp`, `mv`, `ls`, `find`, `ln`, `touch`, `stat`, `install`, `mktemp`, `truncate`, `du`, `df`, `readlink`, `realpath`, `rmdir`, `link`, `unlink`, `chmod`, `chown`, `chgrp`
-### 归档与压缩(6 个)
+### 归档与压缩(6 个)
-`tar`(ustar 格式), `cpio`(newc 格式), `ar`(静态库), `unzip`, `gzip`, `gunzip`
+`tar`(ustar 格式), `cpio`(newc 格式), `ar`(静态库), `unzip`, `gzip`, `gunzip`
-### Shell 与脚本(2 个)
+### Shell 与脚本(2 个)
-`sh`(POSIX shell:管道、重定向、变量展开、命令替换、if/while/for、15 个内置命令), `xargs`
+`sh`(POSIX shell:管道、重定向、变量展开、命令替换、if/while/for、15 个内置命令), `xargs`
-### 系统信息(20 个)
+### 系统信息(21 个)
-`pwd`, `basename`, `dirname`, `uname`, `hostname`, `whoami`, `id`, `tty`, `date`, `nproc`, `logname`, `hostid`, `printenv`, `env`, `uptime`, `free`, `cal`, `dmesg`, `who`, `test`
+`pwd`, `basename`, `dirname`, `uname`, `hostname`, `whoami`, `id`, `tty`, `date`, `nproc`, `logname`, `hostid`, `printenv`, `env`, `uptime`, `free`, `cal`, `dmesg`, `who`, `test`, `[`
-### 进程管理(15 个)
+### 进程管理(16 个)
`ps`, `top`, `kill`, `pgrep`/`pkill`, `pidof`, `pstree`, `pmap`, `fuser`, `pwdx`, `sysctl`, `iostat`, `watch`, `nice`, `renice`, `timeout`
-### 其他(19 个)
+### 文件系统与系统启动(12 个)
+
+`mount`(-a/-t/-o,读 fstab), `umount`(-a/-r/-f), `mdev`(-s 冷启动扫描), `mountpoint`, `init`(PID 1,解析 inittab + askfirst), `reboot`, `poweroff`, `swapoff`, `sync`, `mkfifo`, `mknod`, `clear`
+
+### 其他(13 个)
-`true`, `false`, `yes`, `sleep`, `usleep`, `sync`, `nohup`, `cksum`, `md5sum`, `sum`, `hexdump`, `more`, `tee`, `init`(PID 1 initramfs init 系统), `mkfifo`, `mknod`, `clear`, `which`, `mountpoint`
+`true`, `false`, `yes`, `sleep`, `usleep`, `nohup`, `cksum`, `md5sum`, `sum`, `hexdump`, `more`, `tee`, `which`
> 所有 applet 均支持 `--help` / `--version`
## 系统要求
-- **编译器:** GCC 13+ / Clang 17+(需 C++23 支持)
-- **CMake:** 3.26+
-- **平台:** Linux(x86_64 / aarch64)
+- **编译器:** GCC 13+ / Clang 17+(需 C++23 支持)
+- **CMake:** 3.26+
+- **平台:** Linux(x86_64 / aarch64 / **armhf**,支持静态链接当 PID 1 init)
## 文档
@@ -110,53 +157,23 @@ echo "Hello, World!" # 通过符号链接调用 cfbox
| [持续集成](document/ci.md) | CI 流水线阶段说明 |
| [贡献指南](CONTRIBUTING.md) | 构建、测试、编码规范、提交方式 |
-## 项目结构
+## 下一步计划
-```
-cfbox/
-├── CMakeLists.txt
-├── cmake/
-│ ├── Config.cmake # Per-applet 配置(CFBOX_ENABLE_xxx 选项)
-│ ├── compile/CompilerFlag.cmake # 编译器警告与优化标志
-│ ├── third_party/CPM.cmake # CPM 依赖管理(仅 GTest)
-│ └── toolchain/ # 交叉编译工具链
-├── include/cfbox/
-│ ├── applet.hpp / applets.hpp # 注册表与分发
-│ ├── args.hpp # 短选项 + 长选项参数解析器
-│ ├── error.hpp # std::expected 错误处理 + CFBOX_TRY
-│ ├── io.hpp # 流式 I/O(for_each_line、read_all、write_all)
-│ ├── stream.hpp # 逐行处理管线、LineProcessor
-│ ├── deflate.hpp / inflate.hpp # 手写轻量 DEFLATE(零外部依赖)
-│ ├── compress.hpp # gzip 封装
-│ ├── utf8.hpp # Unicode 宽度/计数(constexpr + static_assert)
-│ ├── term.hpp # ANSI 彩色输出(NO_COLOR 支持)
-│ ├── terminal.hpp # 终端控制(RawMode RAII、光标、双缓冲)
-│ ├── tui.hpp # TUI 框架(ScreenBuffer、Key、TuiApp)
-│ ├── proc.hpp # /proc 解析器(进程、内存、CPU、磁盘)
-│ ├── regex.hpp # POSIX regex RAII(scoped_regex)
-│ └── ... # help.hpp, fs_util.hpp, escape.hpp, checksum.hpp
-├── src/
-│ ├── main.cpp # 分发入口
-│ └── applets/ # 115 个命令实现
-├── tests/
-│ ├── unit/ # GTest 单元测试(379 个用例)
-│ └── integration/ # Shell 集成测试(54 个脚本)
-└── scripts/ # 构建、测试、安装脚本
-```
+当前版本 v0.2.0+。**Phase 1.5 代码质量审查 + L2 rootfs 启动骨架已完成**(cfbox 已上 i.MX6ULL),进入 Phase 2 核心命令深化:
-## 下一步计划
+### 已完成:L2 rootfs 启动骨架(✅ 端到端验证)
-当前版本 v0.2.0(Phase 1 Wave 1 + Phase 1.5 代码质量审查已完成),进入 Phase 2 核心命令深化:
+补齐 cfbox 替代 BusyBox 当 PID 1 的全部缺口:`init`(askfirst)、`mount`、`mdev`、`umount`、`swapoff`、`reboot`/`poweroff` —— 在 i.MX6ULL + imx-forge rootfs 实测启动到 console。
-### Phase 2:核心命令深化(进行中)
+### Phase 2:核心命令深化(进行中)
-将现有命令功能深度从 ~30% 提升到 ~70%,按运维频率分批推进:
+将现有命令功能深度提升,按运维频率分批推进:
| 批次 | 命令 | 关键补充 |
|------|------|---------|
| 第一批 | `tail`、`cp`、`test`、`ls` | tail -f、cp -a、全面 POSIX test、ls -R/--color |
| 第二批 | `grep`、`tar`、`sed`、`sort` | grep -A/-B/-C、tar -z/-v、sed -i、sort -k |
-| 第三批 | `find`、`sh`、`ps`、`df`、`du` | find 布尔表达式、sh case/heredoc/函数 |
+| 第三批 | `find`、`sh`、`ps`、`df`、`du` | find 布尔表达式、sh case/heredoc/函数/行编辑 |
> 详细路线图见 [document/todo/README.md](document/todo/README.md)。
diff --git a/cmake/Config.cmake b/cmake/Config.cmake
index 317b50e..5676fb1 100644
--- a/cmake/Config.cmake
+++ b/cmake/Config.cmake
@@ -27,7 +27,8 @@ set(CFBOX_APPLETS
pwdx pstree pmap fuser iostat
watch top
dmesg hexdump more rev cal renice
- clear which mountpoint chmod chown chgrp
+ clear which mount mountpoint mdev chmod chown chgrp
+ umount swapoff reboot
)
foreach(applet IN LISTS CFBOX_APPLETS)
diff --git a/docs/screenshots/cat_cpuinfo.png b/docs/screenshots/cat_cpuinfo.png
new file mode 100644
index 0000000..94b4adb
Binary files /dev/null and b/docs/screenshots/cat_cpuinfo.png differ
diff --git a/docs/screenshots/cfbox-demo.gif b/docs/screenshots/cfbox-demo.gif
new file mode 100644
index 0000000..c33cdf7
Binary files /dev/null and b/docs/screenshots/cfbox-demo.gif differ
diff --git a/docs/screenshots/enter_shell.png b/docs/screenshots/enter_shell.png
new file mode 100644
index 0000000..2505e73
Binary files /dev/null and b/docs/screenshots/enter_shell.png differ
diff --git a/include/cfbox/applet_config.hpp b/include/cfbox/applet_config.hpp
deleted file mode 100644
index 3febc97..0000000
--- a/include/cfbox/applet_config.hpp
+++ /dev/null
@@ -1,121 +0,0 @@
-// Generated by cmake/Config.cmake — do not edit
-#pragma once
-
-#define CFBOX_VERSION_STRING ""
-
-// Per-applet enable flags (0 or 1)
-#define CFBOX_ENABLE_ECHO 1
-#define CFBOX_ENABLE_PRINTF 1
-#define CFBOX_ENABLE_CAT 1
-#define CFBOX_ENABLE_HEAD 1
-#define CFBOX_ENABLE_TAIL 1
-#define CFBOX_ENABLE_WC 1
-#define CFBOX_ENABLE_SORT 1
-#define CFBOX_ENABLE_UNIQ 1
-#define CFBOX_ENABLE_MKDIR 1
-#define CFBOX_ENABLE_RM 1
-#define CFBOX_ENABLE_CP 1
-#define CFBOX_ENABLE_MV 1
-#define CFBOX_ENABLE_LS 1
-#define CFBOX_ENABLE_GREP 1
-#define CFBOX_ENABLE_FIND 1
-#define CFBOX_ENABLE_SED 1
-#define CFBOX_ENABLE_INIT 1
-#define CFBOX_ENABLE_TRUE 1
-#define CFBOX_ENABLE_FALSE 1
-#define CFBOX_ENABLE_YES 1
-#define CFBOX_ENABLE_PWD 1
-#define CFBOX_ENABLE_BASENAME 1
-#define CFBOX_ENABLE_DIRNAME 1
-#define CFBOX_ENABLE_UNAME 1
-#define CFBOX_ENABLE_NPROC 1
-#define CFBOX_ENABLE_LINK 1
-#define CFBOX_ENABLE_HOSTNAME 1
-#define CFBOX_ENABLE_LOGNAME 1
-#define CFBOX_ENABLE_WHOAMI 1
-#define CFBOX_ENABLE_TTY 1
-#define CFBOX_ENABLE_SLEEP 1
-#define CFBOX_ENABLE_ID 1
-#define CFBOX_ENABLE_TEST 1
-#define CFBOX_ENABLE_SH 1
-#define CFBOX_ENABLE_PRINTENV 1
-#define CFBOX_ENABLE_HOSTID 1
-#define CFBOX_ENABLE_SYNC 1
-#define CFBOX_ENABLE_USLEEP 1
-#define CFBOX_ENABLE_RMDIR 1
-#define CFBOX_ENABLE_UNLINK 1
-#define CFBOX_ENABLE_WHO 1
-#define CFBOX_ENABLE_ENV 1
-#define CFBOX_ENABLE_READLINK 1
-#define CFBOX_ENABLE_REALPATH 1
-#define CFBOX_ENABLE_TOUCH 1
-#define CFBOX_ENABLE_TRUNCATE 1
-#define CFBOX_ENABLE_STAT 1
-#define CFBOX_ENABLE_INSTALL 1
-#define CFBOX_ENABLE_MKTEMP 1
-#define CFBOX_ENABLE_LN 1
-#define CFBOX_ENABLE_MKFIFO 1
-#define CFBOX_ENABLE_MKNOD 1
-#define CFBOX_ENABLE_DU 1
-#define CFBOX_ENABLE_SEQ 1
-#define CFBOX_ENABLE_TEE 1
-#define CFBOX_ENABLE_TAC 1
-#define CFBOX_ENABLE_FOLD 1
-#define CFBOX_ENABLE_EXPAND 1
-#define CFBOX_ENABLE_CUT 1
-#define CFBOX_ENABLE_PASTE 1
-#define CFBOX_ENABLE_NL 1
-#define CFBOX_ENABLE_COMM 1
-#define CFBOX_ENABLE_TR 1
-#define CFBOX_ENABLE_CKSUM 1
-#define CFBOX_ENABLE_MD5SUM 1
-#define CFBOX_ENABLE_SUM 1
-#define CFBOX_ENABLE_DATE 1
-#define CFBOX_ENABLE_OD 1
-#define CFBOX_ENABLE_SPLIT 1
-#define CFBOX_ENABLE_SHUF 1
-#define CFBOX_ENABLE_FACTOR 1
-#define CFBOX_ENABLE_TIMEOUT 1
-#define CFBOX_ENABLE_NICE 1
-#define CFBOX_ENABLE_NOHUP 1
-#define CFBOX_ENABLE_DF 1
-#define CFBOX_ENABLE_EXPR 1
-#define CFBOX_ENABLE_TSORT 1
-#define CFBOX_ENABLE_XARGS 1
-#define CFBOX_ENABLE_GZIP 1
-#define CFBOX_ENABLE_GUNZIP 1
-#define CFBOX_ENABLE_DIFF 1
-#define CFBOX_ENABLE_CMP 1
-#define CFBOX_ENABLE_PATCH 1
-#define CFBOX_ENABLE_ED 1
-#define CFBOX_ENABLE_TAR 1
-#define CFBOX_ENABLE_CPIO 1
-#define CFBOX_ENABLE_AR 1
-#define CFBOX_ENABLE_UNZIP 1
-#define CFBOX_ENABLE_AWK 1
-#define CFBOX_ENABLE_FREE 1
-#define CFBOX_ENABLE_UPTIME 1
-#define CFBOX_ENABLE_KILL 1
-#define CFBOX_ENABLE_PIDOF 1
-#define CFBOX_ENABLE_PS 1
-#define CFBOX_ENABLE_PGREP 1
-#define CFBOX_ENABLE_SYSCTL 1
-#define CFBOX_ENABLE_PWDX 1
-#define CFBOX_ENABLE_PSTREE 1
-#define CFBOX_ENABLE_PMAP 1
-#define CFBOX_ENABLE_FUSER 1
-#define CFBOX_ENABLE_IOSTAT 1
-#define CFBOX_ENABLE_WATCH 1
-#define CFBOX_ENABLE_TOP 1
-#define CFBOX_ENABLE_DMESG 1
-#define CFBOX_ENABLE_HEXDUMP 1
-#define CFBOX_ENABLE_MORE 1
-#define CFBOX_ENABLE_REV 1
-#define CFBOX_ENABLE_CAL 1
-#define CFBOX_ENABLE_RENICE 1
-#define CFBOX_ENABLE_CLEAR 1
-#define CFBOX_ENABLE_WHICH 1
-#define CFBOX_ENABLE_MOUNTPOINT 1
-#define CFBOX_ENABLE_CHMOD 1
-#define CFBOX_ENABLE_CHOWN 1
-#define CFBOX_ENABLE_CHGRP 1
diff --git a/include/cfbox/applet_config.hpp.in b/include/cfbox/applet_config.hpp.in
index 3dc8937..7c12eed 100644
--- a/include/cfbox/applet_config.hpp.in
+++ b/include/cfbox/applet_config.hpp.in
@@ -115,7 +115,12 @@
#cmakedefine01 CFBOX_ENABLE_RENICE
#cmakedefine01 CFBOX_ENABLE_CLEAR
#cmakedefine01 CFBOX_ENABLE_WHICH
+#cmakedefine01 CFBOX_ENABLE_MOUNT
+#cmakedefine01 CFBOX_ENABLE_MDEV
#cmakedefine01 CFBOX_ENABLE_MOUNTPOINT
#cmakedefine01 CFBOX_ENABLE_CHMOD
#cmakedefine01 CFBOX_ENABLE_CHOWN
#cmakedefine01 CFBOX_ENABLE_CHGRP
+#cmakedefine01 CFBOX_ENABLE_UMOUNT
+#cmakedefine01 CFBOX_ENABLE_SWAPOFF
+#cmakedefine01 CFBOX_ENABLE_REBOOT
diff --git a/include/cfbox/applets.hpp b/include/cfbox/applets.hpp
index 9230b0a..21f62e1 100644
--- a/include/cfbox/applets.hpp
+++ b/include/cfbox/applets.hpp
@@ -337,6 +337,12 @@ extern auto clear_main(int argc, char* argv[]) -> int;
#if CFBOX_ENABLE_WHICH
extern auto which_main(int argc, char* argv[]) -> int;
#endif
+#if CFBOX_ENABLE_MOUNT
+extern auto mount_main(int argc, char* argv[]) -> int;
+#endif
+#if CFBOX_ENABLE_MDEV
+extern auto mdev_main(int argc, char* argv[]) -> int;
+#endif
#if CFBOX_ENABLE_MOUNTPOINT
extern auto mountpoint_main(int argc, char* argv[]) -> int;
#endif
@@ -349,135 +355,145 @@ extern auto chown_main(int argc, char* argv[]) -> int;
#if CFBOX_ENABLE_CHGRP
extern auto chgrp_main(int argc, char* argv[]) -> int;
#endif
+#if CFBOX_ENABLE_UMOUNT
+extern auto umount_main(int argc, char* argv[]) -> int;
+#endif
+#if CFBOX_ENABLE_SWAPOFF
+extern auto swapoff_main(int argc, char* argv[]) -> int;
+#endif
+#if CFBOX_ENABLE_REBOOT
+extern auto reboot_main(int argc, char* argv[]) -> int;
+extern auto poweroff_main(int argc, char* argv[]) -> int;
+#endif
// registry — one line per applet, conditionally compiled
constexpr auto APPLET_REGISTRY = std::to_array({
#if CFBOX_ENABLE_ECHO
- {"echo", echo_main, "display a line of text"},
+ {"echo", echo_main, "display a line of text"},
#endif
#if CFBOX_ENABLE_PRINTF
{"printf", printf_main, "format and print data"},
#endif
#if CFBOX_ENABLE_CAT
- {"cat", cat_main, "concatenate files and print"},
+ {"cat", cat_main, "concatenate files and print"},
#endif
#if CFBOX_ENABLE_HEAD
- {"head", head_main, "output the first part of files"},
+ {"head", head_main, "output the first part of files"},
#endif
#if CFBOX_ENABLE_TAIL
- {"tail", tail_main, "output the last part of files"},
+ {"tail", tail_main, "output the last part of files"},
#endif
#if CFBOX_ENABLE_WC
- {"wc", wc_main, "print newline, word, and byte counts"},
+ {"wc", wc_main, "print newline, word, and byte counts"},
#endif
#if CFBOX_ENABLE_SORT
- {"sort", sort_main, "sort lines of text"},
+ {"sort", sort_main, "sort lines of text"},
#endif
#if CFBOX_ENABLE_UNIQ
- {"uniq", uniq_main, "report or omit repeated lines"},
+ {"uniq", uniq_main, "report or omit repeated lines"},
#endif
#if CFBOX_ENABLE_MKDIR
- {"mkdir", mkdir_main, "create directories"},
+ {"mkdir", mkdir_main, "create directories"},
#endif
#if CFBOX_ENABLE_RM
- {"rm", rm_main, "remove files or directories"},
+ {"rm", rm_main, "remove files or directories"},
#endif
#if CFBOX_ENABLE_CP
- {"cp", cp_main, "copy files and directories"},
+ {"cp", cp_main, "copy files and directories"},
#endif
#if CFBOX_ENABLE_MV
- {"mv", mv_main, "move or rename files"},
+ {"mv", mv_main, "move or rename files"},
#endif
#if CFBOX_ENABLE_LS
- {"ls", ls_main, "list directory contents"},
+ {"ls", ls_main, "list directory contents"},
#endif
#if CFBOX_ENABLE_GREP
- {"grep", grep_main, "search patterns in text"},
+ {"grep", grep_main, "search patterns in text"},
#endif
#if CFBOX_ENABLE_FIND
- {"find", find_main, "search for files in directory hierarchy"},
+ {"find", find_main, "search for files in directory hierarchy"},
#endif
#if CFBOX_ENABLE_SED
- {"sed", sed_main, "stream editor for filtering and transforming text"},
+ {"sed", sed_main, "stream editor for filtering and transforming text"},
#endif
#if CFBOX_ENABLE_INIT
- {"init", init_main, "system init for boot testing (PID 1)"},
+ {"init", init_main, "system init for boot testing (PID 1)"},
#endif
#if CFBOX_ENABLE_TRUE
- {"true", true_main, "do nothing, exit with status 0"},
+ {"true", true_main, "do nothing, exit with status 0"},
#endif
#if CFBOX_ENABLE_FALSE
- {"false", false_main, "do nothing, exit with status 1"},
+ {"false", false_main, "do nothing, exit with status 1"},
#endif
#if CFBOX_ENABLE_YES
- {"yes", yes_main, "output a string repeatedly until killed"},
+ {"yes", yes_main, "output a string repeatedly until killed"},
#endif
#if CFBOX_ENABLE_PWD
- {"pwd", pwd_main, "print working directory"},
+ {"pwd", pwd_main, "print working directory"},
#endif
#if CFBOX_ENABLE_BASENAME
{"basename", basename_main, "strip directory and suffix from file names"},
#endif
#if CFBOX_ENABLE_DIRNAME
- {"dirname", dirname_main, "strip last component from file name"},
+ {"dirname", dirname_main, "strip last component from file name"},
#endif
#if CFBOX_ENABLE_UNAME
- {"uname", uname_main, "print system information"},
+ {"uname", uname_main, "print system information"},
#endif
#if CFBOX_ENABLE_NPROC
- {"nproc", nproc_main, "print number of available processors"},
+ {"nproc", nproc_main, "print number of available processors"},
#endif
#if CFBOX_ENABLE_LINK
- {"link", link_main, "create a hard link"},
+ {"link", link_main, "create a hard link"},
#endif
#if CFBOX_ENABLE_HOSTNAME
{"hostname", hostname_main, "show or set the system host name"},
#endif
#if CFBOX_ENABLE_LOGNAME
- {"logname", logname_main, "print the user's login name"},
+ {"logname", logname_main, "print the user's login name"},
#endif
#if CFBOX_ENABLE_WHOAMI
{"whoami", whoami_main, "print effective user ID"},
#endif
#if CFBOX_ENABLE_TTY
- {"tty", tty_main, "print the file name of the terminal connected to stdin"},
+ {"tty", tty_main, "print the file name of the terminal connected to stdin"},
#endif
#if CFBOX_ENABLE_SLEEP
- {"sleep", sleep_main, "delay for a specified amount of time"},
+ {"sleep", sleep_main, "delay for a specified amount of time"},
#endif
#if CFBOX_ENABLE_ID
- {"id", id_main, "print real and effective user and group IDs"},
+ {"id", id_main, "print real and effective user and group IDs"},
#endif
#if CFBOX_ENABLE_TEST
- {"test", test_main, "evaluate conditional expression"},
- {"[", test_main, "evaluate conditional expression"},
+ {"test", test_main, "evaluate conditional expression"},
+ {"[", test_main, "evaluate conditional expression"},
#endif
#if CFBOX_ENABLE_SH
- {"sh", sh_main, "POSIX shell command interpreter"},
+ {"sh", sh_main, "POSIX shell command interpreter"},
#endif
#if CFBOX_ENABLE_PRINTENV
{"printenv", printenv_main, "print all or part of environment"},
#endif
#if CFBOX_ENABLE_HOSTID
- {"hostid", hostid_main, "print the numeric identifier for the current host"},
+ {"hostid", hostid_main, "print the numeric identifier for the current host"},
#endif
#if CFBOX_ENABLE_SYNC
- {"sync", sync_main, "synchronize cached writes to persistent storage"},
+ {"sync", sync_main, "synchronize cached writes to persistent storage"},
#endif
#if CFBOX_ENABLE_USLEEP
- {"usleep", usleep_main, "sleep for a specified number of microseconds"},
+ {"usleep", usleep_main, "sleep for a specified number of microseconds"},
#endif
#if CFBOX_ENABLE_RMDIR
- {"rmdir", rmdir_main, "remove empty directories"},
+ {"rmdir", rmdir_main, "remove empty directories"},
#endif
#if CFBOX_ENABLE_UNLINK
- {"unlink", unlink_main, "call the unlink function to remove a file"},
+ {"unlink", unlink_main, "call the unlink function to remove a file"},
#endif
#if CFBOX_ENABLE_WHO
- {"who", who_main, "show who is logged on"},
+ {"who", who_main, "show who is logged on"},
#endif
#if CFBOX_ENABLE_ENV
- {"env", env_main, "run a program in a modified environment"},
+ {"env", env_main, "run a program in a modified environment"},
#endif
#if CFBOX_ENABLE_READLINK
{"readlink", readlink_main, "print the value of a symbolic link"},
@@ -486,217 +502,233 @@ constexpr auto APPLET_REGISTRY = std::to_array({
{"realpath", realpath_main, "print the resolved path"},
#endif
#if CFBOX_ENABLE_TOUCH
- {"touch", touch_main, "change file timestamps"},
+ {"touch", touch_main, "change file timestamps"},
#endif
#if CFBOX_ENABLE_TRUNCATE
{"truncate", truncate_main, "shrink or extend the size of a file"},
#endif
#if CFBOX_ENABLE_STAT
- {"stat", stat_main, "display file or file system status"},
+ {"stat", stat_main, "display file or file system status"},
#endif
#if CFBOX_ENABLE_INSTALL
- {"install", install_main, "copy files and set attributes"},
+ {"install", install_main, "copy files and set attributes"},
#endif
#if CFBOX_ENABLE_MKTEMP
- {"mktemp", mktemp_main, "create a temporary file or directory"},
+ {"mktemp", mktemp_main, "create a temporary file or directory"},
#endif
#if CFBOX_ENABLE_LN
- {"ln", ln_main, "make links between files"},
+ {"ln", ln_main, "make links between files"},
#endif
#if CFBOX_ENABLE_MKFIFO
- {"mkfifo", mkfifo_main, "make FIFOs (named pipes)"},
+ {"mkfifo", mkfifo_main, "make FIFOs (named pipes)"},
#endif
#if CFBOX_ENABLE_MKNOD
- {"mknod", mknod_main, "make block or character special files"},
+ {"mknod", mknod_main, "make block or character special files"},
#endif
#if CFBOX_ENABLE_DU
- {"du", du_main, "estimate file space usage"},
+ {"du", du_main, "estimate file space usage"},
#endif
#if CFBOX_ENABLE_SEQ
- {"seq", seq_main, "print a sequence of numbers"},
+ {"seq", seq_main, "print a sequence of numbers"},
#endif
#if CFBOX_ENABLE_TEE
- {"tee", tee_main, "read from stdin and write to stdout and files"},
+ {"tee", tee_main, "read from stdin and write to stdout and files"},
#endif
#if CFBOX_ENABLE_TAC
- {"tac", tac_main, "concatenate and print files in reverse"},
+ {"tac", tac_main, "concatenate and print files in reverse"},
#endif
#if CFBOX_ENABLE_FOLD
- {"fold", fold_main, "wrap each input line to fit in specified width"},
+ {"fold", fold_main, "wrap each input line to fit in specified width"},
#endif
#if CFBOX_ENABLE_EXPAND
- {"expand", expand_main, "convert tabs to spaces"},
+ {"expand", expand_main, "convert tabs to spaces"},
#endif
#if CFBOX_ENABLE_CUT
- {"cut", cut_main, "remove sections from each line of files"},
+ {"cut", cut_main, "remove sections from each line of files"},
#endif
#if CFBOX_ENABLE_PASTE
- {"paste", paste_main, "merge lines of files"},
+ {"paste", paste_main, "merge lines of files"},
#endif
#if CFBOX_ENABLE_NL
- {"nl", nl_main, "number lines of files"},
+ {"nl", nl_main, "number lines of files"},
#endif
#if CFBOX_ENABLE_COMM
- {"comm", comm_main, "compare two sorted files line by line"},
+ {"comm", comm_main, "compare two sorted files line by line"},
#endif
#if CFBOX_ENABLE_TR
- {"tr", tr_main, "translate, squeeze, and/or delete characters"},
+ {"tr", tr_main, "translate, squeeze, and/or delete characters"},
#endif
#if CFBOX_ENABLE_CKSUM
- {"cksum", cksum_main, "checksum and count the bytes in a file"},
+ {"cksum", cksum_main, "checksum and count the bytes in a file"},
#endif
#if CFBOX_ENABLE_MD5SUM
- {"md5sum", md5sum_main, "compute and check MD5 message digest"},
+ {"md5sum", md5sum_main, "compute and check MD5 message digest"},
#endif
#if CFBOX_ENABLE_SUM
- {"sum", sum_main, "checksum and count the blocks in a file"},
+ {"sum", sum_main, "checksum and count the blocks in a file"},
#endif
#if CFBOX_ENABLE_DATE
- {"date", date_main, "print or set the system date and time"},
+ {"date", date_main, "print or set the system date and time"},
#endif
#if CFBOX_ENABLE_OD
- {"od", od_main, "dump files in octal and other formats"},
+ {"od", od_main, "dump files in octal and other formats"},
#endif
#if CFBOX_ENABLE_SPLIT
- {"split", split_main, "split a file into pieces"},
+ {"split", split_main, "split a file into pieces"},
#endif
#if CFBOX_ENABLE_SHUF
- {"shuf", shuf_main, "generate random permutations"},
+ {"shuf", shuf_main, "generate random permutations"},
#endif
#if CFBOX_ENABLE_FACTOR
- {"factor", factor_main, "print the prime factors of numbers"},
+ {"factor", factor_main, "print the prime factors of numbers"},
#endif
#if CFBOX_ENABLE_TIMEOUT
- {"timeout", timeout_main, "run a command with a time limit"},
+ {"timeout", timeout_main, "run a command with a time limit"},
#endif
#if CFBOX_ENABLE_NICE
- {"nice", nice_main, "run a program with modified scheduling priority"},
+ {"nice", nice_main, "run a program with modified scheduling priority"},
#endif
#if CFBOX_ENABLE_NOHUP
- {"nohup", nohup_main, "run a command immune to hangups"},
+ {"nohup", nohup_main, "run a command immune to hangups"},
#endif
#if CFBOX_ENABLE_DF
- {"df", df_main, "report file system disk space usage"},
+ {"df", df_main, "report file system disk space usage"},
#endif
#if CFBOX_ENABLE_EXPR
- {"expr", expr_main, "evaluate expressions"},
+ {"expr", expr_main, "evaluate expressions"},
#endif
#if CFBOX_ENABLE_TSORT
- {"tsort", tsort_main, "perform topological sort"},
+ {"tsort", tsort_main, "perform topological sort"},
#endif
#if CFBOX_ENABLE_XARGS
- {"xargs", xargs_main, "build and execute command lines from stdin"},
+ {"xargs", xargs_main, "build and execute command lines from stdin"},
#endif
#if CFBOX_ENABLE_GZIP
- {"gzip", gzip_main, "compress files"},
+ {"gzip", gzip_main, "compress files"},
#endif
#if CFBOX_ENABLE_GUNZIP
- {"gunzip", gunzip_main, "decompress files"},
+ {"gunzip", gunzip_main, "decompress files"},
#endif
#if CFBOX_ENABLE_DIFF
- {"diff", diff_main, "compare files line by line"},
+ {"diff", diff_main, "compare files line by line"},
#endif
#if CFBOX_ENABLE_CMP
- {"cmp", cmp_main, "compare two files byte by byte"},
+ {"cmp", cmp_main, "compare two files byte by byte"},
#endif
#if CFBOX_ENABLE_PATCH
- {"patch", patch_main, "apply a diff file to an original"},
+ {"patch", patch_main, "apply a diff file to an original"},
#endif
#if CFBOX_ENABLE_ED
- {"ed", ed_main, "line-oriented text editor"},
+ {"ed", ed_main, "line-oriented text editor"},
#endif
#if CFBOX_ENABLE_TAR
- {"tar", tar_main, "create, extract, or list tar archives"},
+ {"tar", tar_main, "create, extract, or list tar archives"},
#endif
#if CFBOX_ENABLE_CPIO
- {"cpio", cpio_main, "copy files to and from archives"},
+ {"cpio", cpio_main, "copy files to and from archives"},
#endif
#if CFBOX_ENABLE_AR
- {"ar", ar_main, "create, modify, and extract from archives"},
+ {"ar", ar_main, "create, modify, and extract from archives"},
#endif
#if CFBOX_ENABLE_UNZIP
- {"unzip", unzip_main, "list, test and extract compressed files in a ZIP archive"},
+ {"unzip", unzip_main, "list, test and extract compressed files in a ZIP archive"},
#endif
#if CFBOX_ENABLE_AWK
- {"awk", awk_main, "pattern-directed scanning and processing language"},
+ {"awk", awk_main, "pattern-directed scanning and processing language"},
#endif
#if CFBOX_ENABLE_FREE
- {"free", free_main, "display amount of free and used memory"},
+ {"free", free_main, "display amount of free and used memory"},
#endif
#if CFBOX_ENABLE_UPTIME
- {"uptime", uptime_main, "tell how long the system has been running"},
+ {"uptime", uptime_main, "tell how long the system has been running"},
#endif
#if CFBOX_ENABLE_KILL
- {"kill", kill_main, "send a signal to a process"},
+ {"kill", kill_main, "send a signal to a process"},
#endif
#if CFBOX_ENABLE_PIDOF
- {"pidof", pidof_main, "find the process ID of a running program"},
+ {"pidof", pidof_main, "find the process ID of a running program"},
#endif
#if CFBOX_ENABLE_PS
- {"ps", ps_main, "report a snapshot of current processes"},
+ {"ps", ps_main, "report a snapshot of current processes"},
#endif
#if CFBOX_ENABLE_PGREP
- {"pgrep", pgrep_main, "look up processes based on name"},
- {"pkill", pgrep_main, "signal processes based on name"},
+ {"pgrep", pgrep_main, "look up processes based on name"},
+ {"pkill", pgrep_main, "signal processes based on name"},
#endif
#if CFBOX_ENABLE_SYSCTL
- {"sysctl", sysctl_main, "configure kernel parameters at runtime"},
+ {"sysctl", sysctl_main, "configure kernel parameters at runtime"},
#endif
#if CFBOX_ENABLE_PWDX
- {"pwdx", pwdx_main, "print working directory of a process"},
+ {"pwdx", pwdx_main, "print working directory of a process"},
#endif
#if CFBOX_ENABLE_PSTREE
- {"pstree", pstree_main, "display a tree of processes"},
+ {"pstree", pstree_main, "display a tree of processes"},
#endif
#if CFBOX_ENABLE_PMAP
- {"pmap", pmap_main, "display memory map of a process"},
+ {"pmap", pmap_main, "display memory map of a process"},
#endif
#if CFBOX_ENABLE_FUSER
- {"fuser", fuser_main, "identify processes using files or sockets"},
+ {"fuser", fuser_main, "identify processes using files or sockets"},
#endif
#if CFBOX_ENABLE_IOSTAT
- {"iostat", iostat_main, "report CPU and I/O statistics"},
+ {"iostat", iostat_main, "report CPU and I/O statistics"},
#endif
#if CFBOX_ENABLE_WATCH
- {"watch", watch_main, "execute a program periodically"},
+ {"watch", watch_main, "execute a program periodically"},
#endif
#if CFBOX_ENABLE_TOP
- {"top", top_main, "display Linux processes"},
+ {"top", top_main, "display Linux processes"},
#endif
#if CFBOX_ENABLE_DMESG
- {"dmesg", dmesg_main, "print kernel ring buffer"},
+ {"dmesg", dmesg_main, "print kernel ring buffer"},
#endif
#if CFBOX_ENABLE_HEXDUMP
- {"hexdump", hexdump_main, "display file contents in hexadecimal"},
+ {"hexdump", hexdump_main, "display file contents in hexadecimal"},
#endif
#if CFBOX_ENABLE_MORE
- {"more", more_main, "file perusal filter for crt viewing"},
+ {"more", more_main, "file perusal filter for crt viewing"},
#endif
#if CFBOX_ENABLE_REV
- {"rev", rev_main, "reverse lines characterwise"},
+ {"rev", rev_main, "reverse lines characterwise"},
#endif
#if CFBOX_ENABLE_CAL
- {"cal", cal_main, "display a calendar"},
+ {"cal", cal_main, "display a calendar"},
#endif
#if CFBOX_ENABLE_RENICE
- {"renice", renice_main, "alter priority of running processes"},
+ {"renice", renice_main, "alter priority of running processes"},
#endif
#if CFBOX_ENABLE_CLEAR
- {"clear", clear_main, "clear the terminal screen"},
+ {"clear", clear_main, "clear the terminal screen"},
#endif
#if CFBOX_ENABLE_WHICH
- {"which", which_main, "locate a command"},
+ {"which", which_main, "locate a command"},
+#endif
+#if CFBOX_ENABLE_MOUNT
+ {"mount", mount_main, "mount a filesystem"},
+#endif
+#if CFBOX_ENABLE_MDEV
+ {"mdev", mdev_main, "populate /dev from sysfs"},
#endif
#if CFBOX_ENABLE_MOUNTPOINT
{"mountpoint", mountpoint_main, "check if a path is a mountpoint"},
#endif
#if CFBOX_ENABLE_CHMOD
- {"chmod", chmod_main, "change file mode bits"},
+ {"chmod", chmod_main, "change file mode bits"},
#endif
#if CFBOX_ENABLE_CHOWN
- {"chown", chown_main, "change file owner and group"},
+ {"chown", chown_main, "change file owner and group"},
#endif
#if CFBOX_ENABLE_CHGRP
- {"chgrp", chgrp_main, "change group ownership"},
+ {"chgrp", chgrp_main, "change group ownership"},
+#endif
+#if CFBOX_ENABLE_UMOUNT
+ {"umount", umount_main, "unmount filesystems"},
+#endif
+#if CFBOX_ENABLE_SWAPOFF
+ {"swapoff", swapoff_main, "disable swap"},
+#endif
+#if CFBOX_ENABLE_REBOOT
+ {"reboot", reboot_main, "reboot the system"},
+ {"poweroff", poweroff_main, "power off the system"},
#endif
});
diff --git a/include/cfbox/deflate.hpp b/include/cfbox/deflate.hpp
index b354fac..c5b97b4 100644
--- a/include/cfbox/deflate.hpp
+++ b/include/cfbox/deflate.hpp
@@ -1,5 +1,6 @@
#pragma once
+#include
#include
#include
#include
@@ -13,7 +14,7 @@ class BitWriter {
int bit_pos_ = 0;
std::uint8_t current_ = 0;
-public:
+ public:
explicit BitWriter(std::vector& out) : out_(out) {}
~BitWriter() { flush(); }
@@ -72,33 +73,27 @@ inline auto encode_fixed_dist(std::uint8_t dist_code, BitWriter& bw) -> void {
bw.write_huffman(static_cast(dist_code), 5);
}
-static constexpr int len_base[] = {
- 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
- 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258
-};
-static constexpr int len_extra[] = {
- 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
- 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0
-};
-static constexpr int dst_base[] = {
- 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
- 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
- 8193, 12289, 16385, 24577
-};
-static constexpr int dst_extra[] = {
- 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
- 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13
-};
+static constexpr int len_base[] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27,
+ 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
+static constexpr int len_extra[] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
+ 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
+static constexpr int dst_base[] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25,
+ 33, 49, 65, 97, 129, 193, 257, 385, 513, 769,
+ 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
+static constexpr int dst_extra[] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6,
+ 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
inline auto find_length_code(int length) -> int {
for (int i = 28; i >= 0; --i)
- if (length >= len_base[i]) return i;
+ if (length >= len_base[i])
+ return i;
return 0;
}
inline auto find_dist_code(int dist) -> int {
for (int i = 29; i >= 0; --i)
- if (dist >= dst_base[i]) return i;
+ if (dist >= dst_base[i])
+ return i;
return 0;
}
@@ -120,19 +115,23 @@ class Matcher {
auto b = static_cast(data_[pos + 1]);
auto c = static_cast(data_[pos + 2]);
return (static_cast(a) << HASH_BITS ^
- static_cast(b) << (HASH_BITS - 5) ^
- c) & (HASH_SIZE - 1);
+ static_cast(b) << (HASH_BITS - 5) ^ c) &
+ (HASH_SIZE - 1);
}
-public:
+ public:
Matcher(const std::uint8_t* data, std::size_t size)
: head_(HASH_SIZE, -1), prev_(size, -1), data_(data), size_(size) {}
- struct Match { int length; int distance; };
+ struct Match {
+ int length;
+ int distance;
+ };
auto find(std::size_t pos) -> Match {
Match best{0, 0};
- if (pos + MIN_MATCH > size_) return best;
+ if (pos + MIN_MATCH > size_)
+ return best;
auto h = hash3(pos);
int chain = head_[h];
@@ -140,17 +139,21 @@ class Matcher {
while (chain >= 0 && tries-- > 0) {
auto dist = static_cast(pos - static_cast(chain));
- if (dist > 32768) break;
+ if (dist > 32768)
+ break;
int len = 0;
- auto max_len = static_cast(std::min(
- static_cast(MAX_MATCH), size_ - pos));
- while (len < max_len && data_[static_cast(chain) + static_cast(len)] == data_[pos + static_cast(len)])
+ auto max_len =
+ static_cast(std::min(static_cast(MAX_MATCH), size_ - pos));
+ while (len < max_len &&
+ data_[static_cast(chain) + static_cast(len)] ==
+ data_[pos + static_cast(len)])
++len;
if (len >= MIN_MATCH && len > best.length) {
best = {len, dist};
- if (len == MAX_MATCH) break;
+ if (len == MAX_MATCH)
+ break;
}
chain = prev_[static_cast(chain)];
}
@@ -186,14 +189,12 @@ inline auto deflate_compress(const std::uint8_t* data, std::size_t size)
int lc = find_length_code(match.length);
encode_fixed_lit(static_cast(lc + 257), bw);
if (len_extra[lc] > 0)
- bw.write(static_cast(match.length - len_base[lc]),
- len_extra[lc]);
+ bw.write(static_cast(match.length - len_base[lc]), len_extra[lc]);
int dc = find_dist_code(match.distance);
encode_fixed_dist(static_cast(dc), bw);
if (dst_extra[dc] > 0)
- bw.write(static_cast(match.distance - dst_base[dc]),
- dst_extra[dc]);
+ bw.write(static_cast(match.distance - dst_base[dc]), dst_extra[dc]);
// Update hash chains for skipped positions
auto end = pos + static_cast(match.length);
diff --git a/include/cfbox/inflate.hpp b/include/cfbox/inflate.hpp
index 7760802..78ff67a 100644
--- a/include/cfbox/inflate.hpp
+++ b/include/cfbox/inflate.hpp
@@ -1,5 +1,6 @@
#pragma once
+#include
#include
#include
#include
@@ -28,9 +29,8 @@ class BitReader {
}
}
-public:
- BitReader(const std::uint8_t* data, std::size_t size)
- : data_(data), size_(size) {}
+ public:
+ BitReader(const std::uint8_t* data, std::size_t size) : data_(data), size_(size) {}
auto read(int n) -> std::uint32_t {
fill(n);
@@ -52,7 +52,10 @@ class BitReader {
auto align() -> void {
auto discard = static_cast(buf_bits_ % 8);
- if (discard > 0) { buf_ >>= discard; buf_bits_ -= discard; }
+ if (discard > 0) {
+ buf_ >>= discard;
+ buf_bits_ -= discard;
+ }
}
auto read_block(std::size_t n, std::uint8_t* out) -> bool {
@@ -64,7 +67,8 @@ class BitReader {
}
buf_bits_ = 0;
buf_ = 0;
- if (pos_ + n > size_) return false;
+ if (pos_ + n > size_)
+ return false;
std::memcpy(out, data_ + pos_, n);
pos_ += n;
return true;
@@ -77,7 +81,9 @@ inline auto build_huffman_table(const std::vector& lengths, int max_bits)
std::vector table(sz, {0, 0});
std::vector bl_count(max_bits + 1, 0);
- for (auto l : lengths) if (l > 0) bl_count[l]++;
+ for (auto l : lengths)
+ if (l > 0)
+ bl_count[l]++;
std::vector next_code(max_bits + 1, 0);
int code = 0;
@@ -88,7 +94,8 @@ inline auto build_huffman_table(const std::vector& lengths, int max_bits)
for (std::size_t sym = 0; sym < lengths.size(); ++sym) {
int len = lengths[sym];
- if (len == 0) continue;
+ if (len == 0)
+ continue;
int c = next_code[len]++;
std::uint32_t rev = 0;
for (int i = 0; i < len; ++i)
@@ -100,38 +107,34 @@ inline auto build_huffman_table(const std::vector& lengths, int max_bits)
return table;
}
-inline auto decode_symbol(BitReader& br, const std::vector& table, int max_bits)
- -> int {
+inline auto decode_symbol(BitReader& br, const std::vector& table, int max_bits) -> int {
auto peek = br.peek(max_bits);
auto& e = table[peek];
br.skip(e.bits);
return e.symbol;
}
-static constexpr int length_base[] = {
- 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
- 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258
-};
-static constexpr int length_extra[] = {
- 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
- 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0
-};
-static constexpr int dist_base[] = {
- 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
- 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
- 8193, 12289, 16385, 24577
-};
-static constexpr int dist_extra[] = {
- 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
- 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13
-};
+static constexpr int length_base[] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13,
+ 15, 17, 19, 23, 27, 31, 35, 43, 51, 59,
+ 67, 83, 99, 115, 131, 163, 195, 227, 258};
+static constexpr int length_extra[] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
+ 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
+static constexpr int dist_base[] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25,
+ 33, 49, 65, 97, 129, 193, 257, 385, 513, 769,
+ 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
+static constexpr int dist_extra[] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6,
+ 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
inline auto fixed_lit_lengths() -> std::vector {
std::vector l(288);
- for (int i = 0; i <= 143; ++i) l[static_cast(i)] = 8;
- for (int i = 144; i <= 255; ++i) l[static_cast(i)] = 9;
- for (int i = 256; i <= 279; ++i) l[static_cast(i)] = 7;
- for (int i = 280; i <= 287; ++i) l[static_cast(i)] = 8;
+ for (int i = 0; i <= 143; ++i)
+ l[static_cast(i)] = 8;
+ for (int i = 144; i <= 255; ++i)
+ l[static_cast(i)] = 9;
+ for (int i = 256; i <= 279; ++i)
+ l[static_cast(i)] = 7;
+ for (int i = 280; i <= 287; ++i)
+ l[static_cast(i)] = 8;
return l;
}
@@ -139,14 +142,14 @@ inline auto fixed_dist_lengths() -> std::vector {
return std::vector(32, 5);
}
-inline auto decode_dynamic_tables(BitReader& br,
- std::vector& lit_table, int& lit_bits,
- std::vector& dist_table, int& dist_bits) -> bool {
+inline auto decode_dynamic_tables(BitReader& br, std::vector& lit_table, int& lit_bits,
+ std::vector& dist_table, int& dist_bits) -> bool {
auto hlit = static_cast(br.read(5)) + 257;
auto hdist = static_cast(br.read(5)) + 1;
int hclen = static_cast(br.read(4)) + 4;
- static constexpr int cl_order[] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
+ static constexpr int cl_order[] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5,
+ 11, 4, 12, 3, 13, 2, 14, 1, 15};
std::vector cl_lengths(19, 0);
for (int i = 0; i < hclen; ++i)
cl_lengths[static_cast(cl_order[i])] = static_cast(br.read(3));
@@ -163,13 +166,16 @@ inline auto decode_dynamic_tables(BitReader& br,
} else if (sym == 16) {
int rep = static_cast(br.read(2)) + 3;
int prev = (i > 0) ? all[i - 1] : 0;
- for (int j = 0; j < rep && i < total; ++j) all[i++] = prev;
+ for (int j = 0; j < rep && i < total; ++j)
+ all[i++] = prev;
} else if (sym == 17) {
int rep = static_cast(br.read(3)) + 3;
- for (int j = 0; j < rep && i < total; ++j) all[i++] = 0;
+ for (int j = 0; j < rep && i < total; ++j)
+ all[i++] = 0;
} else if (sym == 18) {
int rep = static_cast(br.read(7)) + 11;
- for (int j = 0; j < rep && i < total; ++j) all[i++] = 0;
+ for (int j = 0; j < rep && i < total; ++j)
+ all[i++] = 0;
} else {
return false;
}
@@ -179,9 +185,11 @@ inline auto decode_dynamic_tables(BitReader& br,
std::vector dl(all.begin() + static_cast(hlit), all.end());
lit_bits = 1;
- for (auto v : ll) lit_bits = std::max(lit_bits, v);
+ for (auto v : ll)
+ lit_bits = std::max(lit_bits, v);
dist_bits = 1;
- for (auto v : dl) dist_bits = std::max(dist_bits, v);
+ for (auto v : dl)
+ dist_bits = std::max(dist_bits, v);
lit_table = build_huffman_table(ll, lit_bits);
dist_table = build_huffman_table(dl, dist_bits);
@@ -192,7 +200,8 @@ inline auto inflate(const std::uint8_t* data, std::size_t size, std::size_t expe
-> std::string {
BitReader br(data, size);
std::string out;
- if (expected > 0) out.reserve(expected);
+ if (expected > 0)
+ out.reserve(expected);
bool done = false;
while (!done) {
@@ -202,11 +211,12 @@ inline auto inflate(const std::uint8_t* data, std::size_t size, std::size_t expe
if (btype == 0) {
br.align();
std::uint8_t hdr[4];
- if (!br.read_block(4, hdr)) break;
- auto len = static_cast(hdr[0]) |
- (static_cast(hdr[1]) << 8);
+ if (!br.read_block(4, hdr))
+ break;
+ auto len = static_cast(hdr[0]) | (static_cast(hdr[1]) << 8);
std::string blk(len, '\0');
- if (!br.read_block(len, reinterpret_cast(blk.data()))) break;
+ if (!br.read_block(len, reinterpret_cast(blk.data())))
+ break;
out += blk;
} else if (btype == 1 || btype == 2) {
std::vector lt, dt;
@@ -215,9 +225,11 @@ inline auto inflate(const std::uint8_t* data, std::size_t size, std::size_t expe
if (btype == 1) {
lt = build_huffman_table(fixed_lit_lengths(), 9);
dt = build_huffman_table(fixed_dist_lengths(), 5);
- lb = 9; db = 5;
+ lb = 9;
+ db = 5;
} else {
- if (!decode_dynamic_tables(br, lt, lb, dt, db)) break;
+ if (!decode_dynamic_tables(br, lt, lb, dt, db))
+ break;
}
for (;;) {
diff --git a/projects/README.md b/projects/README.md
new file mode 100644
index 0000000..c7a6d82
--- /dev/null
+++ b/projects/README.md
@@ -0,0 +1,23 @@
+# projects/ — CFBox 集成 demo
+
+这里挂的是 CFBox 的**参考集成场景**。**CFBox 自己的 CMake/CI 不依赖本目录** ——
+各场景跑自己的 build flow;CFBox 这边只产出二进制喂给它们。两边解耦。
+
+## imx-forge-demo
+
+[imx-forge](https://github.com/Awesome-Embedded-Learning-Studio/imx-forge) 的子模块快照:
+面向 NXP i.MX6ULL 的嵌入式 Linux 开发工坊,rootfs 阶段用 **BusyBox** 当 init + 工具集。
+
+**本 demo 目的**:验证 CFBox 能否在 i.MX6ULL(armhf)rootfs 里**替代 BusyBox** —— 当
+PID 1(init + inittab)+ shell + mdev + 核心工具,撑起 imx-forge 的 rootfs 启动闭环。
+这是 CFBox「立得住 / 不是玩具」的真实锚定场景。
+
+**hack 流程**:
+1. CFBox 产 armhf static 二进制:
+ `cmake -B build-armhf-static -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain/Toolchain-armhf.cmake -DCMAKE_BUILD_TYPE=Release -DCFBOX_OPTIMIZE_FOR_SIZE=ON -DCFBOX_STATIC_LINK=ON`
+2. symlink 成 imx-forge rootfs 要的命令(`init sh cp ls mount mdev ... → cfbox`)
+3. 塞进 `imx-forge-demo/rootfs/` 替换 busybox
+4. qemu / 板子启动,看到哪挂 → 真实 gap = CFBox v1.0 优先级依据
+
+> 子模块**未递归** imx-forge 的 `third_party`(linux/uboot),保持轻量。需要内核/uboot
+> 源码时手动:`git -C projects/imx-forge-demo submodule update --init`。
diff --git a/projects/imx-forge-demo b/projects/imx-forge-demo
new file mode 160000
index 0000000..ff9f657
--- /dev/null
+++ b/projects/imx-forge-demo
@@ -0,0 +1 @@
+Subproject commit ff9f6572d83675179c7516861b6eab8b5005f781
diff --git a/scripts/gen_size_table.sh b/scripts/gen_size_table.sh
new file mode 100755
index 0000000..0efbc9c
--- /dev/null
+++ b/scripts/gen_size_table.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# gen_size_table.sh — Generate the size-comparison markdown table for the README.
+#
+# Measures CFBox for real (size-opt native + armhf static when the toolchain is
+# available); other projects are well-known reference values. Output goes to
+# stdout — pipe into the README's "体积对比" / "Size comparison" section.
+#
+# Usage: scripts/gen_size_table.sh
+
+cd "$(dirname "$0")/.."
+
+# --- applet count from the registry (single source of truth) ---
+applets=$(awk '/constexpr auto APPLET_REGISTRY/,/^}\)/' include/cfbox/applets.hpp | grep -c '{"')
+
+# --- CFBox size-opt (native, dynamic) ---
+cmake --no-warn-unused-cli -B build-size -DCMAKE_BUILD_TYPE=Release -DCFBOX_OPTIMIZE_FOR_SIZE=ON >/dev/null
+cmake --build build-size -j"$(nproc)" >/dev/null
+strip --strip-unneeded build-size/cfbox 2>/dev/null || true
+size_opt_bytes=$(stat -c %s build-size/cfbox)
+size_opt_kb=$(( size_opt_bytes / 1024 ))
+per_applet=$(awk "BEGIN{ printf \"%.1f\", ${size_opt_kb} / ${applets} }")
+
+# --- CFBox armhf static (optional — needs the arm toolchain on PATH) ---
+armhf_row=""
+if command -v arm-none-linux-gnueabihf-gcc >/dev/null 2>&1; then
+ cmake --no-warn-unused-cli -B build-armhf-static \
+ -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain/Toolchain-armhf.cmake \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DCFBOX_OPTIMIZE_FOR_SIZE=ON \
+ -DCFBOX_STATIC_LINK=ON >/dev/null
+ cmake --build build-armhf-static -j"$(nproc)" >/dev/null
+ arm-none-linux-gnueabihf-strip --strip-unneeded build-armhf-static/cfbox 2>/dev/null || true
+ armhf_bytes=$(stat -c %s build-armhf-static/cfbox)
+ armhf_mb=$(awk "BEGIN{ printf \"%.1f\", ${armhf_bytes} / 1048576 }")
+ armhf_row="| CFBox (armhf static) | C++23 | ~${armhf_mb} MB | ${applets} | — |"
+fi
+
+# --- reference values for other projects (well-known, not measured here) ---
+cat <
#include
-#include
#include
+#include
#include
#include
#include
+#include
#include
#include
-#include
namespace {
constexpr cfbox::help::HelpEntry HELP = {
- .name = "ar",
+ .name = "ar",
.version = CFBOX_VERSION_STRING,
.one_line = "create, modify, and extract from archives",
- .usage = "ar -[dmpqrtx] ARCHIVE [FILE]...",
+ .usage = "ar -[dmpqrtx] ARCHIVE [FILE]...",
.options = " -r insert or replace files\n"
" -t list contents\n"
" -x extract files",
- .extra = "",
+ .extra = "",
};
} // namespace
auto ar_main(int argc, char* argv[]) -> int {
- auto parsed = cfbox::args::parse(argc, argv, {
- cfbox::args::OptSpec{'r', false},
- cfbox::args::OptSpec{'t', false},
- cfbox::args::OptSpec{'x', false},
- });
+ auto parsed = cfbox::args::parse(argc, argv,
+ {
+ cfbox::args::OptSpec{'r', false},
+ cfbox::args::OptSpec{'t', false},
+ cfbox::args::OptSpec{'x', false},
+ });
- if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; }
- if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; }
+ if (parsed.has_long("help")) {
+ cfbox::help::print_help(HELP);
+ return 0;
+ }
+ if (parsed.has_long("version")) {
+ cfbox::help::print_version(HELP);
+ return 0;
+ }
bool replace = parsed.has('r');
bool list = parsed.has('t');
@@ -50,19 +58,22 @@ auto ar_main(int argc, char* argv[]) -> int {
for (std::size_t i = 1; i < pos.size(); ++i) {
std::string fname{pos[i]};
auto data = cfbox::io::read_all(fname);
- if (!data) continue;
+ if (!data)
+ continue;
char hdr[60];
std::memset(hdr, ' ', 60);
std::memcpy(hdr, fname.c_str(), std::min(fname.size(), static_cast(16)));
std::snprintf(hdr + 16, 12, "%-10lu", static_cast(::time(nullptr)));
- std::snprintf(hdr + 28, 12, "%-6u", 0u); // uid
- std::snprintf(hdr + 34, 12, "%-6u", 0u); // gid
+ std::snprintf(hdr + 28, 12, "%-6u", 0u); // uid
+ std::snprintf(hdr + 34, 12, "%-6u", 0u); // gid
std::snprintf(hdr + 40, 12, "%-8o", 0100644u);
std::snprintf(hdr + 48, 12, "%-10zu", data->size());
- hdr[58] = '`'; hdr[59] = '\n';
+ hdr[58] = '`';
+ hdr[59] = '\n';
output.append(hdr, 60);
output.append(*data);
- if (data->size() % 2) output += '\n';
+ if (data->size() % 2)
+ output += '\n';
}
auto wresult = cfbox::io::write_all(archive, output);
if (!wresult) {
@@ -87,7 +98,8 @@ auto ar_main(int argc, char* argv[]) -> int {
while (offset + 60 <= data.size()) {
auto name = std::string{data.substr(offset, 16)};
auto space = name.find(' ');
- if (space != std::string::npos) name = name.substr(0, space);
+ if (space != std::string::npos)
+ name = name.substr(0, space);
auto size_str = std::string{data.substr(offset + 48, 10)};
auto fsize = std::stoul(size_str);
@@ -101,7 +113,8 @@ auto ar_main(int argc, char* argv[]) -> int {
}
}
offset += 60 + fsize;
- if (fsize % 2) ++offset;
+ if (fsize % 2)
+ ++offset;
}
return 0;
}
diff --git a/src/applets/init/init.hpp b/src/applets/init/init.hpp
index 9fd259c..af1e86e 100644
--- a/src/applets/init/init.hpp
+++ b/src/applets/init/init.hpp
@@ -5,46 +5,47 @@
#include
#include
#include
-#include
-#include
#include
#include
+#include
+#include
#include
namespace cfbox::init {
constexpr cfbox::help::HelpEntry HELP = {
- .name = "init",
+ .name = "init",
.version = CFBOX_VERSION_STRING,
.one_line = "system init (PID 1) with inittab support",
- .usage = "init",
+ .usage = "init",
.options = "",
- .extra = "If /etc/inittab exists, runs full init system.\n"
- "Otherwise falls back to boot-test mode for QEMU.",
+ .extra = "If /etc/inittab exists, runs full init system.\n"
+ "Otherwise falls back to boot-test mode for QEMU.",
};
// inittab entry: id:runlevels:action:process
struct InittabEntry {
- std::string id; // 1-4 char identifier
- std::string runlevels; // which runlevels (empty = all)
- std::string action; // sysinit, boot, once, respawn, wait, ctrlaltdel, shutdown
- std::string process; // command to run
+ std::string id; // 1-4 char identifier
+ std::string runlevels; // which runlevels (empty = all)
+ std::string action; // sysinit, boot, once, respawn, wait, askfirst, ctrlaltdel, shutdown
+ std::string process; // command to run
};
enum class RunLevel {
SysInit,
Boot,
Single,
- MultiUser, // runlevel 2-5
+ MultiUser, // runlevel 2-5
Shutdown,
};
// Tracks a spawned child process and its inittab entry
struct SpawnedProcess {
pid_t pid = 0;
- std::string process; // command
- bool respawn = false; // true if this is a respawn entry
+ std::string process; // command
+ bool respawn = false; // true if this is a respawn entry
+ bool askfirst = false; // gate exec behind a console keypress before restart
std::chrono::steady_clock::time_point last_spawn;
int respawn_delay_ms = 2000; // initial backoff delay
};
@@ -71,7 +72,8 @@ auto run_boot_entries(InitState& state) -> void;
auto run_level_entries(InitState& state, const std::string& level) -> void;
// init_spawn.cpp
-auto spawn_process(InitState& state, const InittabEntry& entry, bool respawn) -> pid_t;
+auto spawn_process(InitState& state, const InittabEntry& entry, bool respawn, bool askfirst = false)
+ -> pid_t;
auto reap_children(InitState& state) -> void;
auto check_respawn(InitState& state) -> void;
diff --git a/src/applets/init/init_runlevel.cpp b/src/applets/init/init_runlevel.cpp
index 9df56a9..0ec6dc1 100644
--- a/src/applets/init/init_runlevel.cpp
+++ b/src/applets/init/init_runlevel.cpp
@@ -10,9 +10,9 @@ auto run_sysinit(InitState& state) -> void {
// Mount essential filesystems if PID 1
if (state.is_pid1) {
- mount("proc", "/proc", "proc", 0, nullptr);
- mount("sysfs", "/sys", "sysfs", 0, nullptr);
- mount("devtmpfs", "/dev", "devtmpfs", 0, nullptr);
+ mount("proc", "/proc", "proc", 0, nullptr);
+ mount("sysfs", "/sys", "sysfs", 0, nullptr);
+ mount("devtmpfs", "/dev", "devtmpfs", 0, nullptr);
}
// Run sysinit entries (sequentially, wait for each)
@@ -52,9 +52,13 @@ auto run_level_entries(InitState& state, const std::string& level) -> void {
bool should_respawn = (e.action == "respawn");
bool should_wait = (e.action == "wait");
bool is_once = (e.action == "once");
+ bool is_askfirst = (e.action == "askfirst");
- if (should_respawn || is_once || should_wait) {
- auto pid = spawn_process(state, e, should_respawn);
+ if (should_respawn || is_once || should_wait || is_askfirst) {
+ // askfirst is tracked and restarted like respawn, but each launch
+ // waits for a console keypress before exec'ing the process.
+ bool track = should_respawn || is_askfirst;
+ auto pid = spawn_process(state, e, track, is_askfirst);
if (pid > 0 && should_wait) {
int status = 0;
waitpid(pid, &status, 0);
diff --git a/src/applets/init/init_spawn.cpp b/src/applets/init/init_spawn.cpp
index 674b9c5..5d4f68d 100644
--- a/src/applets/init/init_spawn.cpp
+++ b/src/applets/init/init_spawn.cpp
@@ -1,15 +1,17 @@
#include "init.hpp"
+#include
#include
-#include
+#include
#include
-#include
+#include
#include
-#include
+#include
namespace cfbox::init {
-auto spawn_process(InitState& state, const InittabEntry& entry, bool respawn) -> pid_t {
+auto spawn_process(InitState& state, const InittabEntry& entry, bool respawn, bool askfirst)
+ -> pid_t {
std::fflush(nullptr);
pid_t pid = fork();
@@ -25,11 +27,33 @@ auto spawn_process(InitState& state, const InittabEntry& entry, bool respawn) ->
setsid();
}
+ // inittab process may carry a leading '-' (e.g. askfirst "-/bin/sh"),
+ // meaning it wants the controlling console; strip it so sh -c receives
+ // a plain command rather than "-/bin/sh".
+ std::string cmd = entry.process;
+ if (!cmd.empty() && cmd[0] == '-') {
+ cmd.erase(0, 1);
+ }
+
+ // askfirst: gate the exec behind a console keypress so boot logs are
+ // not drowned by an immediate login prompt. EOF (no console wired up)
+ // exits quietly; init respawns, re-prompting once a tty appears.
+ if (askfirst) {
+ static constexpr char prompt[] = "\nPlease press Enter to activate this console.";
+ if (write(STDERR_FILENO, prompt, sizeof(prompt) - 1) < 0) {
+ // best-effort prompt: a write failure does not block the
+ // Enter wait below, so ignore it.
+ }
+ char c = 0;
+ if (read(STDIN_FILENO, &c, 1) <= 0 || c != '\n') {
+ _exit(0);
+ }
+ }
+
// Build argv for: /bin/sh -c "process"
char shell[] = "/bin/sh";
char dash_c[] = "-c";
- std::string cmd = entry.process;
- char* argv[] = { shell, dash_c, cmd.data(), nullptr };
+ char* argv[] = {shell, dash_c, cmd.data(), nullptr};
execv(shell, argv);
// If execv fails, try /bin/cfbox sh
@@ -43,6 +67,7 @@ auto spawn_process(InitState& state, const InittabEntry& entry, bool respawn) ->
sp.pid = pid;
sp.process = entry.process;
sp.respawn = true;
+ sp.askfirst = askfirst;
sp.last_spawn = std::chrono::steady_clock::now();
sp.respawn_delay_ms = 2000;
state.children.push_back(std::move(sp));
@@ -74,10 +99,11 @@ auto check_respawn(InitState& state) -> void {
auto now = std::chrono::steady_clock::now();
for (auto& child : state.children) {
- if (child.pid != 0 || !child.respawn || state.shutting_down) continue;
+ if (child.pid != 0 || !child.respawn || state.shutting_down)
+ continue;
- auto elapsed_ms = std::chrono::duration_cast(
- now - child.last_spawn).count();
+ auto elapsed_ms =
+ std::chrono::duration_cast(now - child.last_spawn).count();
// If the process lived more than 60s, reset backoff
if (elapsed_ms > 60000) {
@@ -86,13 +112,14 @@ auto check_respawn(InitState& state) -> void {
// Check if enough time has passed for respawn
auto wait_ms = child.respawn_delay_ms;
- if (elapsed_ms < wait_ms) continue;
+ if (elapsed_ms < wait_ms)
+ continue;
// Respawn
InittabEntry fake_entry;
fake_entry.process = child.process;
- auto new_pid = spawn_process(state, fake_entry, false);
+ auto new_pid = spawn_process(state, fake_entry, false, child.askfirst);
if (new_pid > 0) {
child.pid = new_pid;
child.last_spawn = now;
diff --git a/src/applets/ls.cpp b/src/applets/ls.cpp
index da8e0e0..c8403f4 100644
--- a/src/applets/ls.cpp
+++ b/src/applets/ls.cpp
@@ -2,14 +2,18 @@
#include
#include
#include
+#include
+#include
#include
#include
+#include
+#include
#include
#include
+#include
#include
#include
-#include
namespace {
@@ -34,14 +38,14 @@ auto format_permissions(std::filesystem::perms p) -> std::string {
char buf[11];
buf[0] = '-'; // will be overridden for special types
- buf[1] = (p & std::filesystem::perms::owner_read) != std::filesystem::perms::none ? 'r' : '-';
+ buf[1] = (p & std::filesystem::perms::owner_read) != std::filesystem::perms::none ? 'r' : '-';
buf[2] = (p & std::filesystem::perms::owner_write) != std::filesystem::perms::none ? 'w' : '-';
- buf[3] = (p & std::filesystem::perms::owner_exec) != std::filesystem::perms::none ? 'x' : '-';
- buf[4] = (p & std::filesystem::perms::group_read) != std::filesystem::perms::none ? 'r' : '-';
+ buf[3] = (p & std::filesystem::perms::owner_exec) != std::filesystem::perms::none ? 'x' : '-';
+ buf[4] = (p & std::filesystem::perms::group_read) != std::filesystem::perms::none ? 'r' : '-';
buf[5] = (p & std::filesystem::perms::group_write) != std::filesystem::perms::none ? 'w' : '-';
- buf[6] = (p & std::filesystem::perms::group_exec) != std::filesystem::perms::none ? 'x' : '-';
+ buf[6] = (p & std::filesystem::perms::group_exec) != std::filesystem::perms::none ? 'x' : '-';
buf[7] = (p & std::filesystem::perms::others_read) != std::filesystem::perms::none ? 'r' : '-';
- buf[8] = (p & std::filesystem::perms::others_write)!= std::filesystem::perms::none ? 'w' : '-';
+ buf[8] = (p & std::filesystem::perms::others_write) != std::filesystem::perms::none ? 'w' : '-';
buf[9] = (p & std::filesystem::perms::others_exec) != std::filesystem::perms::none ? 'x' : '-';
buf[10] = '\0';
return std::string{buf, 10};
@@ -49,13 +53,20 @@ auto format_permissions(std::filesystem::perms p) -> std::string {
auto format_type_char(std::filesystem::file_type type) -> char {
switch (type) {
- case std::filesystem::file_type::directory: return 'd';
- case std::filesystem::file_type::symlink: return 'l';
- case std::filesystem::file_type::block: return 'b';
- case std::filesystem::file_type::character: return 'c';
- case std::filesystem::file_type::fifo: return 'p';
- case std::filesystem::file_type::socket: return 's';
- default: return '-';
+ case std::filesystem::file_type::directory:
+ return 'd';
+ case std::filesystem::file_type::symlink:
+ return 'l';
+ case std::filesystem::file_type::block:
+ return 'b';
+ case std::filesystem::file_type::character:
+ return 'c';
+ case std::filesystem::file_type::fifo:
+ return 'p';
+ case std::filesystem::file_type::socket:
+ return 's';
+ default:
+ return '-';
}
}
@@ -68,10 +79,25 @@ auto format_time(std::filesystem::file_time_type ftime) -> std::string {
return buf;
}
+// Resolve uid/gid to a name; fall back to the numeric id when NSS cannot
+// resolve it (a statically linked cfbox on a minimal rootfs has no NSS libs,
+// so names silently fail — show the number instead of a blank field).
+auto owner_of(uid_t uid) -> std::string {
+ if (auto* pw = getpwuid(uid))
+ return pw->pw_name;
+ return std::to_string(uid);
+}
+
+auto group_of(gid_t gid) -> std::string {
+ if (auto* gr = getgrgid(gid))
+ return gr->gr_name;
+ return std::to_string(gid);
+}
+
struct LsOptions {
- bool all = false; // -a
+ bool all = false; // -a
bool long_format = false; // -l
- bool human = false; // -h
+ bool human = false; // -h
};
auto list_directory(const std::string& path, const LsOptions& opts) -> int {
@@ -88,21 +114,23 @@ auto list_directory(const std::string& path, const LsOptions& opts) -> int {
visible.reserve(entries.size());
for (const auto& e : entries) {
std::string name = e.path().filename().string();
- if (!opts.all && !name.empty() && name[0] == '.') continue;
+ if (!opts.all && !name.empty() && name[0] == '.')
+ continue;
visible.push_back(e);
}
// Sort entries
- std::sort(visible.begin(), visible.end(),
- [](const std::filesystem::directory_entry& a,
- const std::filesystem::directory_entry& b) {
- return a.path().filename().string() < b.path().filename().string();
- });
+ std::sort(
+ visible.begin(), visible.end(),
+ [](const std::filesystem::directory_entry& a, const std::filesystem::directory_entry& b) {
+ return a.path().filename().string() < b.path().filename().string();
+ });
if (opts.long_format) {
for (const auto& e : visible) {
auto status_result = cfbox::fs::symlink_status(e.path().string());
- if (!status_result) continue;
+ if (!status_result)
+ continue;
auto& st = status_result.value();
char type_char = format_type_char(st.type());
@@ -129,6 +157,13 @@ auto list_directory(const std::string& path, const LsOptions& opts) -> int {
}
std::string name = e.path().filename().string();
+ std::string owner = "?";
+ std::string group = "?";
+ struct stat lst;
+ if (::lstat(e.path().string().c_str(), &lst) == 0) {
+ owner = owner_of(lst.st_uid);
+ group = group_of(lst.st_gid);
+ }
if (st.type() == std::filesystem::file_type::symlink) {
std::error_code ec;
auto target = std::filesystem::read_symlink(e.path(), ec);
@@ -137,15 +172,9 @@ auto list_directory(const std::string& path, const LsOptions& opts) -> int {
}
}
- std::printf("%s %3ju %-8s %-8s %*s %s %s\n",
- perms.c_str(),
- static_cast(nlinks),
- "", // owner (placeholder)
- "", // group (placeholder)
- opts.human ? 5 : 8,
- size_str.c_str(),
- time_str.c_str(),
- name.c_str());
+ std::printf("%s %3ju %-8s %-8s %*s %s %s\n", perms.c_str(),
+ static_cast(nlinks), owner.c_str(), group.c_str(),
+ opts.human ? 5 : 8, size_str.c_str(), time_str.c_str(), name.c_str());
}
} else {
for (const auto& e : visible) {
@@ -189,15 +218,17 @@ auto list_path(const std::string& path, const LsOptions& opts, bool show_header)
std::string size_str = opts.human ? format_size_human(size) : std::to_string(size);
std::string name = std::filesystem::path{path}.filename().string();
+ std::string owner = "?";
+ std::string group = "?";
+ struct stat lst;
+ if (::lstat(path.c_str(), &lst) == 0) {
+ owner = owner_of(lst.st_uid);
+ group = group_of(lst.st_gid);
+ }
- std::printf("%s %3ju %-8s %-8s %*s %s %s\n",
- perms.c_str(),
- static_cast(nlinks),
- "", "",
- opts.human ? 5 : 8,
- size_str.c_str(),
- time_str.c_str(),
- name.c_str());
+ std::printf("%s %3ju %-8s %-8s %*s %s %s\n", perms.c_str(),
+ static_cast(nlinks), owner.c_str(), group.c_str(),
+ opts.human ? 5 : 8, size_str.c_str(), time_str.c_str(), name.c_str());
} else {
auto fname = std::filesystem::path{path}.filename().string();
std::printf("%s\n", fname.c_str());
@@ -212,27 +243,34 @@ auto list_path(const std::string& path, const LsOptions& opts, bool show_header)
}
constexpr cfbox::help::HelpEntry HELP = {
- .name = "ls",
+ .name = "ls",
.version = CFBOX_VERSION_STRING,
.one_line = "list directory contents",
- .usage = "ls [OPTIONS] [FILE]...",
+ .usage = "ls [OPTIONS] [FILE]...",
.options = " -a do not ignore entries starting with .\n"
" -l use a long listing format\n"
" -h print sizes in human readable format",
- .extra = "",
+ .extra = "",
};
} // namespace
auto ls_main(int argc, char* argv[]) -> int {
- auto parsed = cfbox::args::parse(argc, argv, {
- cfbox::args::OptSpec{'a', false, "all"},
- cfbox::args::OptSpec{'l', false, "long"},
- cfbox::args::OptSpec{'h', false, "human-readable"},
- });
-
- if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; }
- if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; }
+ auto parsed = cfbox::args::parse(argc, argv,
+ {
+ cfbox::args::OptSpec{'a', false, "all"},
+ cfbox::args::OptSpec{'l', false, "long"},
+ cfbox::args::OptSpec{'h', false, "human-readable"},
+ });
+
+ if (parsed.has_long("help")) {
+ cfbox::help::print_help(HELP);
+ return 0;
+ }
+ if (parsed.has_long("version")) {
+ cfbox::help::print_version(HELP);
+ return 0;
+ }
LsOptions opts;
opts.all = parsed.has('a');
diff --git a/src/applets/mdev.cpp b/src/applets/mdev.cpp
new file mode 100644
index 0000000..d250e9a
--- /dev/null
+++ b/src/applets/mdev.cpp
@@ -0,0 +1,135 @@
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+
+constexpr cfbox::help::HelpEntry HELP = {
+ .name = "mdev",
+ .version = CFBOX_VERSION_STRING,
+ .one_line = "populate /dev from sysfs (coldplug scan)",
+ .usage = "mdev -s",
+ .options = " -s scan /sys and create device nodes under /dev",
+ .extra = "",
+};
+
+struct SysDevice {
+ std::string name;
+ int major = -1;
+ int minor = -1;
+ bool is_block = false;
+};
+
+// Read ":" from /dev; absent or unparseable => nullopt.
+auto read_dev(const std::string& dir) -> std::optional> {
+ FILE* f = std::fopen((dir + "/dev").c_str(), "r");
+ if (!f)
+ return std::nullopt;
+ int maj = -1;
+ int min = -1;
+ int n = std::fscanf(f, "%d:%d", &maj, &min);
+ std::fclose(f);
+ if (n != 2 || maj < 0)
+ return std::nullopt;
+ return std::make_pair(maj, min);
+}
+
+auto collect_dir(const std::string& dir, bool is_block, std::vector& out) -> void {
+ DIR* d = opendir(dir.c_str());
+ if (!d)
+ return;
+ while (auto* de = readdir(d)) {
+ if (de->d_name[0] == '.')
+ continue;
+ std::string sub = dir + "/" + de->d_name;
+ if (auto dv = read_dev(sub)) {
+ out.push_back({de->d_name, dv->first, dv->second, is_block});
+ }
+ }
+ closedir(d);
+}
+
+// Walk /sys/class/* (char devices) and /sys/block (block devices plus their
+// partitions), collecting every entry that exposes a major:minor dev file.
+auto walk_sysfs(const std::string& sysroot) -> std::vector {
+ std::vector out;
+
+ std::string classdir = sysroot + "/class";
+ if (DIR* d = opendir(classdir.c_str())) {
+ while (auto* de = readdir(d)) {
+ if (de->d_name[0] == '.')
+ continue;
+ collect_dir(classdir + "/" + de->d_name, false, out);
+ }
+ closedir(d);
+ }
+
+ std::string blockdir = sysroot + "/block";
+ if (DIR* d = opendir(blockdir.c_str())) {
+ while (auto* de = readdir(d)) {
+ if (de->d_name[0] == '.')
+ continue;
+ std::string blk = blockdir + "/" + de->d_name;
+ if (auto dv = read_dev(blk)) {
+ out.push_back({de->d_name, dv->first, dv->second, true});
+ }
+ collect_dir(blk, true, out); // partitions: /sys/block//
+ }
+ closedir(d);
+ }
+
+ return out;
+}
+
+auto make_node(const SysDevice& dev) -> int {
+ std::string path = "/dev/" + dev.name;
+ mode_t type = dev.is_block ? S_IFBLK : S_IFCHR;
+ unlink(path.c_str()); // replace stale node so attributes stay current
+ if (mknod(path.c_str(), type | 0660, makedev(dev.major, dev.minor)) != 0) {
+ CFBOX_ERR("mdev", "%s: %s", path.c_str(), std::strerror(errno));
+ return 1;
+ }
+ return 0;
+}
+
+} // namespace
+
+auto mdev_main(int argc, char* argv[]) -> int {
+ auto parsed = cfbox::args::parse(argc, argv,
+ {
+ cfbox::args::OptSpec{'s', false, "scan"},
+ });
+
+ if (parsed.has_long("help")) {
+ cfbox::help::print_help(HELP);
+ return 0;
+ }
+ if (parsed.has_long("version")) {
+ cfbox::help::print_version(HELP);
+ return 0;
+ }
+
+ if (!parsed.has('s')) {
+ // Hotplug mode (kernel-invoked, uevent via environment) is not
+ // implemented; imx-forge only ever runs the coldplug scan `mdev -s`.
+ return 0;
+ }
+
+ int rc = 0;
+ for (const auto& dev : walk_sysfs("/sys")) {
+ if (make_node(dev) != 0)
+ rc = 1;
+ }
+ return rc;
+}
diff --git a/src/applets/more.cpp b/src/applets/more.cpp
index b5a5458..96a716d 100644
--- a/src/applets/more.cpp
+++ b/src/applets/more.cpp
@@ -1,3 +1,4 @@
+#include
#include
#include
#include
@@ -5,20 +6,20 @@
#include
#include
+#include
#include
#include
#include
-#include
namespace {
constexpr cfbox::help::HelpEntry HELP = {
- .name = "more",
+ .name = "more",
.version = CFBOX_VERSION_STRING,
.one_line = "file perusal filter for crt viewing",
- .usage = "more [FILE]",
+ .usage = "more [FILE]",
.options = "",
- .extra = "Space=next page Enter=next line q=quit",
+ .extra = "Space=next page Enter=next line q=quit",
};
auto read_lines(std::FILE* f) -> std::vector {
@@ -38,8 +39,14 @@ auto read_lines(std::FILE* f) -> std::vector {
auto more_main(int argc, char* argv[]) -> int {
auto parsed = cfbox::args::parse(argc, argv, {});
- if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; }
- if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; }
+ if (parsed.has_long("help")) {
+ cfbox::help::print_help(HELP);
+ return 0;
+ }
+ if (parsed.has_long("version")) {
+ cfbox::help::print_version(HELP);
+ return 0;
+ }
const auto& pos = parsed.positional();
std::string filename = pos.empty() ? "" : std::string(pos[0]);
@@ -54,19 +61,23 @@ auto more_main(int argc, char* argv[]) -> int {
}
auto lines = read_lines(f);
- if (f != stdin) std::fclose(f);
+ if (f != stdin)
+ std::fclose(f);
- if (lines.empty()) return 0;
+ if (lines.empty())
+ return 0;
// Check if output is a terminal
if (!isatty(STDOUT_FILENO)) {
- for (const auto& line : lines) std::printf("%s\n", line.c_str());
+ for (const auto& line : lines)
+ std::printf("%s\n", line.c_str());
return 0;
}
auto [rows, cols] = cfbox::terminal::get_size();
int usable_rows = rows - 1; // Leave room for status line
- if (usable_rows < 1) usable_rows = 1;
+ if (usable_rows < 1)
+ usable_rows = 1;
cfbox::terminal::RawMode raw_mode;
std::size_t top_line = 0;
@@ -89,7 +100,8 @@ auto more_main(int argc, char* argv[]) -> int {
// Status line
cfbox::terminal::invert_video(true);
cfbox::terminal::move_cursor(rows, 1);
- std::printf("--More--(%zu%%)", std::min(end * 100 / lines.size(), static_cast(100)));
+ std::printf("--More--(%zu%%)",
+ std::min(end * 100 / lines.size(), static_cast(100)));
cfbox::terminal::clear_line();
cfbox::terminal::invert_video(false);
std::fflush(stdout);
@@ -97,17 +109,19 @@ auto more_main(int argc, char* argv[]) -> int {
// Wait for key
while (true) {
auto key = cfbox::tui::read_key(0, -1);
- if (!key) continue;
+ if (!key)
+ continue;
- if (key->is_char() && key->ch == 'q') return 0;
- if (key->type == cfbox::tui::KeyType::Escape) return 0;
+ if (key->is_char() && key->ch == 'q')
+ return 0;
+ if (key->type == cfbox::tui::KeyType::Escape)
+ return 0;
if (key->type == cfbox::tui::KeyType::Enter) {
top_line += 1;
break;
}
- if ((key->is_char() && key->ch == ' ') ||
- key->type == cfbox::tui::KeyType::PageDown) {
+ if ((key->is_char() && key->ch == ' ') || key->type == cfbox::tui::KeyType::PageDown) {
top_line += static_cast(usable_rows);
break;
}
diff --git a/src/applets/mount.cpp b/src/applets/mount.cpp
new file mode 100644
index 0000000..ea0bc13
--- /dev/null
+++ b/src/applets/mount.cpp
@@ -0,0 +1,207 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace {
+
+constexpr cfbox::help::HelpEntry HELP = {
+ .name = "mount",
+ .version = CFBOX_VERSION_STRING,
+ .one_line = "mount a filesystem",
+ .usage = "mount [-a] [-t TYPE] [-o OPTS] [-r] [SOURCE TARGET]",
+ .options = " -a mount all entries listed in /etc/fstab\n"
+ " -t TYPE filesystem type (e.g. proc, devpts, tmpfs)\n"
+ " -o OPTS comma-separated mount options (ro,rw,nosuid,...)\n"
+ " -r mount read-only",
+ .extra = "",
+};
+
+struct FstabEntry {
+ std::string fs;
+ std::string mountpoint;
+ std::string type;
+ std::string options;
+};
+
+struct MountOpts {
+ unsigned long flags = 0;
+ std::string data; // fs-specific options that do not map to VFS flags
+};
+
+// Split a comma-separated option string into VFS mount flags plus the leftover
+// fs-specific data string. Mirrors util-linux's flag mapping for the common
+// cases BusyBox rootfs scripts rely on (ro/rw/defaults/nosuid/...).
+auto parse_mount_options(std::string_view opts) -> MountOpts {
+ MountOpts out;
+ std::string data;
+ std::size_t i = 0;
+ while (i <= opts.size()) {
+ auto comma = opts.find(',', i);
+ auto end = (comma == std::string_view::npos) ? opts.size() : comma;
+ auto token = opts.substr(i, end - i);
+
+ if (token == "ro")
+ out.flags |= MS_RDONLY;
+ else if (token == "rw")
+ out.flags &= ~MS_RDONLY;
+ else if (token == "nosuid")
+ out.flags |= MS_NOSUID;
+ else if (token == "nodev")
+ out.flags |= MS_NODEV;
+ else if (token == "noexec")
+ out.flags |= MS_NOEXEC;
+ else if (token == "noatime")
+ out.flags |= MS_NOATIME;
+ else if (token == "relatime")
+ out.flags |= MS_RELATIME;
+ else if (token == "sync")
+ out.flags |= MS_SYNCHRONOUS;
+ else if (token == "remount")
+ out.flags |= MS_REMOUNT;
+ else if (token == "bind")
+ out.flags |= MS_BIND;
+ else if (token == "defaults" || token.empty()) { /* no-op */
+ } else {
+ if (!data.empty())
+ data += ',';
+ data += std::string(token);
+ }
+
+ if (comma == std::string_view::npos)
+ break;
+ i = comma + 1;
+ }
+ out.data = std::move(data);
+ return out;
+}
+
+// Parse /etc/fstab into mount entries. Skips blank lines and '#' comments;
+// fields beyond options (dump/pass) are ignored.
+auto parse_fstab(const std::string& path) -> std::vector {
+ std::vector entries;
+ FILE* f = std::fopen(path.c_str(), "r");
+ if (!f)
+ return entries;
+
+ char buf[512];
+ while (std::fgets(buf, sizeof(buf), f)) {
+ std::string_view line(buf);
+ while (!line.empty() && (line.back() == '\n' || line.back() == '\r'))
+ line.remove_suffix(1);
+
+ auto first = line.find_first_not_of(" \t");
+ if (first == std::string_view::npos)
+ continue; // blank
+ if (line[first] == '#')
+ continue; // comment
+
+ FstabEntry e;
+ std::size_t pos = first;
+ int field = 0;
+ while (field < 4 && pos < line.size()) {
+ while (pos < line.size() && (line[pos] == ' ' || line[pos] == '\t'))
+ ++pos;
+ if (pos >= line.size())
+ break;
+ auto start = pos;
+ while (pos < line.size() && line[pos] != ' ' && line[pos] != '\t')
+ ++pos;
+ auto tok = line.substr(start, pos - start);
+ switch (field) {
+ case 0:
+ e.fs = std::string(tok);
+ break;
+ case 1:
+ e.mountpoint = std::string(tok);
+ break;
+ case 2:
+ e.type = std::string(tok);
+ break;
+ case 3:
+ e.options = std::string(tok);
+ break;
+ }
+ ++field;
+ }
+ if (!e.fs.empty() && !e.mountpoint.empty()) {
+ entries.push_back(std::move(e));
+ }
+ }
+ std::fclose(f);
+ return entries;
+}
+
+auto mount_one(std::string_view source, std::string_view target, std::string_view type,
+ std::string_view opts) -> int {
+ auto mo = parse_mount_options(opts);
+ std::string src(source);
+ std::string tgt(target);
+ std::string ty(type);
+ const char* data = mo.data.empty() ? nullptr : mo.data.c_str();
+ if (::mount(src.c_str(), tgt.c_str(), ty.empty() ? nullptr : ty.c_str(), mo.flags, data) != 0) {
+ CFBOX_ERR("mount", "%s: %s", tgt.c_str(), std::strerror(errno));
+ return 1;
+ }
+ return 0;
+}
+
+} // namespace
+
+auto mount_main(int argc, char* argv[]) -> int {
+ auto parsed = cfbox::args::parse(argc, argv,
+ {
+ cfbox::args::OptSpec{'a', false, "all"},
+ cfbox::args::OptSpec{'t', true, "types"},
+ cfbox::args::OptSpec{'o', true, "options"},
+ cfbox::args::OptSpec{'r', false, "read-only"},
+ });
+
+ if (parsed.has_long("help")) {
+ cfbox::help::print_help(HELP);
+ return 0;
+ }
+ if (parsed.has_long("version")) {
+ cfbox::help::print_version(HELP);
+ return 0;
+ }
+
+ bool all = parsed.has('a');
+ bool ro = parsed.has('r');
+ auto type = parsed.get('t');
+ auto opts = parsed.get('o');
+ const auto& pos = parsed.positional();
+
+ std::string opts_str = opts ? std::string(*opts) : std::string{};
+ if (ro)
+ opts_str = opts_str.empty() ? std::string("ro") : "ro," + opts_str;
+
+ if (all) {
+ auto entries = parse_fstab("/etc/fstab");
+ if (entries.empty()) {
+ CFBOX_ERR("mount", "no usable entries in /etc/fstab");
+ return 1;
+ }
+ int rc = 0;
+ for (const auto& e : entries) {
+ if (mount_one(e.fs, e.mountpoint, e.type, e.options) != 0)
+ rc = 1;
+ }
+ return rc;
+ }
+
+ // single mount: SOURCE TARGET
+ if (pos.size() < 2) {
+ cfbox::help::print_help(HELP);
+ return 2;
+ }
+ std::string type_str = type ? std::string(*type) : std::string{};
+ return mount_one(pos[0], pos[1], type_str, opts_str);
+}
diff --git a/src/applets/reboot.cpp b/src/applets/reboot.cpp
new file mode 100644
index 0000000..8ac4ef9
--- /dev/null
+++ b/src/applets/reboot.cpp
@@ -0,0 +1,69 @@
+#include
+#include
+
+#include
+#include
+#include
+
+namespace {
+
+constexpr cfbox::help::HelpEntry HELP_REBOOT = {
+ .name = "reboot",
+ .version = CFBOX_VERSION_STRING,
+ .one_line = "reboot the system",
+ .usage = "reboot",
+ .options = "",
+ .extra = "",
+};
+
+constexpr cfbox::help::HelpEntry HELP_POWEROFF = {
+ .name = "poweroff",
+ .version = CFBOX_VERSION_STRING,
+ .one_line = "power off the system",
+ .usage = "poweroff",
+ .options = "",
+ .extra = "",
+};
+
+} // namespace
+
+auto reboot_main(int argc, char* argv[]) -> int {
+ // Honor --help/--version before touching the reboot syscall.
+ for (int i = 1; i < argc; ++i) {
+ std::string_view a{argv[i]};
+ if (a == "--help") {
+ cfbox::help::print_help(HELP_REBOOT);
+ return 0;
+ }
+ if (a == "--version") {
+ cfbox::help::print_version(HELP_REBOOT);
+ return 0;
+ }
+ }
+ sync(); // never reboot with dirty filesystems (gotcha #6)
+ if (reboot(RB_AUTOBOOT) != 0) {
+ CFBOX_ERR("reboot", "%s", std::strerror(errno));
+ return 1;
+ }
+ return 0; // unreachable on a successful reboot
+}
+
+auto poweroff_main(int argc, char* argv[]) -> int {
+ for (int i = 1; i < argc; ++i) {
+ std::string_view a{argv[i]};
+ if (a == "--help") {
+ cfbox::help::print_help(HELP_POWEROFF);
+ return 0;
+ }
+ if (a == "--version") {
+ cfbox::help::print_version(HELP_POWEROFF);
+ return 0;
+ }
+ }
+ sync();
+ if (reboot(RB_POWER_OFF) != 0) {
+ CFBOX_ERR("poweroff", "%s", std::strerror(errno));
+ return 1;
+ }
+ return 0; // unreachable on a successful poweroff
+}
diff --git a/src/applets/sh/sh_main.cpp b/src/applets/sh/sh_main.cpp
index b0e99b6..eaa24a6 100644
--- a/src/applets/sh/sh_main.cpp
+++ b/src/applets/sh/sh_main.cpp
@@ -1,30 +1,34 @@
#include "sh.hpp"
#include
+#include
#include
#include
#include
+#include
+#include
-#include
#include
+#include
namespace {
constexpr cfbox::help::HelpEntry HELP = {
- .name = "sh",
+ .name = "sh",
.version = CFBOX_VERSION_STRING,
.one_line = "POSIX shell command interpreter",
- .usage = "sh [-c command] [script [args...]]",
+ .usage = "sh [-c command] [script [args...]]",
.options = " -c CMD execute CMD string\n"
" -s read commands from stdin",
- .extra = "",
+ .extra = "",
};
auto run_string(const std::string& script, cfbox::sh::ShellState& state) -> int {
cfbox::sh::Lexer lexer(script);
cfbox::sh::Parser parser(lexer);
auto ast = parser.parse_program();
- if (ast) return cfbox::sh::execute(*ast, state);
+ if (ast)
+ return cfbox::sh::execute(*ast, state);
return 0;
}
@@ -43,7 +47,31 @@ auto run_file(const char* path, cfbox::sh::ShellState& state) -> int {
return run_string(script, state);
}
+// Force the controlling tty into canonical mode so the kernel handles line
+// editing (backspace, Ctrl-C). Without it a serial console echoes raw control
+// bytes. Restored when the guard goes out of scope — no leaked global state.
+struct CookedTermios {
+ int fd;
+ bool changed = false;
+ struct termios saved{};
+ explicit CookedTermios(int f) : fd(f) {
+ if (isatty(fd) && tcgetattr(fd, &saved) == 0) {
+ struct termios c = saved;
+ c.c_lflag |= ICANON | ECHO | ECHOE | ECHOK | ISIG;
+ c.c_iflag |= ICRNL | IXON;
+ c.c_oflag |= OPOST | ONLCR;
+ c.c_cc[VERASE] = 0x08; // treat Ctrl-H (BS) as erase; default VERASE is often DEL
+ changed = tcsetattr(fd, TCSANOW, &c) == 0;
+ }
+ }
+ ~CookedTermios() {
+ if (changed)
+ (void)tcsetattr(fd, TCSANOW, &saved);
+ }
+};
+
auto run_interactive(cfbox::sh::ShellState& state) -> int {
+ CookedTermios tty(STDIN_FILENO);
std::string line;
int last_rc = 0;
@@ -56,7 +84,8 @@ auto run_interactive(cfbox::sh::ShellState& state) -> int {
break;
}
- if (line.empty()) continue;
+ if (line.empty())
+ continue;
cfbox::sh::Lexer lexer(line);
cfbox::sh::Parser parser(lexer);
@@ -82,14 +111,21 @@ auto sh_main(int argc, char* argv[]) -> int {
for (int i = 1; i < argc; ++i) {
std::string_view arg{argv[i]};
- if (arg == "--help") { cfbox::help::print_help(HELP); return 0; }
- if (arg == "--version") { cfbox::help::print_version(HELP); return 0; }
+ if (arg == "--help") {
+ cfbox::help::print_help(HELP);
+ return 0;
+ }
+ if (arg == "--version") {
+ cfbox::help::print_version(HELP);
+ return 0;
+ }
if (arg == "-c" && i + 1 < argc) {
command = argv[++i];
first_positional = i + 1;
break; // remaining args after -c CMD are positional
} else if (arg == "-s") {
- from_stdin = true; (void)from_stdin;
+ from_stdin = true;
+ (void)from_stdin;
} else if (arg[0] != '-') {
script_arg = i;
break;
@@ -120,10 +156,16 @@ auto sh_main(int argc, char* argv[]) -> int {
state.set_script_name(argc > 0 ? argv[0] : "sh");
}
- // Set essential env vars
+ // Set essential env vars. cfbox init execs sh with only HOME/TERM, so PATH
+ // is usually empty here — fall back to a sane default and export it so child
+ // applets (which, ...) can resolve commands too.
if (state.get_var("PATH").empty()) {
const char* path = std::getenv("PATH");
- if (path) state.set_var("PATH", path);
+ if (!path || !*path) {
+ path = "/bin:/sbin:/usr/bin:/usr/sbin";
+ setenv("PATH", path, 1);
+ }
+ state.set_var("PATH", path);
}
if (!command.empty()) {
diff --git a/src/applets/swapoff.cpp b/src/applets/swapoff.cpp
new file mode 100644
index 0000000..b5e37d7
--- /dev/null
+++ b/src/applets/swapoff.cpp
@@ -0,0 +1,80 @@
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+namespace {
+
+constexpr cfbox::help::HelpEntry HELP = {
+ .name = "swapoff",
+ .version = CFBOX_VERSION_STRING,
+ .one_line = "disable swap",
+ .usage = "swapoff [-a] [DEVICE]",
+ .options = " -a disable all swap devices listed in /proc/swaps",
+ .extra = "",
+};
+
+// First whitespace-delimited token of a /proc/swaps line (the device path).
+auto first_token(const char* line) -> std::string {
+ std::string dev;
+ for (const char* p = line; *p && *p != ' ' && *p != '\t' && *p != '\n'; ++p)
+ dev += *p;
+ return dev;
+}
+
+} // namespace
+
+auto swapoff_main(int argc, char* argv[]) -> int {
+ auto parsed = cfbox::args::parse(argc, argv,
+ {
+ cfbox::args::OptSpec{'a', false, "all"},
+ });
+
+ if (parsed.has_long("help")) {
+ cfbox::help::print_help(HELP);
+ return 0;
+ }
+ if (parsed.has_long("version")) {
+ cfbox::help::print_version(HELP);
+ return 0;
+ }
+
+ if (parsed.has('a')) {
+ FILE* f = std::fopen("/proc/swaps", "r");
+ if (!f)
+ return 0; // no swap configured — nothing to do
+ char line[512];
+ if (!std::fgets(line, sizeof(line), f)) { // skip header; empty swaps file is fine
+ std::fclose(f);
+ return 0;
+ }
+ int rc = 0;
+ while (std::fgets(line, sizeof(line), f)) {
+ std::string dev = first_token(line);
+ if (dev.empty())
+ continue;
+ if (::swapoff(dev.c_str()) != 0) {
+ CFBOX_ERR("swapoff", "%s: %s", dev.c_str(), std::strerror(errno));
+ rc = 1;
+ }
+ }
+ std::fclose(f);
+ return rc;
+ }
+
+ const auto& pos = parsed.positional();
+ if (pos.empty()) {
+ cfbox::help::print_help(HELP);
+ return 2;
+ }
+ std::string dev(pos[0]);
+ if (::swapoff(dev.c_str()) != 0) {
+ CFBOX_ERR("swapoff", "%s: %s", dev.c_str(), std::strerror(errno));
+ return 1;
+ }
+ return 0;
+}
diff --git a/src/applets/umount.cpp b/src/applets/umount.cpp
new file mode 100644
index 0000000..4869dfc
--- /dev/null
+++ b/src/applets/umount.cpp
@@ -0,0 +1,83 @@
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace {
+
+constexpr cfbox::help::HelpEntry HELP = {
+ .name = "umount",
+ .version = CFBOX_VERSION_STRING,
+ .one_line = "unmount filesystems",
+ .usage = "umount [-a] [-r] [-f] [TARGET]",
+ .options = " -a unmount all (except / /proc /sys /dev)\n"
+ " -r remount read-only if unmount fails\n"
+ " -f force unmount (MNT_FORCE)",
+ .extra = "",
+};
+
+auto is_protected(const std::string& mp) -> bool {
+ return mp == "/" || mp == "/proc" || mp == "/sys" || mp == "/dev";
+}
+
+// Try to drop a mount; on failure, optionally fall back to a read-only remount.
+auto drop_one(const std::string& target, int flags, bool ro) -> int {
+ if (umount2(target.c_str(), flags) == 0)
+ return 0;
+ if (ro && mount(nullptr, target.c_str(), nullptr, MS_REMOUNT | MS_RDONLY, nullptr) == 0)
+ return 0;
+ CFBOX_ERR("umount", "%s: %s", target.c_str(), std::strerror(errno));
+ return 1;
+}
+
+} // namespace
+
+auto umount_main(int argc, char* argv[]) -> int {
+ auto parsed = cfbox::args::parse(argc, argv,
+ {
+ cfbox::args::OptSpec{'a', false, "all"},
+ cfbox::args::OptSpec{'r', false, "read-only"},
+ cfbox::args::OptSpec{'f', false, "force"},
+ });
+
+ if (parsed.has_long("help")) {
+ cfbox::help::print_help(HELP);
+ return 0;
+ }
+ if (parsed.has_long("version")) {
+ cfbox::help::print_version(HELP);
+ return 0;
+ }
+
+ bool all = parsed.has('a');
+ bool ro = parsed.has('r');
+ int flags = parsed.has('f') ? MNT_FORCE : 0;
+ const auto& pos = parsed.positional();
+
+ if (all) {
+ auto mounts = cfbox::proc::read_mounts();
+ if (!mounts) {
+ CFBOX_ERR("umount", "cannot read /proc/mounts");
+ return 1;
+ }
+ const auto& vec = *mounts;
+ int rc = 0;
+ for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
+ if (is_protected(it->mountpoint))
+ continue; // never drop the boot fs
+ if (drop_one(it->mountpoint, flags, ro) != 0)
+ rc = 1;
+ }
+ return rc;
+ }
+
+ if (pos.empty()) {
+ cfbox::help::print_help(HELP);
+ return 2;
+ }
+ return drop_one(std::string(pos[0]), flags, ro);
+}
diff --git a/tests/unit/test_init_inittab.cpp b/tests/unit/test_init_inittab.cpp
index 6803ac3..7b021aa 100644
--- a/tests/unit/test_init_inittab.cpp
+++ b/tests/unit/test_init_inittab.cpp
@@ -17,21 +17,26 @@ struct InittabEntry {
auto parse_inittab_line(std::string_view line) -> InittabEntry {
InittabEntry entry;
- if (line.empty() || line[0] == '#') return entry;
+ if (line.empty() || line[0] == '#')
+ return entry;
auto c1 = line.find(':');
- if (c1 == std::string_view::npos) return entry;
+ if (c1 == std::string_view::npos)
+ return entry;
auto c2 = line.find(':', c1 + 1);
- if (c2 == std::string_view::npos) return entry;
+ if (c2 == std::string_view::npos)
+ return entry;
auto c3 = line.find(':', c2 + 1);
- if (c3 == std::string_view::npos) return entry;
+ if (c3 == std::string_view::npos)
+ return entry;
entry.id = std::string(line.substr(0, c1));
entry.runlevels = std::string(line.substr(c1 + 1, c2 - c1 - 1));
entry.action = std::string(line.substr(c2 + 1, c3 - c2 - 1));
entry.process = std::string(line.substr(c3 + 1));
- while (!entry.process.empty() && (entry.process.back() == ' ' || entry.process.back() == '\t' || entry.process.back() == '\r'))
+ while (!entry.process.empty() && (entry.process.back() == ' ' || entry.process.back() == '\t' ||
+ entry.process.back() == '\r'))
entry.process.pop_back();
return entry;
@@ -88,3 +93,14 @@ TEST(InittabTest, ParseShutdown) {
EXPECT_EQ(entry.action, "shutdown");
EXPECT_EQ(entry.process, "/bin/umount -a");
}
+
+// imx-forge console getty: the leading '-' on the process means "give me the
+// controlling console"; the parser must preserve it verbatim so init can strip
+// it before exec (see init_spawn.cpp).
+TEST(InittabTest, ParseAskfirst) {
+ auto entry = parse_inittab_line("console::askfirst:-/bin/sh");
+ EXPECT_EQ(entry.id, "console");
+ EXPECT_EQ(entry.runlevels, "");
+ EXPECT_EQ(entry.action, "askfirst");
+ EXPECT_EQ(entry.process, "-/bin/sh");
+}
diff --git a/tests/unit/test_mdev.cpp b/tests/unit/test_mdev.cpp
new file mode 100644
index 0000000..d215757
--- /dev/null
+++ b/tests/unit/test_mdev.cpp
@@ -0,0 +1,30 @@
+#include
+#include
+#include
+
+#include "test_capture.hpp"
+
+#if CFBOX_ENABLE_MDEV
+
+TEST(MdevTest, HelpPrintsUsage) {
+ char a0[] = "mdev", a1[] = "--help";
+ char* argv[] = {a0, a1, nullptr};
+ auto out = cfbox::test::capture_stdout([&] { return mdev_main(2, argv); });
+ EXPECT_NE(out.find("mdev"), std::string::npos);
+}
+
+TEST(MdevTest, VersionExitsZero) {
+ char a0[] = "mdev", a1[] = "--version";
+ char* argv[] = {a0, a1, nullptr};
+ EXPECT_EQ(mdev_main(2, argv), 0);
+}
+
+// Without -s, mdev is in hotplug mode which cfbox does not implement;
+// it must exit cleanly (imx-forge only ever runs `mdev -s`).
+TEST(MdevTest, NoScanExitsCleanly) {
+ char a0[] = "mdev";
+ char* argv[] = {a0, nullptr};
+ EXPECT_EQ(mdev_main(1, argv), 0);
+}
+
+#endif // CFBOX_ENABLE_MDEV
diff --git a/tests/unit/test_mount.cpp b/tests/unit/test_mount.cpp
new file mode 100644
index 0000000..33b195f
--- /dev/null
+++ b/tests/unit/test_mount.cpp
@@ -0,0 +1,36 @@
+#include
+#include
+#include
+
+#include "test_capture.hpp"
+
+#if CFBOX_ENABLE_MOUNT
+
+TEST(MountTest, HelpPrintsUsage) {
+ char a0[] = "mount", a1[] = "--help";
+ char* argv[] = {a0, a1, nullptr};
+ auto out = cfbox::test::capture_stdout([&] { return mount_main(2, argv); });
+ EXPECT_NE(out.find("mount"), std::string::npos);
+}
+
+TEST(MountTest, VersionExitsZero) {
+ char a0[] = "mount", a1[] = "--version";
+ char* argv[] = {a0, a1, nullptr};
+ EXPECT_EQ(mount_main(2, argv), 0);
+}
+
+// No SOURCE TARGET pair => usage error, not a syscall attempt.
+TEST(MountTest, NoArgsIsUsageError) {
+ char a0[] = "mount";
+ char* argv[] = {a0, nullptr};
+ EXPECT_EQ(mount_main(1, argv), 2);
+}
+
+// Only one positional (missing TARGET) => usage error.
+TEST(MountTest, SinglePositionalIsUsageError) {
+ char a0[] = "mount", a1[] = "-t", a2[] = "proc", a3[] = "/proc";
+ char* argv[] = {a0, a1, a2, a3, nullptr};
+ EXPECT_EQ(mount_main(4, argv), 2);
+}
+
+#endif // CFBOX_ENABLE_MOUNT
diff --git a/tests/unit/test_reboot.cpp b/tests/unit/test_reboot.cpp
new file mode 100644
index 0000000..1b2615f
--- /dev/null
+++ b/tests/unit/test_reboot.cpp
@@ -0,0 +1,39 @@
+#include
+#include
+#include
+
+#include "test_capture.hpp"
+
+// NOTE: reboot/poweroff must only be exercised through --help/--version here;
+// a bare call invokes the reboot(2) syscall and would actually reboot the
+// machine. Real reboot/poweroff behavior is verified end-to-end on a board.
+
+#if CFBOX_ENABLE_REBOOT
+
+TEST(RebootTest, HelpPrintsUsage) {
+ char a0[] = "reboot", a1[] = "--help";
+ char* argv[] = {a0, a1, nullptr};
+ auto out = cfbox::test::capture_stdout([&] { return reboot_main(2, argv); });
+ EXPECT_NE(out.find("reboot"), std::string::npos);
+}
+
+TEST(RebootTest, VersionExitsZero) {
+ char a0[] = "reboot", a1[] = "--version";
+ char* argv[] = {a0, a1, nullptr};
+ EXPECT_EQ(reboot_main(2, argv), 0);
+}
+
+TEST(PoweroffTest, HelpPrintsUsage) {
+ char a0[] = "poweroff", a1[] = "--help";
+ char* argv[] = {a0, a1, nullptr};
+ auto out = cfbox::test::capture_stdout([&] { return poweroff_main(2, argv); });
+ EXPECT_NE(out.find("poweroff"), std::string::npos);
+}
+
+TEST(PoweroffTest, VersionExitsZero) {
+ char a0[] = "poweroff", a1[] = "--version";
+ char* argv[] = {a0, a1, nullptr};
+ EXPECT_EQ(poweroff_main(2, argv), 0);
+}
+
+#endif // CFBOX_ENABLE_REBOOT
diff --git a/tests/unit/test_swapoff.cpp b/tests/unit/test_swapoff.cpp
new file mode 100644
index 0000000..36d7e32
--- /dev/null
+++ b/tests/unit/test_swapoff.cpp
@@ -0,0 +1,29 @@
+#include
+#include
+#include
+
+#include "test_capture.hpp"
+
+#if CFBOX_ENABLE_SWAPOFF
+
+TEST(SwapoffTest, HelpPrintsUsage) {
+ char a0[] = "swapoff", a1[] = "--help";
+ char* argv[] = {a0, a1, nullptr};
+ auto out = cfbox::test::capture_stdout([&] { return swapoff_main(2, argv); });
+ EXPECT_NE(out.find("swapoff"), std::string::npos);
+}
+
+TEST(SwapoffTest, VersionExitsZero) {
+ char a0[] = "swapoff", a1[] = "--version";
+ char* argv[] = {a0, a1, nullptr};
+ EXPECT_EQ(swapoff_main(2, argv), 0);
+}
+
+// No device and no -a => usage error, not a syscall attempt.
+TEST(SwapoffTest, NoDeviceIsUsageError) {
+ char a0[] = "swapoff";
+ char* argv[] = {a0, nullptr};
+ EXPECT_EQ(swapoff_main(1, argv), 2);
+}
+
+#endif // CFBOX_ENABLE_SWAPOFF
diff --git a/tests/unit/test_umount.cpp b/tests/unit/test_umount.cpp
new file mode 100644
index 0000000..8222fc5
--- /dev/null
+++ b/tests/unit/test_umount.cpp
@@ -0,0 +1,29 @@
+#include
+#include
+#include
+
+#include "test_capture.hpp"
+
+#if CFBOX_ENABLE_UMOUNT
+
+TEST(UmountTest, HelpPrintsUsage) {
+ char a0[] = "umount", a1[] = "--help";
+ char* argv[] = {a0, a1, nullptr};
+ auto out = cfbox::test::capture_stdout([&] { return umount_main(2, argv); });
+ EXPECT_NE(out.find("umount"), std::string::npos);
+}
+
+TEST(UmountTest, VersionExitsZero) {
+ char a0[] = "umount", a1[] = "--version";
+ char* argv[] = {a0, a1, nullptr};
+ EXPECT_EQ(umount_main(2, argv), 0);
+}
+
+// No target and no -a => usage error, not a syscall attempt.
+TEST(UmountTest, NoTargetIsUsageError) {
+ char a0[] = "umount";
+ char* argv[] = {a0, nullptr};
+ EXPECT_EQ(umount_main(1, argv), 2);
+}
+
+#endif // CFBOX_ENABLE_UMOUNT