From cc6d0f989d6e8c21c684f459d9f5a43d160f6f0c Mon Sep 17 00:00:00 2001 From: randogoth Date: Sun, 19 Oct 2025 18:01:06 +0300 Subject: [PATCH] Add configurable DOS drive volumes and tests --- Dockerfile | 6 +- README.md | 9 +++ compose.yml | 1 + dos_drives/.gitignore | 2 + scripts/dos-shell | 93 +++++++++++++++++++++++- tests/run-dos-shell-tests.sh | 134 +++++++++++++++++++++++++++++++++++ 6 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 dos_drives/.gitignore create mode 100644 tests/run-dos-shell-tests.sh diff --git a/Dockerfile b/Dockerfile index 4facce9..5807d07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,9 +36,9 @@ RUN useradd -m -s /usr/local/bin/dos-shell dosuser && \ mkdir -p /home/dosuser/.ssh && chown -R dosuser:dosuser /home/dosuser && \ echo "dosuser:dosuser" | chpasswd -# Create directories for allowed DOS files and the C: drive mount -RUN mkdir -p /opt/allowed_repo /cdrive /etc/dos_env /opt/svardos && \ - chown -R dosuser:dosuser /opt/allowed_repo /cdrive +# Create directories for allowed DOS files, extra drives, and the C: drive mount +RUN mkdir -p /opt/allowed_repo /opt/dos_drives /cdrive /etc/dos_env /opt/svardos && \ + chown -R dosuser:dosuser /opt/allowed_repo /opt/dos_drives /cdrive # Download and stage SvarDOS base files ARG SVARDOS_IMG_URL diff --git a/README.md b/README.md index 2cabf7d..6e35b21 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ All persistent user data inside the guest lives under `/home/dosuser/.dosemu`, w - **`config/dos_allowed`** – list relative paths (from `allowed_repo/`) to permit when `DOS_ALLOW_MODE=list`. With `DOS_ALLOW_MODE=all` every file in the repo is staged. - **`dos_env/` templates** – place `AUTOEXEC.BAT` and/or `CONFIG.SYS` to control boot scripts. - **`dos_home/` volume** – bind-mounted to `/home/dosuser`; persists the DOS user profile, `.dosemu` state, SSH keys, etc. +- **`dos_drives/` volume** – subdirectories named after drive letters (e.g. `F`, `G`) are exposed to DOS as additional drives. Letters `C`, `D`, and `E` are reserved by default (see `DOS_RESERVED_DRIVES` below); set that variable if you want to repurpose them. - **Pre-boot hook** – create an executable `dos_env/pre-boot.sh` (or point `DOS_PRE_BOOT_HOOK` at another path). It runs as `dosuser` immediately before dosemu starts, with helper environment variables (`C_DRIVE`, `DOSEMU_DIR`, `ALLOWED_REPO`, `SVARDOS_ROOT`, `SVARDOS_BASE`) so you can copy, delete, or patch files on the DOS drive. - **Forcing a reinstall** – set `DOS_FORCE_INSTALL=1` in the environment before logging in; the script re-seeds the drive from `/opt/svardos/base`. @@ -88,6 +89,10 @@ allowed_repo/ dos_home/ .ssh/ authorized_keys +dos_drives/ + F/ + demos/ + intro.exe dos_env/ AUTOEXEC.BAT CONFIG.SYS @@ -107,6 +112,10 @@ You can influence runtime behaviour with environment variables. Set them either | `DOS_TERMINAL_MODE` | `auto` | Determines whether `dosemu` launches with X11 (`-X`), terminal (`-td`) or `-dumb`. | | `DOS_ENV_DIR` | defaults to `/etc/dos_env` | Override if you mount templates somewhere else. | | `DOS_PRE_BOOT_HOOK` | `${DOS_ENV_DIR}/pre-boot.sh` | Custom shell script to run (as `dosuser`) before launching dosemu. Must be executable. | +| `DOS_EXTRA_DRIVE_ROOT` | `/opt/dos_drives` | Override where `dos-shell` looks for extra drive directories (one per drive letter). | +| `DOS_RESERVED_DRIVES` | `CDE` | Letters to leave untouched when mapping extra drives (remove or change to claim `D:`/`E:` etc.). | +| `DOS_ALLOWED_REPO` | `/opt/allowed_repo` | Alternate root for the allowed-repo staging area (useful in tests or custom mounts). | +| `DOS_ALLOWED_LIST` | `/etc/dos_allowed` | Path to the allowlist file consumed when `DOS_ALLOW_MODE=list`. | | `SVARDOS_ROOT`/`SVARDOS_BASE` | default `/opt/svardos` | Changes where the base image lives (mostly useful during debugging). | | `AO_DRIVER` | auto-set to `null` when sound is muted | You can override to force libao to a specific backend. | diff --git a/compose.yml b/compose.yml index 392beaf..038a8e9 100644 --- a/compose.yml +++ b/compose.yml @@ -19,4 +19,5 @@ services: - ./config/dos_allowed:/etc/dos_allowed:ro - ./dos_env:/etc/dos_env - ./dos_home:/home/dosuser + - ./dos_drives:/opt/dos_drives restart: unless-stopped diff --git a/dos_drives/.gitignore b/dos_drives/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/dos_drives/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/scripts/dos-shell b/scripts/dos-shell index 6e41201..973efd1 100755 --- a/scripts/dos-shell +++ b/scripts/dos-shell @@ -4,8 +4,10 @@ set -euo pipefail -ALLOWED_LIST="/etc/dos_allowed" -ALLOWED_REPO="/opt/allowed_repo" +ALLOWED_LIST="${DOS_ALLOWED_LIST:-/etc/dos_allowed}" +ALLOWED_REPO="${DOS_ALLOWED_REPO:-/opt/allowed_repo}" +EXTRA_DRIVE_ROOT="${DOS_EXTRA_DRIVE_ROOT:-/opt/dos_drives}" +RESERVED_DRIVES_RAW="${DOS_RESERVED_DRIVES:-CDE}" ENV_DIR="/etc/dos_env" AUTOEXEC_TEMPLATE="${ENV_DIR}/AUTOEXEC.BAT" CONFIG_TEMPLATE="${ENV_DIR}/CONFIG.SYS" @@ -113,6 +115,8 @@ detect_dosemu_args() { esac } allowed_entries=() +extra_drive_logs=() +declare -A reserved_letters=() mkdir -p "${DOSEMU_DIR}" "${C_DRIVE}" if [ "$(id -u)" -eq 0 ] && [ ! -d "${ENV_DIR}" ]; then @@ -140,6 +144,19 @@ reset_drive_c() { echo "SvarDOS base staged to ${C_DRIVE}" } +normalise_reserved_drives() { + local raw="$1" + local cleaned="${raw//[^[:alpha:]]/}" + local i letter + for ((i=0; i<${#cleaned}; i++)); do + letter="${cleaned:i:1}" + letter="${letter,,}" + reserved_letters["${letter}"]=1 + done +} + +normalise_reserved_drives "${RESERVED_DRIVES_RAW}" + sync_allowed_content() { allowed_entries=() local mode="${ALLOW_MODE}" @@ -179,6 +196,70 @@ sync_allowed_content() { done } +sync_extra_drives() { + extra_drive_logs=() + local root="${EXTRA_DRIVE_ROOT}" + local -A seen_letters=() + + if [ -d "${root}" ]; then + shopt -s nullglob + local entry base letter letter_lower letter_upper dest + for entry in "${root}"/*; do + if [ ! -d "$entry" ]; then + continue + fi + base="$(basename "$entry")" + case "$base" in + [A-Za-z]|[A-Za-z]:) + letter="${base:0:1}" + ;; + *) + continue + ;; + esac + letter_lower="${letter,,}" + letter_upper="${letter^^}" + if [ -n "${reserved_letters[${letter_lower}]+x}" ]; then + echo "dos-shell: skipping extra drive '${base}' (${letter_upper}: is reserved)" >&2 + continue + fi + dest="${DOSEMU_DIR}/drive_${letter_lower}" + if [ -e "${dest}" ] && [ ! -L "${dest}" ]; then + echo "dos-shell: cannot map ${letter_upper}: - ${dest} already exists and is not a symlink" >&2 + continue + fi + if ! ln -sfn "$entry" "$dest"; then + echo "dos-shell: failed to link ${letter_upper}: to ${entry}" >&2 + continue + fi + seen_letters["${letter_lower}"]=1 + extra_drive_logs+=("${letter_upper}: ${entry}") + done + shopt -u nullglob + fi + + shopt -s nullglob + local link base letter target + for link in "${DOSEMU_DIR}"/drive_[[:lower:]]; do + [ -L "$link" ] || continue + base="$(basename "$link")" + letter="${base#drive_}" + if [ -z "$letter" ] || [ "$letter" = "c" ]; then + continue + fi + if [ -n "${seen_letters[$letter]+x}" ]; then + continue + fi + target="$(readlink "$link")" + case "$target" in + "${EXTRA_DRIVE_ROOT}"/*) + rm -f "$link" + ;; + esac + done + shopt -u nullglob +} + apply_templates() { if [ -f "${AUTOEXEC_TEMPLATE}" ]; then cp "${AUTOEXEC_TEMPLATE}" "${C_DRIVE}/AUTOEXEC.BAT" @@ -364,6 +445,7 @@ else fi sync_allowed_content +sync_extra_drives apply_templates run_pre_boot_hook write_autoconfig @@ -381,6 +463,13 @@ elif [ "${#allowed_entries[@]}" -gt 0 ]; then done fi +if [ "${#extra_drive_logs[@]}" -gt 0 ]; then + echo "Additional DOS drives mapped:" + for mapping in "${extra_drive_logs[@]}"; do + echo " ${mapping}" + done +fi + if [ "$(id -u)" -eq 0 ]; then exec runuser -u "${DOS_USER}" -- dosemu "${DOSEMU_ARGS[@]}" else diff --git a/tests/run-dos-shell-tests.sh b/tests/run-dos-shell-tests.sh new file mode 100644 index 0000000..a447533 --- /dev/null +++ b/tests/run-dos-shell-tests.sh @@ -0,0 +1,134 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DOS_SHELL="${ROOT_DIR}/scripts/dos-shell" + +if [ ! -x "${DOS_SHELL}" ]; then + echo "dos-shell not found or not executable at ${DOS_SHELL}" >&2 + exit 1 +fi + +tmp_root="$(mktemp -d)" +cleanup() { + rm -rf "${tmp_root}" +} +trap cleanup EXIT + +stub_bin="${tmp_root}/bin" +mkdir -p "${stub_bin}" + +cat > "${stub_bin}/dosemu" <<'EOF' +#!/bin/sh +echo "dosemu stub invoked with: $*" >&2 +exit 0 +EOF +chmod +x "${stub_bin}/dosemu" + +run_case() { + local name="$1" + local allow_mode="$2" + local reserved="$3" + local expected_files="$4" + shift 4 + local asserts=("$@") + + local case_dir="${tmp_root}/${name}" + local home_dir="${case_dir}/home" + local base_dir="${case_dir}/svardos_base" + local allowed_repo="${case_dir}/allowed_repo" + local allow_list="${case_dir}/allowed_list" + local extra_root="${case_dir}/extra_drives" + + mkdir -p "${home_dir}" "${base_dir}" "${allowed_repo}" "${extra_root}" + + # Populate minimal base image + echo "base-${name}" > "${base_dir}/BASE.TXT" + + # Populate allowed repo fixtures + echo "common-${name}" > "${allowed_repo}/common.txt" + mkdir -p "${allowed_repo}/subdir" + echo "sub-${name}" > "${allowed_repo}/subdir/sub.txt" + echo "denied-${name}" > "${allowed_repo}/denied.txt" + + # Prepare allow list (filled later per mode) + : > "${allow_list}" + + # Create extra drive scaffolding + mkdir -p "${extra_root}/F" + echo "driveF-${name}" > "${extra_root}/F/file.txt" + mkdir -p "${extra_root}/D" + echo "driveD-${name}" > "${extra_root}/D/file.txt" + + # Additional file for list mode + if [ "${allow_mode}" = "list" ]; then + { + echo "common.txt" + echo "subdir/sub.txt" + } > "${allow_list}" + fi + + # Expected files in C: after sync + IFS=',' read -ra expected_array <<< "${expected_files}" + + ( + export PATH="${stub_bin}:${PATH}" + export HOME="${home_dir}" + export SVARDOS_ROOT="${case_dir}/svardos_root" + export SVARDOS_BASE="${base_dir}" + export DOS_FORCE_INSTALL=1 + export DOS_ALLOW_MODE="${allow_mode}" + export DOS_EXTRA_DRIVE_ROOT="${extra_root}" + export DOS_RESERVED_DRIVES="${reserved}" + export DOS_ALLOWED_REPO="${allowed_repo}" + export DOS_ALLOWED_LIST="${allow_list}" + "${DOS_SHELL}" >/dev/null + ) + + local c_drive="${home_dir}/.dosemu/drive_c" + + for rel_path in "${expected_array[@]}"; do + if [ ! -f "${c_drive}/${rel_path}" ]; then + echo "[${name}] expected file missing in C: ${rel_path}" >&2 + exit 1 + fi + done + + for assert in "${asserts[@]}"; do + case "${assert}" in + expect_symlink:*) + local letter="${assert#expect_symlink:}" + local link="${home_dir}/.dosemu/drive_${letter,,}" + if [ ! -L "${link}" ]; then + echo "[${name}] expected symlink for drive ${letter^^}: not found" >&2 + exit 1 + fi + ;; + expect_absent:*) + local letter="${assert#expect_absent:}" + local link="${home_dir}/.dosemu/drive_${letter,,}" + if [ -e "${link}" ]; then + echo "[${name}] expected drive ${letter^^}: to be absent, found $(ls -ld "${link}")" >&2 + exit 1 + fi + ;; + expect_no_file:*) + local rel="${assert#expect_no_file:}" + if [ -e "${c_drive}/${rel}" ]; then + echo "[${name}] unexpected file present in C: ${rel}" >&2 + exit 1 + fi + ;; + esac + done + + echo "[${name}] OK" +} + +run_case "allow-all" "all" "CDE" "BASE.TXT,common.txt,subdir/sub.txt,denied.txt" \ + "expect_symlink:f" "expect_absent:d" + +run_case "allow-list-reserved-override" "list" "C" "BASE.TXT,common.txt,subdir/sub.txt" \ + "expect_symlink:f" "expect_symlink:d" "expect_no_file:denied.txt" + +echo "All tests passed."