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 on i.MX6ULL: rcS → askfirst → console +

+ +> 上图:cfbox 作为 PID 1 启动 imx-forge rootfs —— 跑 `rcS`(mount/mdev),打印 `Please press Enter to activate this console.`(askfirst),回车进入 cfbox `sh`。 [![CI](https://github.com/Awesome-Embedded-Learning-Studio/CFBox/actions/workflows/ci.yml/badge.svg)](https://github.com/Awesome-Embedded-Learning-Studio/CFBox/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![C++23](https://img.shields.io/badge/C++23-00599C?logo=cplusplus)](https://en.cppreference.com/w/cpp/23) [![CMake](https://img.shields.io/badge/CMake-3.26+-064F8C?logo=cmake)](https://cmake.org/) -[![Tests](https://img.shields.io/badge/Tests-379_passing-brightgreen)](tests/) -[![Applets](https://img.shields.io/badge/Applets-115-brightgreen)](src/applets/) +[![Tests](https://img.shields.io/badge/Tests-399_passing-brightgreen)](tests/) +[![Applets](https://img.shields.io/badge/Applets-123-brightgreen)](src/applets/) +[![armhf](https://img.shields.io/badge/armhf-static-1.2MB-blue)](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` | + +

+ cat /proc/cpuinfo on cfbox: ARMv7 i.MX6ULL +

-## 概述 +实测:`/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 风格长选项、彩色帮助输出。 +

+ cfbox interactive demo +

-**设计理念:** 简洁优先 — 现代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