use std::ffi::OsStr;
use crate::policy::{
DiagnosticInputBudget,
OutputCharge,
};
use crate::{
RedactionSession,
Redactor,
Sensitivity,
policy::ResolvedField,
};
use super::{
ArgvItem,
RedactedArgv,
pending_field::PendingField,
redacted_argv_builder::TRUNCATED_ITEM,
};
#[must_use = "use the redactor to produce a safe argv rendering"]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArgvRedactor {
redactor: Redactor,
}
impl ArgvRedactor {
#[inline(always)]
pub const fn new(redactor: Redactor) -> Self {
Self { redactor }
}
#[inline(always)]
pub const fn redactor(&self) -> &Redactor {
&self.redactor
}
pub fn redact_items<'a, I>(&self, items: I) -> RedactedArgv
where
I: IntoIterator<Item = ArgvItem<'a>>,
{
let session = RedactionSession::diagnostic(self.redactor.policy());
self.redact_items_with_session(items, &session)
}
pub(crate) fn redact_items_with_input_budget<'a, I>(
&self,
items: I,
input_budget: &mut DiagnosticInputBudget,
) -> RedactedArgv
where
I: IntoIterator<Item = ArgvItem<'a>>,
{
let mut rendered = RedactedArgv::builder(
self.redactor.policy().limits().diagnostic_event(),
);
for item in items {
if !input_budget.reserve(item.value().as_encoded_bytes().len()) {
let _ = rendered.push(TRUNCATED_ITEM);
break;
}
if !rendered.push(&self.render_explicit_or_plain(item)) {
break;
}
}
rendered.finish()
}
pub fn redact_heuristically<'a, I>(&self, items: I) -> RedactedArgv
where
I: IntoIterator<Item = ArgvItem<'a>>,
{
let session = RedactionSession::diagnostic(self.redactor.policy());
self.redact_heuristically_with_session(items, &session)
}
pub fn redact_items_with_session<'a, I>(
&self,
items: I,
session: &RedactionSession<'_>,
) -> RedactedArgv
where
I: IntoIterator<Item = ArgvItem<'a>>,
{
self.render_with_session(items, session, |redactor, items, budget| {
redactor.redact_items_with_input_budget(items, budget)
})
}
pub fn redact_heuristically_with_session<'a, I>(
&self,
items: I,
session: &RedactionSession<'_>,
) -> RedactedArgv
where
I: IntoIterator<Item = ArgvItem<'a>>,
{
self.render_with_session(items, session, |redactor, items, budget| {
redactor.redact_heuristically_with_input_budget(items, budget)
})
}
fn render_with_session<'a, I, F>(
&self,
items: I,
session: &RedactionSession<'_>,
render: F,
) -> RedactedArgv
where
I: IntoIterator<Item = ArgvItem<'a>>,
F: FnOnce(&Self, I, &mut DiagnosticInputBudget) -> RedactedArgv,
{
let available = session.remaining_input_bytes();
let mut input_budget = DiagnosticInputBudget::new(available);
let result = render(self, items, &mut input_budget);
let consumed =
available.saturating_sub(input_budget.remaining_input_bytes());
if input_budget.remaining_input_bytes() == 0 {
let _ = session.consume_input(available);
} else {
let _ = session.consume_input(consumed);
}
match session.charge_output_or_fallback(
result.as_log_safe_text().as_str().len(),
TRUNCATED_ITEM.len(),
) {
OutputCharge::Complete => result,
OutputCharge::Fallback => {
RedactedArgv::from_rendered(TRUNCATED_ITEM.to_owned())
}
OutputCharge::Exhausted => {
RedactedArgv::from_rendered(String::new())
}
}
}
pub(crate) fn redact_heuristically_with_input_budget<'a, I>(
&self,
items: I,
input_budget: &mut DiagnosticInputBudget,
) -> RedactedArgv
where
I: IntoIterator<Item = ArgvItem<'a>>,
{
let mut rendered = RedactedArgv::builder(
self.redactor.policy().limits().diagnostic_event(),
);
let mut pending_field = None;
for item in items {
if !input_budget.reserve(item.value().as_encoded_bytes().len()) {
let _ = rendered.push(TRUNCATED_ITEM);
break;
}
if let Some(level) = item.sensitivity() {
pending_field = None;
if !rendered.push(&self.mask_os_value(item.value(), level)) {
break;
}
continue;
}
if !rendered
.push(&self.redact_plain_item(item.value(), &mut pending_field))
{
break;
}
}
rendered.finish()
}
#[inline]
fn render_explicit_or_plain(&self, item: ArgvItem<'_>) -> String {
match item.sensitivity() {
Some(level) => self.mask_os_value(item.value(), level),
None => item.value().to_string_lossy().into_owned(),
}
}
#[inline]
fn mask_os_value(&self, value: &OsStr, level: Sensitivity) -> String {
match value.to_str() {
Some(value) => self
.redactor
.policy()
.masking()
.mask_bounded(level, value, self.mask_output_limit())
.into_owned(),
None => self.mask_opaque_value(),
}
}
fn redact_plain_item(
&self,
value: &OsStr,
pending_field: &mut Option<PendingField>,
) -> String {
let Some(value) = value.to_str() else {
*pending_field = Some(PendingField {
field: String::new(),
exact: false,
});
return self.mask_opaque_value();
};
let option = self.option_field(value);
if let Some(pending) = pending_field.take() {
if let Some((field, exact)) = option
&& self.option_is_sensitive(field, exact)
{
*pending_field = Some(PendingField {
field: field.to_owned(),
exact,
});
}
if pending.field.is_empty() {
return self.mask_opaque_value();
}
return self.mask_pending_value(&pending, value);
}
if let Some(value) = self.redact_assignment(value) {
return value;
}
if let Some(value) = self.redact_inline_option(value) {
return value;
}
if let Some(value) = self.redact_jvm_property(value) {
return value;
}
if let Some((field, exact)) = option
&& self.option_is_sensitive(field, exact)
{
*pending_field = Some(PendingField {
field: field.to_owned(),
exact,
});
}
value.to_owned()
}
#[inline]
fn option_field<'a>(&self, value: &'a str) -> Option<(&'a str, bool)> {
let name = option_name(value)?;
if value.starts_with("--") {
Some((name, false))
} else {
Some((name, true))
}
}
fn option_is_sensitive(&self, field: &str, exact: bool) -> bool {
if exact {
self.redactor
.policy()
.sensitivity_for_exact(field)
.is_some()
} else {
self.redactor.policy().sensitivity_for(field).is_some()
}
}
fn redact_assignment(&self, value: &str) -> Option<String> {
if value.starts_with('-') {
return None;
}
let (name, raw_value) = value.split_once('=')?;
if name.is_empty() {
return None;
}
let redacted = self.mask_field_value(name, raw_value)?;
Some(format!("{name}={redacted}"))
}
#[inline]
fn redact_inline_option(&self, value: &str) -> Option<String> {
if !value.starts_with("--") {
return None;
}
let (left, raw_value) = value.split_once('=')?;
let name = option_name(left)?;
let redacted = self.mask_field_value(name, raw_value)?;
Some(format!("{left}={redacted}"))
}
fn redact_jvm_property(&self, value: &str) -> Option<String> {
let property = value.strip_prefix("-D")?;
let (name, raw_value) = property.split_once('=')?;
if name.is_empty() {
return None;
}
let redacted = self.mask_field_value(name, raw_value)?;
Some(format!("-D{name}={redacted}"))
}
fn mask_pending_value(
&self,
pending: &PendingField,
value: &str,
) -> String {
let resolved = if pending.exact {
self.redactor.policy().resolve_field_exact(&pending.field)
} else {
self.redactor.policy().resolve_field(&pending.field)
};
match resolved {
ResolvedField::Sensitive { sensitivity } => self
.redactor
.policy()
.masking()
.mask_bounded(sensitivity, value, self.mask_output_limit())
.into_owned(),
ResolvedField::PassThrough => value.to_owned(),
}
}
fn mask_field_value(&self, field: &str, value: &str) -> Option<String> {
let resolved = self.redactor.policy().resolve_field(field);
match resolved {
ResolvedField::Sensitive { sensitivity } => Some(
self.redactor
.policy()
.masking()
.mask_bounded(sensitivity, value, self.mask_output_limit())
.into_owned(),
),
ResolvedField::PassThrough => None,
}
}
#[inline(always)]
fn mask_opaque_value(&self) -> String {
self.redactor
.policy()
.masking()
.mask_opaque_bounded(Sensitivity::Secret, self.mask_output_limit())
}
#[inline(always)]
fn mask_output_limit(&self) -> usize {
self.redactor
.policy()
.limits()
.diagnostic_event()
.max_output_bytes()
}
}
impl Default for ArgvRedactor {
#[inline(always)]
fn default() -> Self {
Self::new(Redactor::default())
}
}
#[inline]
fn option_name(value: &str) -> Option<&str> {
if !value.starts_with('-') || value == "-" || value.contains('=') {
return None;
}
let name = value.trim_start_matches('-');
if name.is_empty() { None } else { Some(name) }
}