Skip to content

Commit 2c620e0

Browse files
committed
feat: utf8to16 / utf16to8 helper wrappers (LE-default iconv filters)
Rather than baking UTF-16 into all five implementations, add two thin stdin/stdout iconv wrappers in bin/: utf8to16 (UTF-8 -> UTF-16) and utf16to8 (UTF-16 -> UTF-8). Both default to LITTLE-ENDIAN (ARM/x86_64; iconv's UTF-16 default is BE per RFC 2781, which surprised us). utf8to16 emits a BOM by default (--no-bom to skip); utf16to8 auto-detects and strips a BOM, falling back to LE. Every glyph is BMP, so UTF-8<->UTF-16 is a lossless bijection — verified round-trips (default LE, --be, --no-bom). Un-ignore them in .gitignore (bin/* is ignored), annotate via dirtree, and document the recipe + the iconv escape hatch in the README.
1 parent 67bb4e9 commit 2c620e0

5 files changed

Lines changed: 94 additions & 0 deletions

File tree

.dirtree-state

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ annotate=[
2626
bin/printable-binary = LuaJIT CLI (the original reference implementation)
2727
bin/printable-binary-node.js = Node.js CLI (ESM)
2828
bin/printable_binary_ape.com = Prebuilt Actually-Portable-Executable (Cosmopolitan) C binary
29+
bin/utf16to8 = Helper: transcode UTF-16 stdin -> UTF-8 (iconv wrapper; auto-detects BOM, else LE)
30+
bin/utf8to16 = Helper: transcode UTF-8 stdin -> UTF-16 (iconv wrapper; default LE + BOM)
2931
bm = Ad-hoc benchmark scripts (C vs Lua vs Zig)
3032
bm/benchmark-zig-opt = Zig optimization benchmark
3133
bm/benchmark_c_vs_lua.sh = C vs Lua benchmark script

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ bm/results
3535

3636
# anything else
3737
ZIG_0.15_TO_0.16_MIGRATION.md
38+
!bin/utf8to16
39+
!bin/utf16to8

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,24 @@ Because `--passthrough` sends the original binary to stdout, you can insert Prin
294294
# Should show 89PNG⏎␣␣ if the PNG signature is intact.
295295
```
296296
297+
### Piping to UTF-16 (or other text encodings)
298+
299+
The encoded output is UTF-8 text. Because every glyph is in the BMP, it transcodes **losslessly** to UTF-16 — handy for Windows/PowerShell/JavaScript/.NET consumers. Two thin `iconv` wrappers default to **little-endian** (ARM and x86_64) and handle the BOM:
300+
301+
```bash
302+
# Encode -> UTF-16LE (with BOM)
303+
printable-binary file | bin/utf8to16 > out.utf16
304+
305+
# Decode a UTF-16 stream (BOM auto-detected; little-endian if absent)
306+
bin/utf16to8 < out.utf16 | printable-binary -d
307+
308+
# Explicit endianness / no BOM
309+
printable-binary file | bin/utf8to16 --be # UTF-16BE + BOM
310+
printable-binary file | bin/utf8to16 --no-bom # UTF-16LE, no BOM
311+
```
312+
313+
They are just `iconv` wrappers, so any encoding iconv supports works directly too (e.g. `... | iconv -f UTF-8 -t UTF-16LE`).
314+
297315
## Format Compatibility
298316
299317
The PrintableBinary character set is specifically designed to be highly compatible with common text formats:

bin/utf16to8

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
#
3+
# utf16to8 — transcode UTF-16 on stdin to UTF-8 on stdout (a thin iconv wrapper).
4+
#
5+
# A leading BOM is auto-detected, used to pick endianness, and stripped. If there
6+
# is no BOM, little-endian is assumed (ARM/x86_64 default); override with --le/--be.
7+
#
8+
# utf16to8 < file.utf16 | printable-binary -d # auto-detect BOM, else LE
9+
# utf16to8 --be < file.utf16be | printable-binary -d
10+
set -euo pipefail
11+
12+
endian=LE
13+
forced=0
14+
for arg in "$@"; do
15+
case "$arg" in
16+
--le) endian=LE; forced=1 ;;
17+
--be) endian=BE; forced=1 ;;
18+
-h|--help)
19+
echo "usage: utf16to8 [--le|--be] (UTF-16 stdin -> UTF-8 stdout; BOM auto-detected, else LE)"
20+
exit 0 ;;
21+
*) echo "utf16to8: unknown option: $arg" >&2; exit 2 ;;
22+
esac
23+
done
24+
25+
tmp="$(mktemp)"
26+
trap 'rm -f "$tmp"' EXIT
27+
cat > "$tmp"
28+
29+
skip=0
30+
bom="$(head -c 2 "$tmp" | xxd -p 2>/dev/null || true)"
31+
case "$bom" in
32+
fffe) skip=2; [ "$forced" -eq 1 ] || endian=LE ;;
33+
feff) skip=2; [ "$forced" -eq 1 ] || endian=BE ;;
34+
esac
35+
36+
tail -c "+$((skip + 1))" "$tmp" | iconv -f "UTF-16$endian" -t UTF-8

bin/utf8to16

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
#
3+
# utf8to16 — transcode UTF-8 on stdin to UTF-16 on stdout (a thin iconv wrapper).
4+
#
5+
# Defaults to LITTLE-ENDIAN with a BOM. ARM and x86_64 are little-endian, and
6+
# `iconv -t UTF-16` historically emits big-endian (per RFC 2781), so this wrapper
7+
# makes LE the explicit, predictable default.
8+
#
9+
# printable-binary file | utf8to16 # UTF-16LE + BOM
10+
# printable-binary file | utf8to16 --be # UTF-16BE + BOM
11+
# printable-binary file | utf8to16 --no-bom # UTF-16LE, no BOM
12+
#
13+
# Useful because every printable-binary glyph is in the BMP, so UTF-8<->UTF-16 is
14+
# a lossless bijection — pair with utf16to8 to round-trip through UTF-16.
15+
set -euo pipefail
16+
17+
endian=LE
18+
bom=1
19+
for arg in "$@"; do
20+
case "$arg" in
21+
--le) endian=LE ;;
22+
--be) endian=BE ;;
23+
--no-bom) bom=0 ;;
24+
-h|--help)
25+
echo "usage: utf8to16 [--le|--be] [--no-bom] (UTF-8 stdin -> UTF-16 stdout; default: LE + BOM)"
26+
exit 0 ;;
27+
*) echo "utf8to16: unknown option: $arg" >&2; exit 2 ;;
28+
esac
29+
done
30+
31+
# Emit the BOM ourselves so endianness is guaranteed regardless of iconv's default.
32+
if [ "$bom" -eq 1 ]; then
33+
if [ "$endian" = LE ]; then printf '\xff\xfe'; else printf '\xfe\xff'; fi
34+
fi
35+
36+
exec iconv -f UTF-8 -t "UTF-16$endian"

0 commit comments

Comments
 (0)