use std::borrow::Cow;
use std::ffi::OsString;
use std::fmt::{self, Display};
use std::ops::Range;
use std::path::PathBuf;
use anyhow::Result;
use base64_simd::STANDARD as base64;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum RgMessageKind {
Begin,
End,
Match,
Context,
Summary,
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")]
#[serde(tag = "type", content = "data")]
pub enum RgMessage<'a> {
Begin {
#[serde(borrow)]
path: ArbitraryData<'a>,
},
End {
#[serde(borrow)]
path: ArbitraryData<'a>,
binary_offset: Option<usize>,
stats: Stats<'a>,
},
Match {
#[serde(borrow)]
path: ArbitraryData<'a>,
#[serde(borrow)]
lines: ArbitraryData<'a>,
line_number: Option<usize>,
absolute_offset: usize,
submatches: Vec<SubMatch<'a>>,
},
Context {
#[serde(borrow)]
path: ArbitraryData<'a>,
#[serde(borrow)]
lines: ArbitraryData<'a>,
line_number: Option<usize>,
absolute_offset: usize,
submatches: Vec<SubMatch<'a>>,
},
Summary {
elapsed_total: Duration<'a>,
stats: Stats<'a>,
},
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash)]
#[serde(untagged)]
pub enum ArbitraryData<'a> {
Text {
#[serde(borrow)]
text: Cow<'a, str>,
},
Base64 {
#[serde(borrow)]
bytes: Cow<'a, str>,
},
}
impl<'a> ArbitraryData<'a> {
pub fn to_vec(&self) -> Vec<u8> {
match self {
ArbitraryData::Text { text } => text.as_bytes().to_vec(),
ArbitraryData::Base64 { bytes } => base64.decode_to_vec(bytes.as_bytes()).unwrap(),
}
}
#[cfg(unix)]
pub fn to_os_string(&self) -> Result<OsString> {
use std::os::unix::ffi::OsStringExt;
Ok(match self {
ArbitraryData::Text { text } => OsString::from(text.to_string()),
ArbitraryData::Base64 { .. } => OsString::from_vec(self.to_vec()),
})
}
#[cfg(windows)]
pub fn to_os_string(&self) -> Result<OsString> {
use std::os::windows::ffi::OsStringExt;
Ok(match self {
ArbitraryData::Text { text } => OsString::from(text.to_string()),
ArbitraryData::Base64 { .. } => {
let bytes_u16 = safe_transmute::transmute_vec::<u8, u16>(self.to_vec())
.or_else(|e| e.copy())?;
OsString::from_wide(&bytes_u16)
}
})
}
pub fn to_path_buf(&self) -> Result<PathBuf> {
self.to_os_string().map(PathBuf::from)
}
pub fn lossy_utf8(&self) -> String {
match self {
ArbitraryData::Text { text } => text.to_string(),
ArbitraryData::Base64 { bytes } => {
String::from_utf8_lossy(base64.decode_to_vec(bytes.as_bytes()).unwrap().as_slice())
.to_string()
}
}
}
}
impl<'a> Display for ArbitraryData<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.lossy_utf8())
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub struct Stats<'a> {
#[serde(borrow)]
pub elapsed: Duration<'a>,
pub searches: usize,
pub searches_with_match: usize,
pub bytes_searched: usize,
pub bytes_printed: usize,
pub matched_lines: usize,
pub matches: usize,
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub struct Duration<'a> {
pub secs: usize,
pub nanos: usize,
#[serde(borrow)]
pub human: &'a str,
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename = "submatch")]
pub struct SubMatch<'a> {
#[serde(rename = "match", borrow)]
pub text: ArbitraryData<'a>,
#[serde(flatten)]
pub range: Range<usize>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn de_bytes() {
#[derive(Debug, Deserialize, Serialize)]
struct Foo<'a> {
data: &'a str,
}
let text = "foo\n";
let data = Foo { data: text };
let serialised = serde_json::to_string(&data).unwrap();
assert_eq!(serialised, r#"{"data":"foo\n"}"#);
dbg!(&data);
dbg!(data.data.as_bytes());
}
#[test]
fn arbitrary_data_text() {
let text = "foo\n";
let data = ArbitraryData::Text { text };
let ser = serde_json::to_string(&data).unwrap();
assert_eq!(ser, r#"{"text":"foo\n"}"#);
let de: ArbitraryData = serde_json::from_str(&ser).unwrap();
assert_eq!(
de,
ArbitraryData::TextOwned {
text: text.to_string()
}
);
}
#[test]
fn arbitrary_data_bytes() {
let bytes = "text";
let data = ArbitraryData::Base64 { bytes };
let ser = serde_json::to_string(&data).unwrap();
assert_eq!(ser, r#"{"bytes":"text"}"#);
let de: ArbitraryData = serde_json::from_str(&ser).unwrap();
assert_eq!(de, data);
}
#[test]
fn submatch() {
let text = "text";
let submatch = SubMatch {
text: ArbitraryData::Text { text },
range: 0..1,
};
let ser = serde_json::to_string(&submatch).unwrap();
assert_eq!(ser, r#"{"match":{"text":"text"},"start":0,"end":1}"#);
let de: SubMatch = serde_json::from_str(&ser).unwrap();
assert_eq!(de, submatch);
}
#[test]
fn rg_message_begin() {
let text = "foobar";
let msg = RgMessage::Begin {
path: ArbitraryData::Text { text },
};
let ser = serde_json::to_string(&msg).unwrap();
assert_eq!(ser, r#"{"type":"begin","data":{"path":{"text":"foobar"}}}"#);
let de: RgMessage = serde_json::from_str(&ser).unwrap();
assert_eq!(de, msg);
}
#[test]
fn rg_message_end() {
let text = "foobar";
let msg = RgMessage::End {
binary_offset: None,
stats: Stats {
elapsed: Duration {
secs: 1,
nanos: 1,
human: text,
},
searches: 1,
searches_with_match: 1,
bytes_searched: 1,
bytes_printed: 1,
matched_lines: 1,
matches: 1,
},
path: ArbitraryData::Text { text },
};
let ser = serde_json::to_string(&msg).unwrap();
assert_eq!(
ser,
r#"{"type":"end","data":{"path":{"text":"foobar"},"binary_offset":null,"stats":{"elapsed":{"secs":1,"nanos":1,"human":"foobar"},"searches":1,"searches_with_match":1,"bytes_searched":1,"bytes_printed":1,"matched_lines":1,"matches":1}}}"#
);
let de: RgMessage = serde_json::from_str(&ser).unwrap();
assert_eq!(de, msg);
}
#[test]
fn rg_message_match() {
let text = "foo";
let msg = RgMessage::Match {
path: ArbitraryData::Text { text },
lines: ArbitraryData::Text { text },
line_number: None,
absolute_offset: 1,
submatches: vec![],
};
let ser = serde_json::to_string(&msg).unwrap();
assert_eq!(
ser,
r#"{"type":"match","data":{"path":{"text":"foo"},"lines":{"text":"foo"},"line_number":null,"absolute_offset":1,"submatches":[]}}"#
);
let de: RgMessage = serde_json::from_str(&ser).unwrap();
assert_eq!(de, msg);
}
#[test]
fn rg_message_context() {
let text = "foobar";
let msg = RgMessage::Context {
path: ArbitraryData::Text { text },
lines: ArbitraryData::Text { text },
line_number: None,
absolute_offset: 1,
submatches: vec![],
};
let ser = serde_json::to_string(&msg).unwrap();
assert_eq!(
ser,
r#"{"type":"context","data":{"path":{"text":"foobar"},"lines":{"text":"foobar"},"line_number":null,"absolute_offset":1,"submatches":[]}}"#
);
let de: RgMessage = serde_json::from_str(&ser).unwrap();
assert_eq!(de, msg);
}
#[test]
fn rg_message_summary() {
let text = "foobar";
let msg = RgMessage::Summary {
elapsed_total: Duration {
secs: 1,
nanos: 1,
human: text,
},
stats: Stats {
elapsed: Duration {
secs: 1,
nanos: 1,
human: text,
},
searches: 1,
searches_with_match: 1,
bytes_searched: 1,
bytes_printed: 1,
matched_lines: 1,
matches: 1,
},
};
let ser = serde_json::to_string(&msg).unwrap();
assert_eq!(
ser,
r#"{"type":"summary","data":{"elapsed_total":{"secs":1,"nanos":1,"human":"foobar"},"stats":{"elapsed":{"secs":1,"nanos":1,"human":"foobar"},"searches":1,"searches_with_match":1,"bytes_searched":1,"bytes_printed":1,"matched_lines":1,"matches":1}}}"#
);
let de: RgMessage = serde_json::from_str(&ser).unwrap();
assert_eq!(de, msg);
}
}