Skip to content

Instantly share code, notes, and snippets.

@iwconfig
Last active December 8, 2025 01:06
Show Gist options
  • Select an option

  • Save iwconfig/f8d285f3017d911313fe026bcb84188a to your computer and use it in GitHub Desktop.

Select an option

Save iwconfig/f8d285f3017d911313fe026bcb84188a to your computer and use it in GitHub Desktop.
bencode decoder vibe coded in bash. it's slow :)

bdecode.sh is slightly faster than posix-bdecode.sh

Super crude benchmark:

/ # curl -s 'http://172.20.0.14:8080/info?ih=08ada5a7a6183aae1e09d831df6748d566095a10&path=Sintel.mp4' | time bash bdecode.sh 1>/dev/null
real    0m 0.81s
user    0m 0.56s
sys     0m 0.29s
/ # curl -s 'http://172.20.0.14:8080/info?ih=08ada5a7a6183aae1e09d831df6748d566095a10&path=Sintel.mp4' | time bash posix-bdecode.sh 1>/dev/null
real    0m 1.20s
user    0m 0.91s
sys     0m 0.34s
#!/bin/bash
# ==============================================================================
# Pure Bash Bencode to JSON Converter
# ==============================================================================
# Usage: curl ... | bash bdecode.sh
# Dependencies: od, tr, sed (Standard POSIX tools)
# ==============================================================================
set -e
# --- 1. Ingest Data ---
# Read stdin, convert to single-line hex string.
# We use 'od' to create a safe hex stream because Bash cannot hold null bytes.
HEX_STREAM=$(od -v -An -tx1 | tr -d ' \n')
HEX_LEN=${#HEX_STREAM}
POS=0
# --- 2. Helper Functions ---
# Get current byte (2 hex chars)
peek() {
echo "${HEX_STREAM:$POS:2}"
}
# Advance cursor by N bytes (N * 2 hex chars)
consume() {
POS=$((POS + $1 * 2))
}
# Convert hex string (e.g. "666f6f") to JSON string ("foo")
# Handles escaping for JSON format.
to_json_str() {
local h=$1
local len=${#h}
local res=""
# Iterate hex pairs
for (( i=0; i<len; i+=2 )); do
local pair="${h:$i:2}"
case "$pair" in
22) res+='\"' ;; # "
5c) res+='\\' ;; # \
08) res+='\b' ;; # BS
0c) res+='\f' ;; # FF
0a) res+='\n' ;; # NL
0d) res+='\r' ;; # CR
09) res+='\t' ;; # TAB
*) res+="\\x$pair" ;;
esac
done
# Expand the escaped sequence
printf "\"%b\"" "$res"
}
# Convert Hex-encoded ASCII digits to Integer
# Example: Input "3532" (Hex for "52") -> Output 52
hex_digits_to_int() {
local input_hex=$1
if [[ -z "$input_hex" ]]; then echo 0; return; fi
# 1. Insert \x before every 2 chars (3532 -> \x35\x32)
local escaped=$(echo "$input_hex" | sed 's/../\\x&/g')
# 2. printf %b converts \xHH to actual chars (digits)
printf "%b" "$escaped"
}
# --- 3. Recursive Parser ---
parse() {
local indent_level=$1
local indent_str=""
for (( k=0; k<indent_level; k++ )); do indent_str+=" "; done
if (( POS >= HEX_LEN )); then return; fi
local type=$(peek)
# --- Dictionary (d) ---
if [[ "$type" == "64" ]]; then
consume 1 # eat 'd'
echo "{"
local first=1
while true; do
local next=$(peek)
if [[ "$next" == "65" ]]; then # 'e'
consume 1
break
fi
if (( first == 0 )); then echo ","; fi
first=0
# -- Parse Key --
# Read length prefix until ':' (3a)
local klen_hex=""
while [[ $(peek) != "3a" ]]; do
klen_hex+="$(peek)"
consume 1
done
consume 1 # eat ':'
# Decode length
local klen=$(hex_digits_to_int "$klen_hex")
# Extract key hex
local key_hex="${HEX_STREAM:$POS:$((klen*2))}"
consume "$klen"
# Check for "pieces" key (hex 706965636573)
# We compare hex directly to avoid ascii conversion issues
local is_pieces=0
if [[ "$key_hex" == "706965636573" ]]; then
is_pieces=1
fi
# Print Key
printf "%s " "$indent_str"
to_json_str "$key_hex"
printf ": "
# -- Parse Value --
if [[ "$is_pieces" == "1" ]]; then
# SPECIAL CASE: Skip the pieces value
skip_element
printf "\"<skipped binary pieces>\""
else
parse $((indent_level + 1))
fi
done
echo ""
printf "%s}" "$indent_str"
return
fi
# --- List (l) ---
if [[ "$type" == "6c" ]]; then
consume 1 # eat 'l'
echo "["
local first=1
while true; do
local next=$(peek)
if [[ "$next" == "65" ]]; then # 'e'
consume 1
break
fi
if (( first == 0 )); then echo ","; fi
first=0
printf "%s " "$indent_str"
parse $((indent_level + 1))
done
echo ""
printf "%s]" "$indent_str"
return
fi
# --- Integer (i) ---
if [[ "$type" == "69" ]]; then
consume 1 # eat 'i'
local num_hex=""
while [[ $(peek) != "65" ]]; do
num_hex+="$(peek)"
consume 1
done
consume 1 # eat 'e'
# Convert hex to ascii string for the number
printf "%b" "$(echo "$num_hex" | sed 's/../\\x&/g')"
return
fi
# --- String (digit) ---
# ASCII '0'-'9' are hex 30-39
if [[ "$type" > "2f" && "$type" < "3a" ]]; then
local len_hex=""
while [[ $(peek) != "3a" ]]; do
len_hex+="$(peek)"
consume 1
done
consume 1 # eat ':'
local len=$(hex_digits_to_int "$len_hex")
local content_hex="${HEX_STREAM:$POS:$((len*2))}"
consume "$len"
to_json_str "$content_hex"
return
fi
}
# Helper to skip element without printing (used for 'pieces')
skip_element() {
local type=$(peek)
# Dict
if [[ "$type" == "64" ]]; then
consume 1
while [[ $(peek) != "65" ]]; do
skip_element # key
skip_element # value
done
consume 1
# List
elif [[ "$type" == "6c" ]]; then
consume 1
while [[ $(peek) != "65" ]]; do
skip_element
done
consume 1
# Integer
elif [[ "$type" == "69" ]]; then
consume 1
while [[ $(peek) != "65" ]]; do consume 1; done
consume 1
# String (default)
else
local len_hex=""
while [[ $(peek) != "3a" ]]; do
len_hex+="$(peek)"
consume 1
done
consume 1
local len=$(hex_digits_to_int "$len_hex")
consume "$len"
fi
}
# --- Main Execution ---
parse 0
echo ""
#!/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 ""
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment