try-ascii 1.1.0

Helper to format byte slices that probably/mostly contain ASCII-encoded text
Documentation
// Copyright Open Logistics Foundation
//
// Licensed under the Open Logistics Foundation License 1.3.
// For details on the licensing terms, see the LICENSE file.
// SPDX-License-Identifier: OLFL-1.3

#![doc = include_str!("../README.md")]
#![no_std]

/// Wrapper type that contains the byte slice and implements [Debug]
///
/// The [Debug] implementation prints the text parts as text and falls back to the default [Debug]
/// implementation for `&[u8]` for the rest.
pub struct MaybeAscii<'a> {
    pub bytes: &'a [u8],
}

impl<'a> core::fmt::Debug for MaybeAscii<'a> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
        let mut start = 0;
        while start < self.bytes.len() {
            let is_ascii = self.bytes[start] < 128;
            let mut end = start + 1;
            loop {
                if end >= self.bytes.len() {
                    break;
                }
                if (self.bytes[end] < 128) != is_ascii {
                    break;
                }
                end += 1;
            }
            if is_ascii {
                let s = core::str::from_utf8(&self.bytes[start..end]).expect("The slice should only contain ascii characters (<128) which should be convertible to a utf8 string.");
                f.write_str(s)?;
            } else {
                self.bytes[start..end].fmt(f)?;
            }
            start = end;
        }
        Ok(())
    }
}

/// Helper function to format the text parts of a byte slice as text and the non-text parts with
/// the default [Debug] implementation for byte slices
pub fn try_ascii(bytes: &[u8]) -> MaybeAscii<'_> {
    MaybeAscii { bytes }
}

/// Trait that encapsulates the logic of `try_ascii`
///
/// The trait is automatically implemented for `[u8]`, `&[u8]` and everything that implements `AsRef<[u8]>`.
pub trait TryAscii {
    fn try_ascii(&self) -> MaybeAscii<'_>;
}

impl TryAscii for [u8] {
    fn try_ascii(&self) -> MaybeAscii<'_> {
        try_ascii(self)
    }
}

impl TryAscii for &[u8] {
    fn try_ascii(&self) -> MaybeAscii<'_> {
        try_ascii(self)
    }
}

impl TryAscii for dyn AsRef<[u8]> {
    fn try_ascii(&self) -> MaybeAscii<'_> {
        try_ascii(self.as_ref())
    }
}

#[cfg(test)]
mod tests {
    extern crate std;
    use crate::try_ascii;
    use crate::TryAscii;
    use std::*;

    #[test]
    fn format_ascii() {
        let res = format!("{:?}", try_ascii(b"Hello, World"));
        assert_eq!(res, "Hello, World");
    }

    #[test]
    fn format_non_ascii() {
        let res = format!("{:?}", try_ascii(b"Hello, \xa0"));
        assert_eq!(res, "Hello, [160]");
    }

    #[test]
    fn format_slice() {
        let res = format!("{:?}", try_ascii(b"Hello, \xa0\xa1\xa2\xa3"));
        assert_eq!(res, "Hello, [160, 161, 162, 163]");
    }

    #[test]
    fn format_hex() {
        let res = format!("{:x?}", try_ascii(b"Hello, \xa0\xa1\xa2\xa3"));
        assert_eq!(res, "Hello, [a0, a1, a2, a3]");
    }

    #[test]
    fn format_pretty() {
        let res = format!("{:#X?}", try_ascii(&[0xa0, 0xa1]));
        assert_eq!(res, "[\n    0xA0,\n    0xA1,\n]");
    }

    #[test]
    fn format_empty() {
        let res = format!("{:?}", try_ascii(&[]));
        assert_eq!(res, "");
    }

    #[test]
    fn trait_for_u8() {
        let res = format!("{:x?}", b"Hello, \xa0\xa1\xa2\xa3".try_ascii());
        assert_eq!(res, "Hello, [a0, a1, a2, a3]");
    }

    #[test]
    fn trait_for_ref_u8() {
        let arr: &[u8] = b"Hello, \xa0\xa1\xa2\xa3";
        let res = format!("{:x?}", arr.try_ascii());
        assert_eq!(res, "Hello, [a0, a1, a2, a3]");
    }

    #[test]
    fn trait_for_as_ref_u8() {
        let arr: [u8; 11] = *b"Hello, \xa0\xa1\xa2\xa3";
        let res = format!("{:x?}", arr.as_ref().try_ascii());
        assert_eq!(res, "Hello, [a0, a1, a2, a3]");
    }
}