use std::{io, str};
use anyhow::{bail, Result};
use regex::{escape, Regex};
#[cfg(feature = "redact-info")]
use crate::data::Info;
#[cfg(feature = "redact-json")]
use crate::json;
use crate::{
data::{Pattern, REDACT_PLACEHOLDER},
pattern,
};
pub struct Redaction {
#[cfg(feature = "redact-json")]
json: json::Redact,
pattern: pattern::Redact,
}
impl Default for Redaction {
fn default() -> Self {
Self::new()
}
}
impl Redaction {
#[must_use]
pub fn new() -> Self {
Self::custom(REDACT_PLACEHOLDER)
}
#[must_use]
pub fn custom(redact_placeholder: &str) -> Self {
Self {
#[cfg(feature = "redact-json")]
json: json::Redact::with_redact_placeholder(redact_placeholder),
pattern: pattern::Redact::with_redact_placeholder(redact_placeholder),
}
}
pub fn add_value(self, value: &str) -> Result<Self> {
let pattern = Pattern {
test: Regex::new(&format!("({})", escape(value)))?,
group: 1,
};
Ok(self.add_pattern(pattern))
}
pub fn add_values(self, values: Vec<&str>) -> Result<Self> {
let mut errors = vec![];
let patterns = values
.iter()
.filter_map(|val| match Regex::new(&format!("({})", escape(val))) {
Ok(test) => Some(Pattern { test, group: 1 }),
Err(_e) => {
errors.push((*val).to_string());
None
}
})
.collect::<Vec<_>>();
if !errors.is_empty() {
bail!("could not parse {} to regex", errors.join(","))
}
Ok(self.add_patterns(patterns))
}
#[must_use]
pub fn add_pattern(mut self, pattern: Pattern) -> Self {
self.pattern = self.pattern.add_pattern(pattern);
self
}
#[must_use]
pub fn add_patterns(mut self, patterns: Vec<Pattern>) -> Self {
self.pattern = self.pattern.add_patterns(patterns);
self
}
#[cfg(feature = "redact-json")]
#[must_use]
pub fn add_keys(mut self, keys: Vec<&str>) -> Self {
self.json = self.json.add_keys(keys);
self
}
#[cfg(feature = "redact-json")]
#[must_use]
pub fn add_paths(mut self, key: Vec<&str>) -> Self {
self.json = self.json.add_paths(key);
self
}
#[must_use]
pub fn redact_str(&self, str: &str) -> String {
self.pattern.redact_patterns(str, false).string
}
#[cfg(feature = "redact-info")]
#[must_use]
pub fn redact_str_with_info(&self, str: &str) -> Info {
self.pattern.redact_patterns(str, true)
}
pub fn redact_reader<R>(&self, rdr: R) -> Result<String>
where
R: io::Read,
{
let mut rdr_box = Box::new(rdr);
let mut buffer = Vec::new();
rdr_box.read_to_end(&mut buffer)?;
Ok(self.redact_str(str::from_utf8(&buffer)?))
}
#[cfg(feature = "redact-info")]
pub fn redact_reader_with_info<R>(&self, rdr: R) -> Result<Info>
where
R: io::Read,
{
let mut rdr_box = Box::new(rdr);
let mut buffer = Vec::new();
rdr_box.read_to_end(&mut buffer)?;
Ok(self.redact_str_with_info(str::from_utf8(&buffer)?))
}
#[cfg(feature = "redact-json")]
pub fn redact_json(&self, str: &str) -> Result<String> {
self.json.redact_str(&self.redact_str(str))
}
#[cfg(feature = "redact-json")]
pub fn redact_json_value(&self, value: &serde_json::Value) -> Result<serde_json::Value> {
let redact_str = self.redact_str(&value.to_string());
let mut value: serde_json::Value = serde_json::from_str(&redact_str)?;
Ok(self.json.redact_from_value(&mut value))
}
}
#[cfg(test)]
mod test_redaction {
use std::{env, fs::File, io::Write};
use insta::assert_debug_snapshot;
use super::*;
const TEXT: &str = "foo,bar,baz,extra";
#[cfg(feature = "redact-json")]
use serde_json::json;
#[test]
fn test_by_pattern() {
let pattern = Pattern {
test: Regex::new("(foo)").unwrap(),
group: 1,
};
let patterns = vec![
Pattern {
test: Regex::new("(bar)").unwrap(),
group: 1,
},
Pattern {
test: Regex::new("(baz)").unwrap(),
group: 1,
},
];
assert_debug_snapshot!(Redaction::new()
.add_pattern(pattern)
.add_patterns(patterns)
.redact_str(TEXT));
}
#[test]
fn test_bt_value() {
assert_debug_snapshot!(Redaction::new()
.add_value("foo")
.unwrap()
.add_values(vec!["bar", "baz"])
.unwrap()
.redact_str(TEXT));
}
#[test]
fn can_redact_str() {
let pattern = Pattern {
test: Regex::new("(bar)").unwrap(),
group: 1,
};
let redaction = Redaction::new().add_pattern(pattern);
assert_debug_snapshot!(redaction.redact_str(TEXT));
}
#[test]
#[cfg(feature = "redact-info")]
fn can_redact_str_with_info() {
let pattern = Pattern {
test: Regex::new("(bar)").unwrap(),
group: 1,
};
let redaction = Redaction::new().add_pattern(pattern);
assert_debug_snapshot!(redaction.redact_str_with_info(TEXT));
}
#[test]
fn can_redact_reader() {
let file_path = env::temp_dir().join("foo.txt");
let mut f = File::create(&file_path).unwrap();
#[allow(clippy::unused_io_amount)]
f.write(TEXT.as_bytes()).unwrap();
let pattern = Pattern {
test: Regex::new("(bar)").unwrap(),
group: 1,
};
let redaction = Redaction::new().add_pattern(pattern);
assert_debug_snapshot!(redaction.redact_reader(File::open(file_path).unwrap()));
}
#[test]
#[cfg(feature = "redact-info")]
fn can_redact_reader_with_info() {
let file_path = env::temp_dir().join("foo.txt");
let mut f = File::create(&file_path).unwrap();
#[allow(clippy::unused_io_amount)]
f.write(TEXT.as_bytes()).unwrap();
let pattern = Pattern {
test: Regex::new("(bar)").unwrap(),
group: 1,
};
let redaction = Redaction::new().add_pattern(pattern);
assert_debug_snapshot!(redaction.redact_reader_with_info(File::open(file_path).unwrap()));
}
#[test]
fn can_redact_with_multiple_patterns() {
let patterns = vec![
Pattern {
test: Regex::new("(bar)").unwrap(),
group: 1,
},
Pattern {
test: Regex::new("(foo),(bar),(baz)").unwrap(),
group: 3,
},
];
let redaction = Redaction::new().add_patterns(patterns);
assert_debug_snapshot!(redaction.redact_str(TEXT));
}
#[test]
fn can_redact_with_placeholder_text() {
let pattern = Pattern {
test: Regex::new("(bar)").unwrap(),
group: 1,
};
let redaction = Redaction::custom("[HIDDEN_TEXT]").add_pattern(pattern);
assert_debug_snapshot!(redaction.redact_str(TEXT));
}
#[test]
#[cfg(feature = "redact-json")]
fn can_redact_json() {
let pattern = Pattern {
test: Regex::new("(redact-by-pattern)").unwrap(),
group: 1,
};
let json = json!({
"all-path": {
"b": {
"key": "redact_me",
},
"foo": "redact_me",
"key": "redact_me",
},
"specific-key": {
"b": {
"key": "skip-redaction",
},
"foo": "skip-redaction",
"key": "redact_me"
},
"key": "redact_me",
"skip": "skip-redaction",
"by-value": "bar",
"by-pattern": "redact-by-pattern",
})
.to_string();
let redaction = Redaction::default()
.add_pattern(pattern)
.add_paths(vec!["all-path.*", "specific-key.key"])
.add_keys(vec!["key"])
.add_value("bar")
.unwrap();
assert_debug_snapshot!(redaction.redact_json(&json));
}
#[test]
#[cfg(feature = "redact-json")]
fn can_redact_json_value() {
let pattern = Pattern {
test: Regex::new("(redact-by-pattern)").unwrap(),
group: 1,
};
let json = json!({
"all-path": {
"b": {
"key": "redact_me",
},
"foo": "redact_me",
"key": "redact_me",
},
"specific-key": {
"b": {
"key": "skip-redaction",
},
"foo": "skip-redaction",
"key": "redact_me"
},
"key": "redact_me",
"skip": "skip-redaction",
"by-value": "bar",
"by-pattern": "redact-by-pattern",
});
let redaction = Redaction::default()
.add_pattern(pattern)
.add_paths(vec!["all-path.*", "specific-key.key"])
.add_keys(vec!["key"])
.add_value("bar")
.unwrap();
assert_debug_snapshot!(redaction.redact_json_value(&json));
}
}