# safe-decode
[](https://crates.io/crates/safe-decode)
[](https://docs.rs/safe-decode)
[](rust-toolchain.toml)
[](LICENSE)
[](https://github.com/sponsors/h4x0r)
[](https://github.com/SecurityRonin/safe-decode/actions/workflows/ci.yml)
[](https://doc.rust-lang.org/reference/behavior-considered-undefined.html)
[](https://rustsec.org/)
**Byte-to-text transforms that name what they do to NUL.** Fourteen hand-rolled UTF-16 decoders across one codebase, four different NUL policies, and every one of them called `decode_utf16le`. This crate ends that: the policy is in the function name, so choosing the wrong one is a decision rather than an accident.
```rust
use safe_decode::{
decode_utf16le_keep_nuls, decode_utf16le_trim_end_nuls, decode_utf16le_until_nul,
split_utf16le_on_nul,
};
// UTF-16LE 'A', NUL, 'B', NUL — the same bytes, four correct answers.
let bytes = b"A\0\0\0B\0\0\0";
assert_eq!(decode_utf16le_keep_nuls(bytes).text, "A\0B\0"); // NUL is a character
assert_eq!(decode_utf16le_until_nul(bytes).text, "A"); // NUL terminates
assert_eq!(decode_utf16le_trim_end_nuls(bytes).text, "A\0B"); // NUL is padding
let parts = split_utf16le_on_nul(bytes); // NUL separates
assert_eq!(parts.iter().map(|d| d.text.as_str()).collect::<Vec<_>>(), ["A", "B", ""]);
```
## Install
```toml
[dependencies]
safe-decode = "0.1"
```
## A decode that lost something says so
Every decode returns a `DecodedUtf16`, not a bare `String`. `text` is always well-formed UTF-8, so a caller may ignore the rest — but the rest is what a forensic report needs, because "the field decoded" and "the field decoded faithfully" are different claims.
```rust
use safe_decode::decode_utf16le_keep_nuls;
let d = decode_utf16le_keep_nuls(&[0x41, 0x00, 0x00, 0xD8, 0x42]);
assert_eq!(d.text, "A\u{FFFD}");
assert_eq!(d.unpaired_surrogates, 1); // a lone high surrogate, replaced
assert!(d.dangling_byte); // odd length: a byte could not form a code unit
assert!(d.is_lossy());
```
## What's here
| `rot13` | Rotate ASCII letters by thirteen; everything else, including non-ASCII, unchanged. |
| `decode_utf16le_keep_nuls` · `decode_utf16be_keep_nuls` | Decode the whole slice; NUL code units become U+0000 characters. |
| `decode_utf16le_until_nul` · `decode_utf16be_until_nul` | Stop at the first NUL; discard it and everything after. |
| `decode_utf16le_trim_end_nuls` · `decode_utf16be_trim_end_nuls` | Decode all, then strip trailing NULs only; interior NULs kept. |
| `split_utf16le_on_nul` · `split_utf16be_on_nul` | Split on NUL and decode every segment, empty ones included. |
| `to_hex_lower` · `to_hex_upper` | Two hex digits per byte, no separator. |
## Trust, but verify
- **Fuzzed.** A `cargo-fuzz` target per public function drives arbitrary byte slices; a panic is a build failure, and `cargo fuzz check` runs in CI.
- **Panic-free by lint and by construction.** `unwrap_used`, `expect_used` and `indexing_slicing` are `deny`, and `unsafe_code` is `forbid`. Code units come from `chunks_exact` through a fallible array conversion rather than an index; truncation goes through `Vec::truncate` rather than a range slice; the hex digit is masked to a nibble. There is no construct in the crate that can panic on any input.
- **100% line and function coverage**, gated in CI.
- **Zero dependencies**, `#![no_std]` plus `alloc`, MSRV 1.75 verified in CI — nothing here raises a consumer's floor.
## Scope
A function belongs in this crate only if it is **panic-free, allocating, format-agnostic, and free of domain knowledge**. `alloc` is the line between this crate and [`safe-read`](https://crates.io/crates/safe-read), its `no_std`-without-allocator sibling for fixed-width integer reads.
Deliberately absent: anything that needs to know what the bytes *mean*. `REG_MULTI_SZ`'s double-NUL terminator and `MRUListEx`'s `0xFFFFFFFF` sentinel are Windows registry conventions, not properties of an encoding, so they live with the crate that owns that knowledge. `split_utf16le_on_nul` gives that caller the structural half; the convention stays where the fact is. See [`docs/decisions/`](docs/decisions/).
---
[Privacy Policy](https://securityronin.github.io/safe-decode/privacy/) · [Terms of Service](https://securityronin.github.io/safe-decode/terms/) · © 2026 Security Ronin Ltd