Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 147 additions & 34 deletions scripts/maintenance/extract.sh
Original file line number Diff line number Diff line change
@@ -1,55 +1,168 @@
#!/usr/bin/env bash

set -euo pipefail

show_help() {
cat <<'EOF'
Usage: $(basename "$0") [OPTIONS] ARCHIVE [DEST]

Extract common archive types to a destination directory.
If DEST is omitted, extracts to the current directory.
cat <<'EOF'
Usage: extract.sh [options] <archive_file> [destination]

Options:
-h Show this help message
-h, --help Show this help message and exit
-l, --list List archive contents without extracting

Description:
Safely extracts common archive types (.tar, .tar.gz, .tgz, .tar.bz2,
.tbz2, .tar.xz, .txz, .zip, .7z, .rar) to the destination directory
(defaults to current directory).
Performs security checks to prevent directory traversal attacks (../)
and absolute path overwrites.
EOF
exit 0
}

if [[ "${1-}" == "-h" ]]; then
show_help
exit 0
fi
LIST_ONLY=false
ARCHIVE=""
DEST=""

if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "Error: Invalid number of arguments." >&2
show_help
exit 1
fi
while (( "$#" )); do
case "$1" in
-h|--help)
show_help
;;
-l|--list)
LIST_ONLY=true
;;
-*)
echo "Error: Unknown option: $1" >&2
exit 1
;;
*)
if [[ -z "$ARCHIVE" ]]; then
ARCHIVE="$1"
elif [[ -z "$DEST" ]]; then
DEST="$1"
else
echo "Error: Unexpected additional argument: $1" >&2
exit 1
fi
;;
esac
shift
done

ARCHIVE="$1"
DEST="${2:-.}"
if [[ -z "$ARCHIVE" ]]; then
echo "Error: Archive file required." >&2
echo "Run '$0 --help' for usage." >&2
exit 1
fi

if [[ ! -f "$ARCHIVE" ]]; then
echo "Error: Archive file '$ARCHIVE' does not exist." >&2
exit 1
echo "Error: Archive file '$ARCHIVE' does not exist." >&2
exit 1
fi

DEST="${DEST:-.}"

if [[ ! -d "$DEST" ]]; then
echo "Error: Destination '$DEST' is not a directory." >&2
exit 1
echo "Error: Destination '$DEST' is not a directory." >&2
exit 1
fi

# Determine archive type based on extension
# Detect format
FORMAT=""
case "$ARCHIVE" in
*.tar.gz|*.tgz) tar -xzf "$ARCHIVE" -C "$DEST" ;;
*.tar.bz2|*.tbz2) tar -xjf "$ARCHIVE" -C "$DEST" ;;
*.tar.xz|*.txz) tar -xJf "$ARCHIVE" -C "$DEST" ;;
*.zip) unzip -d "$DEST" "$ARCHIVE" ;;
*.rar) unrar x "$ARCHIVE" "$DEST" ;;
*.7z) 7z x "$ARCHIVE" -o"$DEST" ;;
*)
echo "Error: Unsupported archive format: $ARCHIVE" >&2
exit 1
;;
*.tar.gz|*.tgz) FORMAT="tar.gz" ;;
*.tar.bz2|*.tbz2) FORMAT="tar.bz2" ;;
*.tar.xz|*.txz) FORMAT="tar.xz" ;;
*.tar) FORMAT="tar" ;;
*.zip) FORMAT="zip" ;;
*.rar) FORMAT="rar" ;;
*.7z) FORMAT="7z" ;;
*)
echo "Error: Unsupported archive format: $ARCHIVE" >&2
exit 1
;;
esac

# Validate required tool
check_tool() {
local tool="$1"
if ! command -v "$tool" >/dev/null 2>&1; then
echo "Error: Required utility '$tool' is not installed." >&2
exit 1
fi
}

case "$FORMAT" in
tar*) check_tool tar ;;
zip) check_tool unzip ;;
rar) check_tool unrar ;;
7z)
if command -v 7z >/dev/null 2>&1; then
SEVEN_Z="7z"
elif command -v 7za >/dev/null 2>&1; then
SEVEN_Z="7za"
else
echo "Error: Required utility '7z' or '7za' is not installed." >&2
exit 1
fi
;;
esac

# Security check: verify no entries contain absolute paths or ../ traversal
validate_entries() {
local entry
while IFS= read -r entry; do
[[ -z "$entry" ]] && continue
if [[ "$entry" =~ ^/ ]] || [[ "$entry" =~ ^[a-zA-Z]: ]]; then
echo "Security Error: Archive contains absolute path entry: $entry" >&2
return 1
fi
if [[ "$entry" =~ (^|/)\.\.(/|$) ]]; then
echo "Security Error: Archive contains path traversal entry: $entry" >&2
return 1
fi
done
return 0
}

# List or check contents
list_entries() {
case "$FORMAT" in
tar*)
tar -tf "$ARCHIVE"
;;
zip)
unzip -Z -1 "$ARCHIVE"
;;
rar)
unrar lb "$ARCHIVE"
;;
7z)
"$SEVEN_Z" l -ba -slt "$ARCHIVE" | grep '^Path = ' | sed 's/^Path = //'
;;
esac
}

if $LIST_ONLY; then
list_entries
exit 0
fi

# Run traversal inspection
if ! list_entries | validate_entries; then
echo "Error: Extraction aborted due to security violations in archive." >&2
exit 1
fi

# Extract
case "$FORMAT" in
tar.gz) tar -xzf "$ARCHIVE" -C "$DEST" ;;
tar.bz2) tar -xjf "$ARCHIVE" -C "$DEST" ;;
tar.xz) tar -xJf "$ARCHIVE" -C "$DEST" ;;
tar) tar -xf "$ARCHIVE" -C "$DEST" ;;
zip) unzip -q -o -d "$DEST" "$ARCHIVE" ;;
rar) unrar x -o+ "$ARCHIVE" "$DEST/" ;;
7z) "$SEVEN_Z" x -y -o"$DEST" "$ARCHIVE" ;;
esac

echo "Extraction of '$ARCHIVE' completed."
echo "Extraction of '$ARCHIVE' into '$DEST' completed successfully."
Loading