escnul – NUL-safe byte escaping for embedding in shell scripts

# no_std support

`escnul` supports `no_std` with `alloc`.

- default features: `std`
- `no_std` build: disable default features

```toml
[dependencies]
escnul = { version = "0.2", default-features = false }
```

The core escaping APIs work in `no_std + alloc`.

- `Encode`
- `Decode`
- `OnePassEscapeEngine`
- `TwoPassEscapeEngine`
- `ShellDecodeMode::decoder_fragment`
- `TrSedEngine`

The stream-oriented APIs require `std` and are only available with the `std` feature.

- `StreamEncodeError`
- `TwoPassEscapeEngine::choose_shell_decode_mode_reader`
- `ShellDecodeMode::encode_reader`

# Overview

- [`escape`] module provides a low-overhead, reversible byte-escaping scheme
  inspired by the original **arx/shdat** tools. It lets you transform
  arbitrary binary data into a stream that can be safely embedded in
  a Bourne-shell script and decoded later with only `tr`+`sed` (or
  via the provided Rust APIs).
- [`OnePassEscapeEngine`] performs a single-pass encode/decode using a
  user-chosen escape byte.
- [`TwoPassEscapeEngine`] scans your data first, then either copies it raw
  (if no NULs) or picks a rare escape byte and delegates to
  [`OnePassEscapeEngine`].
- [`ShellDecodeMode`] exposes the low-level shell decode plan used for
  embedding payloads into POSIX shell scripts.
- [`ShellDecodeMode::encode_reader`] supports streaming `Read -> Write`
  encoding without buffering the whole payload in memory.
- All engines implement the [`Encode`], [`Decode`], and [`Engine`]
  traits for zero-allocation streaming into pre-allocated buffers.

# Example

```rust
use escnul::{Encode, Decode, OnePassEscapeEngine, TwoPassEscapeEngine, Engine, escape::TWO_PASS_SED};

// 1-pass encode/decode with user-chosen escape '@'
let data = b"hello\0world";
let mut buf = vec![0u8; data.len() * 2];
let m = OnePassEscapeEngine::new(b'@').encode_slice(data, &mut buf).unwrap();
assert_eq!(&buf[..m], b"hello\x01world");

// 2-pass engine: chooses raw copy if no NUL, else escapes
let engine = TWO_PASS_SED;
let enc = engine.encode(data);
assert_eq!(&enc[1..], b"hello\x01world");
let dec = engine.decode(enc);
assert_eq!(dec, data);
```

Shell-oriented usage:

```rust
use escnul::{ShellDecodeMode, escape::TWO_PASS_SED};

let data = b"hello\0world";
let mode = TWO_PASS_SED.choose_shell_decode_mode(data);
assert!(matches!(mode, ShellDecodeMode::TrSed { .. }));

let fragment = mode.decoder_fragment(data.len() as u64);
assert!(fragment.contains("sed"));
```
