What problem does your feature solve?
Two friction points show up together whenever a downstream crate wants to embed escaped output in a log line or CLI message.
1. No way to format escaped bytes without allocating. The crate exposes escape(bytes) -> Vec<u8> and escape_into(&mut [u8], bytes) -> Result<usize, _>. Neither plugs into format! / write! directly — callers must allocate an intermediate Vec<u8> (and usually convert it to String) even though the result is going straight into another format machine.
2. String inputs require .as_bytes() at every call site. The input bound is IntoIterator<Item: Borrow<u8>>, which &str does not satisfy, so callers must remember the conversion every time.
Example from stellar-cli (stellar/stellar-cli#2509), which is the motivating downstream:
fn sanitize(s: &str) -> String {
escape_bytes::escape(s.as_bytes())
.into_iter()
.map(char::from)
.collect()
}
print.infoln(format!("Message: {}", sanitize(&message_display)));
Allocation + UTF-8 construction + an .as_bytes() dance, all for output that is immediately consumed by another formatter.
What would you like to see?
1. Display for Escape<I>. Implement core::fmt::Display on the existing Escape<I> iterator so it can be dropped into format! / write! with no intermediate buffer. Because Display::fmt takes &self, the impl needs I::IntoIter: Clone — the same bound the existing impl Clone for Escape<I> already carries, so &[u8], &Vec<u8>, array references, etc. all satisfy it. Works under no_std.
2. Accept &str wherever bytes are accepted today. Let Escape::new, escape, and escape_into take string input directly, without requiring callers to write .as_bytes(). Existing inputs (&[u8], Vec<u8>, [u8; N], &[u8; N], arbitrary IntoIterator<Item: Borrow<u8>>) must continue to compile unchanged.
Together, callers would then write:
use escape_bytes::Escape;
print.infoln(format!("Message: {}", Escape::new(&message_display)));
No .as_bytes(), no intermediate buffer, no helper wrapper, no unsafe.
One implementation shape for the &str piece is a small conversion trait (e.g. IntoEscape) blanket-implemented for IntoIterator<Item: Borrow<u8>> and separately implemented for &str, with the constructors switching to impl IntoEscape. Coherence between the blanket and the &str impl needs verification during implementation; an equivalent alternative is a sealed adapter trait explicitly implemented for each supported input type.
Scope:
- Add
impl core::fmt::Display for Escape<I> in src/escape.rs.
- Widen the input bound on
Escape::new, escape, and escape_into so &str is accepted.
- Tests covering: empty input, printable-only input, the hex-escape path, the format-interpolation case, and
&str input to each entry point.
- Rustdoc examples using
&str directly.
What alternatives are there?
- Free function
display(bytes: &[u8]) -> impl Display + '_ instead of impl Display for Escape. Works, but adds API surface for something the existing iterator can already carry, and the bare name reads out of place against the rest of the escape_* API.
- Separate
from_str constructor on Escape instead of widening new. Backward-compatible with zero risk, but still asks the caller to pick the right constructor per input type rather than "just pass the thing."
escape_to_string(bytes: &[u8]) -> String convenience wrapper. Still allocates; only addresses the "I want a String" case, not the "interpolate inline" case that's the hot path downstream.
- Leave as-is; downstream crates keep writing a local
sanitize helper. What stellar-cli does today. Reasonable as a workaround, but the helper has to live somewhere — in stellar-cli it currently sits in a contract-spec formatter and is called from an unrelated command, a layering smell that goes away once this crate exposes the primitive.
Prior art for the proposed shape: str::escape_default (and friends) in std return iterators that implement Display; bstr::ByteSlice::escape_bytes does the same.
What problem does your feature solve?
Two friction points show up together whenever a downstream crate wants to embed escaped output in a log line or CLI message.
1. No way to format escaped bytes without allocating. The crate exposes
escape(bytes) -> Vec<u8>andescape_into(&mut [u8], bytes) -> Result<usize, _>. Neither plugs intoformat!/write!directly — callers must allocate an intermediateVec<u8>(and usually convert it toString) even though the result is going straight into another format machine.2. String inputs require
.as_bytes()at every call site. The input bound isIntoIterator<Item: Borrow<u8>>, which&strdoes not satisfy, so callers must remember the conversion every time.Example from stellar-cli (stellar/stellar-cli#2509), which is the motivating downstream:
Allocation + UTF-8 construction + an
.as_bytes()dance, all for output that is immediately consumed by another formatter.What would you like to see?
1.
DisplayforEscape<I>. Implementcore::fmt::Displayon the existingEscape<I>iterator so it can be dropped intoformat!/write!with no intermediate buffer. BecauseDisplay::fmttakes&self, the impl needsI::IntoIter: Clone— the same bound the existingimpl Clone for Escape<I>already carries, so&[u8],&Vec<u8>, array references, etc. all satisfy it. Works underno_std.2. Accept
&strwherever bytes are accepted today. LetEscape::new,escape, andescape_intotake string input directly, without requiring callers to write.as_bytes(). Existing inputs (&[u8],Vec<u8>,[u8; N],&[u8; N], arbitraryIntoIterator<Item: Borrow<u8>>) must continue to compile unchanged.Together, callers would then write:
No
.as_bytes(), no intermediate buffer, no helper wrapper, nounsafe.One implementation shape for the
&strpiece is a small conversion trait (e.g.IntoEscape) blanket-implemented forIntoIterator<Item: Borrow<u8>>and separately implemented for&str, with the constructors switching toimpl IntoEscape. Coherence between the blanket and the&strimpl needs verification during implementation; an equivalent alternative is a sealed adapter trait explicitly implemented for each supported input type.Scope:
impl core::fmt::Display for Escape<I>insrc/escape.rs.Escape::new,escape, andescape_intoso&stris accepted.&strinput to each entry point.&strdirectly.What alternatives are there?
display(bytes: &[u8]) -> impl Display + '_instead ofimpl Display for Escape. Works, but adds API surface for something the existing iterator can already carry, and the bare name reads out of place against the rest of theescape_*API.from_strconstructor onEscapeinstead of wideningnew. Backward-compatible with zero risk, but still asks the caller to pick the right constructor per input type rather than "just pass the thing."escape_to_string(bytes: &[u8]) -> Stringconvenience wrapper. Still allocates; only addresses the "I want aString" case, not the "interpolate inline" case that's the hot path downstream.sanitizehelper. What stellar-cli does today. Reasonable as a workaround, but the helper has to live somewhere — in stellar-cli it currently sits in a contract-spec formatter and is called from an unrelated command, a layering smell that goes away once this crate exposes the primitive.Prior art for the proposed shape:
str::escape_default(and friends) in std return iterators that implementDisplay;bstr::ByteSlice::escape_bytesdoes the same.