|
#!/bin/sh |
|
|
|
# ============================================================================== |
|
# POSIX Bencode to JSON Converter (Stream Optimized) |
|
# ============================================================================== |
|
# - Fixed indentation (Recursion safety) |
|
# - Fixed "pieces" freeze (O(N) streaming instead of O(N^2) buffering) |
|
# - Validates on strictly POSIX shells (dash, ash, etc.) |
|
# ============================================================================== |
|
|
|
set -e |
|
|
|
# State variable: Current Hex Byte |
|
HEX="" |
|
# State variable: Last captured dictionary key |
|
LAST_KEY="" |
|
# State variable: Flag to capture next string as key |
|
CAPTURE_KEY=0 |
|
|
|
# Read next byte from pipeline |
|
consume() { |
|
if ! read -r HEX; then |
|
HEX="" |
|
fi |
|
} |
|
|
|
# Print a single hex byte as JSON-safe char |
|
# $1: The hex byte (e.g., "61") |
|
print_byte() { |
|
_pb_val=$((0x$1)) |
|
|
|
# Check for JSON special chars |
|
case "$1" in |
|
22) printf '\\"' ;; # " |
|
5c) printf '\\\\' ;; # \ |
|
08) printf '\\b' ;; |
|
0c) printf '\\f' ;; |
|
0a) printf '\\n' ;; |
|
0d) printf '\\r' ;; |
|
09) printf '\\t' ;; |
|
*) |
|
# Printable ASCII (32-126) |
|
if [ "$_pb_val" -ge 32 ] && [ "$_pb_val" -le 126 ]; then |
|
# Convert octal to char |
|
printf "%b" "\\$(printf %03o "$_pb_val")" |
|
else |
|
# Fallback to unicode escape for safety |
|
printf "\\\\u00%s" "$1" |
|
fi |
|
;; |
|
esac |
|
} |
|
|
|
# Recursive Parser |
|
# $1: Indentation Level (int) |
|
# $2: Mode (1=Print, 0=Skip) |
|
parse() { |
|
# In POSIX sh, $1 and $2 are local to the function call. |
|
# We use them directly to avoid global variable corruption. |
|
|
|
# Generate Indent String |
|
_indent="" |
|
_i=0 |
|
while [ "$_i" -lt "$1" ]; do |
|
_indent="${_indent} " |
|
_i=$((_i + 1)) |
|
done |
|
|
|
# Loop until end of object or stream |
|
while [ -n "$HEX" ]; do |
|
case "$HEX" in |
|
# 'd' Dictionary |
|
64) |
|
consume |
|
if [ "$2" -eq 1 ]; then echo "{"; fi |
|
|
|
_first=1 |
|
while [ "$HEX" != "65" ] && [ -n "$HEX" ]; do |
|
if [ "$_first" -eq 0 ] && [ "$2" -eq 1 ]; then echo ","; fi |
|
_first=0 |
|
|
|
if [ "$2" -eq 1 ]; then printf "%s" "$_indent "; fi |
|
|
|
# --- Parse Key --- |
|
CAPTURE_KEY=1 |
|
parse $(($1 + 1)) "$2" |
|
CAPTURE_KEY=0 |
|
|
|
if [ "$2" -eq 1 ]; then printf ": "; fi |
|
|
|
# --- Parse Value --- |
|
# Check if the key we just parsed was "pieces" (hex 706965636573) |
|
if [ "$LAST_KEY" = "706965636573" ]; then |
|
# Skip mode: Recurse with Mode=0 |
|
parse $(($1 + 1)) 0 |
|
if [ "$2" -eq 1 ]; then printf '"<skipped binary pieces>"'; fi |
|
else |
|
# Normal mode |
|
parse $(($1 + 1)) "$2" |
|
fi |
|
done |
|
consume # eat 'e' |
|
|
|
if [ "$2" -eq 1 ]; then printf "\n%s}" "$_indent"; fi |
|
return |
|
;; |
|
|
|
# 'l' List |
|
6c) |
|
consume |
|
if [ "$2" -eq 1 ]; then echo "["; fi |
|
|
|
_first=1 |
|
while [ "$HEX" != "65" ] && [ -n "$HEX" ]; do |
|
if [ "$_first" -eq 0 ] && [ "$2" -eq 1 ]; then echo ","; fi |
|
_first=0 |
|
|
|
if [ "$2" -eq 1 ]; then printf "%s" "$_indent "; fi |
|
parse $(($1 + 1)) "$2" |
|
done |
|
consume # eat 'e' |
|
|
|
if [ "$2" -eq 1 ]; then printf "\n%s]" "$_indent"; fi |
|
return |
|
;; |
|
|
|
# 'i' Integer |
|
69) |
|
consume |
|
_int_hex="" |
|
while [ "$HEX" != "65" ]; do |
|
_int_hex="${_int_hex}${HEX}" |
|
consume |
|
done |
|
consume # eat 'e' |
|
|
|
# Decode integer (hex string -> ascii) |
|
if [ "$2" -eq 1 ]; then |
|
_int_val="" |
|
while [ -n "$_int_hex" ]; do |
|
_byte="${_int_hex%${_int_hex#??}}" |
|
_int_hex="${_int_hex#??}" |
|
_int_val="${_int_val}$(printf "%b" "\\$(printf %03o $((0x$_byte)))")" |
|
done |
|
printf "%s" "$_int_val" |
|
fi |
|
return |
|
;; |
|
|
|
# String (starts with digit) |
|
3[0-9]) |
|
# 1. Parse Length |
|
_len_hex="" |
|
while [ "$HEX" != "3a" ]; do |
|
_len_hex="${_len_hex}${HEX}" |
|
consume |
|
done |
|
consume # eat ':' |
|
|
|
# Decode length string to number |
|
_len_dec="" |
|
while [ -n "$_len_hex" ]; do |
|
_byte="${_len_hex%${_len_hex#??}}" |
|
_len_hex="${_len_hex#??}" |
|
_len_dec="${_len_dec}$(printf "%b" "\\$(printf %03o $((0x$_byte)))")" |
|
done |
|
|
|
# 2. Consume Content |
|
if [ "$2" -eq 1 ]; then printf '"'; fi |
|
|
|
_buf="" |
|
_k=0 |
|
while [ "$_k" -lt "$_len_dec" ]; do |
|
# If printing, stream to stdout (don't buffer) |
|
if [ "$2" -eq 1 ]; then |
|
print_byte "$HEX" |
|
fi |
|
|
|
# If capturing key, buffer it (keys are short) |
|
if [ "$CAPTURE_KEY" -eq 1 ]; then |
|
_buf="${_buf}${HEX}" |
|
fi |
|
|
|
consume |
|
_k=$((_k + 1)) |
|
done |
|
|
|
if [ "$2" -eq 1 ]; then printf '"'; fi |
|
|
|
# Update LAST_KEY global |
|
if [ "$CAPTURE_KEY" -eq 1 ]; then |
|
LAST_KEY="$_buf" |
|
fi |
|
return |
|
;; |
|
|
|
*) |
|
# Unknown/Error -> consume and return to avoid infinite loop |
|
consume |
|
return |
|
;; |
|
esac |
|
done |
|
} |
|
|
|
# --- Pipeline --- |
|
# od: convert binary to hex (one byte per line roughly) |
|
# tr: ensure strictly one hex byte per line (remove spaces) |
|
# grep: remove empty lines |
|
# loop: parse |
|
|
|
od -v -An -t x1 | tr -s ' \t' '\n' | grep . | ( |
|
consume # Prime the pump |
|
parse 0 1 |
|
echo "" |
|
) |