use std::ffi::OsStr;
use crate::{
DiagnosticInputBudget,
Redactor,
Sensitivity,
};
use super::{
ArgvItem,
RedactedArgv,
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 mut input_budget =
self.redactor.policy().diagnostic_budget().input_budget();
self.redact_items_with_input_budget(items, &mut input_budget)
}
pub 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().diagnostic_budget());
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 mut input_budget =
self.redactor.policy().diagnostic_budget().input_budget();
self.redact_heuristically_with_input_budget(items, &mut input_budget)
}
pub 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().diagnostic_budget());
let mut pending_sensitivity = 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_sensitivity = None;
if !rendered.push(&self.mask_os_value(item.value(), level)) {
break;
}
continue;
}
if !rendered.push(
&self.redact_plain_item(item.value(), &mut pending_sensitivity),
) {
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_sensitivity: &mut Option<Sensitivity>,
) -> String {
let Some(value) = value.to_str() else {
let encoded = value.as_encoded_bytes();
let may_take_separate_value =
encoded.starts_with(b"-") && !encoded.contains(&b'=');
*pending_sensitivity =
may_take_separate_value.then_some(Sensitivity::Secret);
return self.mask_opaque_value();
};
let option_sensitivity = self.option_sensitivity(value);
if let Some(pending) = pending_sensitivity.take() {
if let Some(level) = option_sensitivity {
*pending_sensitivity = Some(level);
}
return self.mask_utf8_value(value, pending);
}
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(level) = option_sensitivity {
*pending_sensitivity = Some(level);
}
value.to_owned()
}
#[inline]
fn option_sensitivity(&self, value: &str) -> Option<Sensitivity> {
let name = option_name(value)?;
if value.starts_with("--") {
self.redactor.policy().sensitivity_for(name)
} else {
self.redactor.policy().sensitivity_for_exact(name)
}
}
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 level = self.redactor.policy().sensitivity_for(name)?;
let redacted = self.mask_utf8_value(raw_value, level);
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 level = self.redactor.policy().sensitivity_for(name)?;
let redacted = self.mask_utf8_value(raw_value, level);
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 level = self.redactor.policy().sensitivity_for(name)?;
let redacted = self.mask_utf8_value(raw_value, level);
Some(format!("-D{name}={redacted}"))
}
#[inline(always)]
fn mask_utf8_value(&self, value: &str, level: Sensitivity) -> String {
self.redactor
.policy()
.masking()
.mask_bounded(level, value, self.mask_output_limit())
.into_owned()
}
#[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()
.diagnostic_budget()
.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) }
}