use std::borrow::Cow;
use std::fmt;
#[derive(Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RawText(Vec<u8>);
impl RawText {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.0).ok()
}
pub fn is_utf8(&self) -> bool {
std::str::from_utf8(&self.0).is_ok()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn to_lossy(&self) -> Cow<'_, str> {
String::from_utf8_lossy(&self.0)
}
pub fn into_bytes(self) -> Vec<u8> {
self.0
}
}
impl AsRef<[u8]> for RawText {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for RawText {
fn from(b: Vec<u8>) -> RawText {
RawText(b)
}
}
impl From<&[u8]> for RawText {
fn from(b: &[u8]) -> RawText {
RawText(b.to_vec())
}
}
impl From<String> for RawText {
fn from(s: String) -> RawText {
RawText(s.into_bytes())
}
}
impl From<&str> for RawText {
fn from(s: &str) -> RawText {
RawText(s.as_bytes().to_vec())
}
}
impl fmt::Display for RawText {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_lossy())
}
}
impl fmt::Debug for RawText {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\"")?;
let mut rest: &[u8] = &self.0;
while !rest.is_empty() {
match std::str::from_utf8(rest) {
Ok(s) => {
write_escaped(f, s)?;
break;
}
Err(e) => {
let good = e.valid_up_to();
if let Ok(s) = std::str::from_utf8(&rest[..good]) {
write_escaped(f, s)?;
}
let bad = e.error_len().unwrap_or(rest.len() - good);
for b in &rest[good..good + bad] {
write!(f, "\\x{b:02x}")?;
}
rest = &rest[good + bad..];
}
}
}
f.write_str("\"")
}
}
fn write_escaped(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
for c in s.chars() {
match c {
'"' => f.write_str("\\\"")?,
'\\' => f.write_str("\\\\")?,
'\n' => f.write_str("\\n")?,
'\r' => f.write_str("\\r")?,
'\t' => f.write_str("\\t")?,
c => write!(f, "{c}")?,
}
}
Ok(())
}
impl PartialEq<str> for RawText {
fn eq(&self, other: &str) -> bool {
self.0 == other.as_bytes()
}
}
impl PartialEq<&str> for RawText {
fn eq(&self, other: &&str) -> bool {
self.0 == other.as_bytes()
}
}
impl PartialEq<String> for RawText {
fn eq(&self, other: &String) -> bool {
self.0 == other.as_bytes()
}
}
impl PartialEq<RawText> for str {
fn eq(&self, other: &RawText) -> bool {
self.as_bytes() == other.0
}
}
impl PartialEq<RawText> for &str {
fn eq(&self, other: &RawText) -> bool {
self.as_bytes() == other.0
}
}
impl PartialEq<RawText> for String {
fn eq(&self, other: &RawText) -> bool {
self.as_bytes() == other.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn non_utf8_bytes_survive_and_say_so() {
let v = RawText::from(vec![b'"', 0xc3, b'(', b'"']);
assert_eq!(v.as_bytes(), [b'"', 0xc3, b'(', b'"']);
assert_eq!(v.as_str(), None);
assert!(!v.is_utf8());
assert_eq!(v.to_lossy(), "\"\u{fffd}(\"");
assert_eq!(v.to_string(), "\"\u{fffd}(\"");
assert_eq!(format!("{v:?}"), r#""\"\xc3(\"""#);
}
#[test]
fn utf8_bytes_read_as_text() {
let v = RawText::from("\"2023-11-14 22:13:20\"");
assert_eq!(v.as_str(), Some("\"2023-11-14 22:13:20\""));
assert!(v.is_utf8());
assert_eq!(v, "\"2023-11-14 22:13:20\"");
assert_eq!("\"2023-11-14 22:13:20\"", v);
assert_eq!(v.len(), 21);
assert!(!v.is_empty());
}
#[test]
fn empty_is_no_value_and_a_stored_null_is_not_empty() {
assert!(RawText::default().is_empty());
assert_eq!(RawText::default(), "");
let null = RawText::from("null");
assert!(!null.is_empty());
assert_eq!(null, "null");
}
}