#!/bin/sh
#
# xchroot - Script to enter chroot environment with proper mounts
# POSIX-compliant version for production use
#
# Description:
#   This script safely enters a chroot environment by mounting all necessary
#   filesystems (/dev, /proc, /sys, /run, /tmp, /etc/resolv.conf) and
#   provides automatic cleanup on exit.
#
# Requirements:
#   - Root privileges
#   - Target directory must exist
#   - Standard Linux filesystem hierarchy
#
# Exit codes:
#   0 - Success
#   1 - Error (permission denied, invalid directory, mount failure)
#

# Display usage information
help() {
	cat << 'EOF'

Usage:
  xchroot <chroot-dir> [command]
  
If 'command' is unspecified, xchroot will launch /bin/sh.

Arguments:
  <chroot-dir>  Directory to use as new root filesystem
  [command]     Command to execute inside chroot (optional)

Description:
  This script mounts necessary filesystems and enters a chroot environment.
  It automatically mounts: /dev, /proc, /sys, /run, /tmp, and /etc/resolv.conf
  
Examples:
  xchroot /mnt/mysystem
  xchroot /mnt/mysystem portsync
  xchroot /mnt/mysystem scratch sysup -y
  xchroot /mnt/mysystem /bin/bash
  xchroot /mnt/mysystem /bin/mksh -c portsync

Notes:
  - If you want to run a command using a specific shell, use: shell -c 'command'
  - Arguments are passed directly to the command, preserving their structure

EOF
}

# Print error message to stderr
msgerr() {
	printf "ERROR: %s\n" "$*" >&2
}

# Print info message to stdout
msginfo() {
	printf "==> %s\n" "$*"
}

# Check for root privileges
if [ "$(id -u)" != "0" ]; then
	msgerr "This script requires root privileges"
	help
	exit 1
fi

# Unmount all filesystems in reverse order
# This function is called automatically on script exit via trap
# Note: Uses reverse order to avoid "device busy" errors
umountall() {
	# Iterate through mount points in reverse mount order
	for t in etc/resolv.conf tmp sys/firmware/efi/efivars sys run proc dev/shm dev/pts dev; do
		# Check if mountpoint exists and is mounted before attempting unmount
		if mountpoint -q "$TARGET/$t" 2>/dev/null; then
			# Try normal unmount first, fallback to lazy unmount
			umount "$TARGET/$t" 2>/dev/null || \
				umount -l "$TARGET/$t" 2>/dev/null
		fi
	done
}

# Extract script basename using parameter expansion
SCRIPT_NAME="${0##*/}"

# Validate that target directory argument is provided
if [ -z "$1" ]; then
	msgerr "Please specify a directory for chroot"
	help
	exit 1
fi

# Remove trailing slash from directory path if present
TARGET="${1%/}"

# Verify target directory exists
if [ ! -d "$TARGET" ]; then
	msgerr "Directory '$TARGET' does not exist"
	help
	exit 1
fi

# Optional check: inform if target is already a mount point
if mountpoint -q "$TARGET" 2>/dev/null; then
	msginfo "Target '$TARGET' is already a mount point"
fi

# Shift to process command arguments
shift

# Determine command to execute in chroot
if [ -z "$1" ]; then
	set -- /bin/sh
fi

# At this point $@ contains the command and all its arguments
# This preserves argument separation for commands like: ls -la /tmp

# Install trap handlers BEFORE mounting to ensure cleanup
# Using signal numbers for maximum POSIX portability:
#   0  = EXIT  - normal script exit
#   1  = HUP   - hangup (terminal closed)
#   2  = INT   - interrupt (Ctrl+C)
#   3  = QUIT  - quit signal (Ctrl+\)
#   15 = TERM  - termination signal (kill default)
trap umountall 0 1 2 3 15

msginfo "Mounting filesystems for chroot environment..."

# Mount all required filesystems using a loop with break pattern
# Benefits: single exit point, automatic cleanup via trap, less code repetition
# Pattern: while-true loop acts as POSIX "try-catch" mechanism
while true; do
	# Mount /dev - required for device access
	mount -o bind /dev "$TARGET/dev" || {
		msgerr "Failed to mount /dev"
		break
	}

	# Mount devpts - required for pseudo-terminals (PTY)
	# gid=5 typically corresponds to 'tty' group
	mount -t devpts devpts -o gid=5,mode=620 "$TARGET/dev/pts" || {
		msgerr "Failed to mount /dev/pts"
		break
	}

	# Mount /dev/shm - shared memory tmpfs
	# nosuid,nodev for security
	mount -t tmpfs tmpfs -o nosuid,nodev "$TARGET/dev/shm" || {
		msgerr "Failed to mount /dev/shm"
		break
	}

	# Mount /proc - process information pseudo-filesystem
	# Critical for most system tools
	mount -t proc proc "$TARGET/proc" || {
		msgerr "Failed to mount /proc"
		break
	}

	# Mount /run - runtime data tmpfs
	# Required for socket files, PIDs, etc.
	mount -t tmpfs tmpfs "$TARGET/run" || {
		msgerr "Failed to mount /run"
		break
	}

	# Mount /sys - kernel and device information
	# Critical for hardware interaction
	mount -t sysfs sysfs "$TARGET/sys" || {
		msgerr "Failed to mount /sys"
		break
	}

	# Mount EFI variables (optional, UEFI systems only)
	# Does not break loop on failure - not all systems have EFI
	mount -t efivarfs efivarfs "$TARGET/sys/firmware/efi/efivars" 2>/dev/null || \
		msginfo "EFI variables not mounted (may not be available)"

	# Mount /tmp - temporary files
	# mode=1777 = rwxrwxrwt (sticky bit for multi-user safety)
	mount -t tmpfs tmpfs -o mode=1777,nosuid,nodev "$TARGET/tmp" || {
		msgerr "Failed to mount /tmp"
		break
	}

	# If we reach here, all critical mounts succeeded
	# Break out of loop with success status
	break
done

# Post-loop verification: check if any mount failed
# $? contains exit status of last command (last mount or break)
if [ $? -ne 0 ]; then
	msgerr "Mount sequence failed, cleaning up..."
	exit 1
fi

# Handle DNS resolution configuration
if [ -f /etc/resolv.conf ]; then
	# Create empty resolv.conf in target if needed
	touch "$TARGET/etc/resolv.conf" 2>/dev/null || \
		msginfo "Could not create resolv.conf in chroot"
	
	# Bind mount host's resolv.conf for DNS resolution
	# Non-critical: doesn't fail if unsuccessful
	mount -o bind /etc/resolv.conf "$TARGET/etc/resolv.conf" 2>/dev/null || \
		msginfo "Could not bind mount resolv.conf"
fi

msginfo "Entering chroot environment: $TARGET"
msginfo "Executing: $*"

# Enter chroot with clean environment
# /usr/bin/env -i creates fresh environment with only specified variables
# Use exec "$@" to properly handle command with multiple arguments
# This preserves argument boundaries: ["ls", "-la", "/tmp"] not ["ls -la /tmp"]
chroot "$TARGET" /usr/bin/env -i \
	HOME=/root \
	TERM="$TERM" \
	PS1='\u:\w\$ ' \
	PATH=/bin:/usr/bin:/sbin:/usr/sbin \
	"$@"

# Capture chroot exit status immediately
# Must be done before any other command to preserve $?
retval=$?

# Trap will automatically call umountall on EXIT
# No need to call it manually here

msginfo "Exited chroot with status: $retval"

# Exit with same status as chrooted command
exit "$retval"
