use std::{
borrow::Cow,
ffi::OsStr,
fmt::Write as _,
};
use crate::{
DiagnosticInputBudget,
LogOutputLimit,
LogSafeText,
RedactedText,
Redactor,
Sensitivity,
text::internal::BoundedLogEscapeWriter,
};
use super::RedactedEnvPair;
#[must_use = "use the redactor to produce safe environment diagnostics"]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvRedactor {
redactor: Redactor,
}
impl EnvRedactor {
#[inline(always)]
pub const fn new(redactor: Redactor) -> Self {
Self { redactor }
}
#[inline(always)]
pub const fn redactor(&self) -> &Redactor {
&self.redactor
}
#[inline]
pub fn redact_pair(&self, name: &str, value: &str) -> RedactedEnvPair {
let value = self.redactor.redact(name, value).into_owned();
let name = log_safe_owned(name.to_owned());
RedactedEnvPair::new(name, log_safe_owned(value))
}
pub fn redact_os_pair(
&self,
name: &OsStr,
value: &OsStr,
) -> RedactedEnvPair {
match (name.to_str(), value.to_str()) {
(Some(name), Some(value)) => self.redact_pair(name, value),
_ => {
let name = log_safe_owned(name.to_string_lossy().into_owned());
let value = self.mask_opaque_value();
RedactedEnvPair::new(name, log_safe_owned(value))
}
}
}
pub fn redact_os_pairs<'a, I>(&self, pairs: I) -> LogSafeText<'static>
where
I: IntoIterator<Item = (&'a OsStr, &'a OsStr)>,
{
let mut input_budget =
self.redactor.policy().diagnostic_budget().input_budget();
self.redact_os_pairs_with_input_budget(pairs, &mut input_budget)
}
pub fn redact_os_pairs_with_input_budget<'a, I>(
&self,
pairs: I,
input_budget: &mut DiagnosticInputBudget,
) -> LogSafeText<'static>
where
I: IntoIterator<Item = (&'a OsStr, &'a OsStr)>,
{
let budget = self.redactor.policy().diagnostic_budget();
let limit = LogOutputLimit::from(budget);
let mut writer = BoundedLogEscapeWriter::new(limit);
let _ = writer.write_str("[");
let mut has_item = false;
for (name, value) in pairs {
if writer.is_truncated() {
break;
}
let pair_bytes = name
.as_encoded_bytes()
.len()
.saturating_add(value.as_encoded_bytes().len());
if !input_budget.reserve(pair_bytes) {
write_debug_item(&mut writer, &mut has_item, "<truncated>");
break;
}
let pair = self.redact_os_pair_bounded(
name,
value,
budget.max_output_bytes(),
);
write_debug_item(&mut writer, &mut has_item, &pair);
}
if !writer.is_truncated() {
let _ = writer.write_str("]");
}
LogSafeText::from_escaped(Cow::Owned(writer.finish()))
}
#[inline]
pub fn redact_assignment(&self, assignment: &str) -> RedactedEnvPair {
let (name, value) =
assignment.split_once('=').unwrap_or((assignment, ""));
self.redact_pair(name, value)
}
#[inline(always)]
fn mask_opaque_value(&self) -> String {
self.redactor
.policy()
.masking()
.mask_opaque(Sensitivity::Secret)
.to_owned()
}
fn redact_os_pair_bounded(
&self,
name: &OsStr,
value: &OsStr,
max_mask_bytes: usize,
) -> String {
let pair = match (name.to_str(), value.to_str()) {
(Some(name), Some(value)) => {
let value = match self.redactor.policy().sensitivity_for(name) {
Some(level) => self
.redactor
.policy()
.masking()
.mask_bounded(level, value, max_mask_bytes)
.into_owned(),
None => value.to_owned(),
};
RedactedEnvPair::new(
log_safe_owned(name.to_owned()),
log_safe_owned(value),
)
}
_ => RedactedEnvPair::new(
log_safe_owned(name.to_string_lossy().into_owned()),
log_safe_owned(
self.redactor.policy().masking().mask_opaque_bounded(
Sensitivity::Secret,
max_mask_bytes,
),
),
),
};
pair.to_string()
}
}
impl Default for EnvRedactor {
fn default() -> Self {
Self::new(Redactor::default())
}
}
#[inline(always)]
fn log_safe_owned(value: String) -> LogSafeText<'static> {
RedactedText::new(Cow::Owned(value)).escape_for_log()
}
fn write_debug_item(
writer: &mut BoundedLogEscapeWriter,
has_item: &mut bool,
item: &str,
) {
if *has_item {
let _ = writer.write_str(", ");
}
let _ = write!(writer, "{item:?}");
*has_item = true;
}