use std::{
borrow::Cow,
ffi::OsStr,
fmt::Write as _,
};
use crate::policy::{
DiagnosticInputBudget,
OutputCharge,
};
use crate::{
LogOutputLimit,
LogSafeText,
RedactedText,
RedactionSession,
Redactor,
Sensitivity,
policy::ResolvedField,
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 session = RedactionSession::operation(self.redactor.policy());
self.redact_pair_with_session(name, value, &session)
}
#[must_use = "use the returned redacted environment pair"]
pub fn redact_pair_with_session(
&self,
name: &str,
value: &str,
session: &RedactionSession<'_>,
) -> RedactedEnvPair {
const FALLBACK: &str = "<redacted>=<redacted>";
if !session.consume_input(name.len().saturating_add(value.len())) {
return match session
.charge_output_or_fallback(FALLBACK.len(), FALLBACK.len())
{
OutputCharge::Complete => {
RedactedEnvPair::from_rendered(FALLBACK.to_owned())
}
OutputCharge::Fallback | OutputCharge::Exhausted => {
RedactedEnvPair::from_rendered(String::new())
}
};
}
let value = self.redactor.redact_field(name, value).into_owned();
let name = log_safe_owned(name.to_owned());
let pair = RedactedEnvPair::new(name, log_safe_owned(value));
let rendered = pair.to_string();
match session.charge_output_or_fallback(rendered.len(), FALLBACK.len())
{
OutputCharge::Complete => pair,
OutputCharge::Fallback => {
RedactedEnvPair::from_rendered(FALLBACK.to_owned())
}
OutputCharge::Exhausted => {
RedactedEnvPair::from_rendered(String::new())
}
}
}
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 session = RedactionSession::diagnostic(self.redactor.policy());
self.redact_os_pairs_with_session(pairs, &session)
}
pub fn redact_os_pairs_with_session<'a, I>(
&self,
pairs: I,
session: &RedactionSession<'_>,
) -> LogSafeText<'static>
where
I: IntoIterator<Item = (&'a OsStr, &'a OsStr)>,
{
let available = session.remaining_input_bytes();
let mut input_budget = DiagnosticInputBudget::new(available);
let result =
self.redact_os_pairs_with_input_budget(pairs, &mut input_budget);
let consumed =
available.saturating_sub(input_budget.remaining_input_bytes());
let _ = session.consume_input(
if input_budget.remaining_input_bytes() == 0 {
available
} else {
consumed
},
);
const LIMIT_MARKER: &str = "<redacted: diagnostic limit exceeded>";
match session.charge_output_or_fallback(
result.as_str().len(),
LIMIT_MARKER.len(),
) {
OutputCharge::Complete => result,
OutputCharge::Fallback => log_safe_owned(LIMIT_MARKER.to_owned()),
OutputCharge::Exhausted => log_safe_owned(String::new()),
}
}
pub(crate) 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().limits().diagnostic_event();
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 resolved = self.redactor.policy().resolve_field(name);
let value = match resolved {
ResolvedField::Sensitive { sensitivity } => self
.redactor
.policy()
.masking()
.mask_bounded(sensitivity, value, max_mask_bytes)
.into_owned(),
ResolvedField::PassThrough => 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;
}