If I can't assume a particular encoding, the way I normally do it is with the std::ascii::escape_default
function. Basically, it will show most ASCII characters as they are, and then escape everything else. The downside is that you won't see every possible Unicode codepoint even if portions of your string are correct UTF-8, but it does the job for most uses:
use std::ascii::escape_default;
use std::str;
fn show(bs: &[u8]) -> String {
let mut visible = String::new();
for &b in bs {
let part: Vec<u8> = escape_default(b).collect();
visible.push_str(str::from_utf8(&part).unwrap());
}
visible
}
fn main() {
let bytes = b"foo\xE2\x98\x83bar\xFFbaz";
println!("{}", show(bytes));
}
Output: foo\xe2\x98\x83bar\xffbaz
Another approach is to lossily decode the contents into a string and print that. If there's any invalid UTF-8, you'll get a Unicode replacement character instead of hex escapes of the raw bytes, but you will get to see all valid UTF-8 encoded Unicode codepoints:
fn show(bs: &[u8]) -> String {
String::from_utf8_lossy(bs).into_owned()
}
fn main() {
let bytes = b"foo\xE2\x98\x83bar\xFFbaz";
println!("{}", show(bytes));
}
Output: foo☃bar�baz