use std::{
borrow::Cow,
fmt::{
self,
Write,
},
};
use fluent_uri::Uri;
use crate::policy::OutputCharge;
use crate::{
RedactedText,
RedactionPolicy,
RedactionSession,
Sensitivity,
policy::ResolvedField,
};
use super::{
UriComponent,
UriFragmentPolicy,
UriInspection,
UriPathPolicy,
UriRedaction,
UriRedactionReason,
UriRedactionStatus,
};
use super::internal::{
BoundedUriWriter,
UriComponentWriter,
};
const INVALID_URI: &str = "<invalid URI>";
#[must_use]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UriRedactor {
policy: RedactionPolicy,
}
impl UriRedactor {
#[inline]
pub const fn new(policy: RedactionPolicy) -> Self {
Self { policy }
}
#[must_use = "use the URI policy snapshot"]
#[inline]
pub const fn policy(&self) -> &RedactionPolicy {
&self.policy
}
#[must_use = "use the structured URI redaction result"]
pub fn redact_uri_str(&self, input: &str) -> UriRedaction {
if input.len()
> self.policy.limits().diagnostic_event().max_input_bytes()
{
return invalid_result(UriRedactionReason::InputLimitExceeded);
}
let parsed = match Uri::<&str>::parse(input) {
Ok(parsed) => parsed,
Err(_) => return invalid_result(UriRedactionReason::InvalidUri),
};
let mut reasons = Vec::new();
let mut components = Vec::new();
let mut rendered = BoundedUriWriter::new(
self.policy.limits().diagnostic_event().max_output_bytes(),
);
let scheme = parsed.scheme().as_str();
let scheme_end = scheme.len() + 1;
rendered.write_str(&input[..scheme_end]);
if let Some(authority) = parsed.authority() {
rendered.write_str("//");
if redact_authority(
authority.as_str(),
&self.policy,
&mut reasons,
&mut components,
&mut rendered,
)
.is_err()
{
return invalid_result(UriRedactionReason::InvalidUri);
}
}
let path = parsed.path().as_str();
if self.policy.path_policy() == UriPathPolicy::Redact
&& !path.is_empty()
&& path != "/"
{
mark_component(UriComponent::Path, &mut reasons, &mut components);
if path.starts_with('/') {
rendered.write_str("/%3Credacted%3E");
} else {
rendered.write_str("%3Credacted%3E");
}
} else {
rendered.write_str(path);
}
if let Some(query) = parsed.query() {
rendered.write_str("?");
if let Err(reason) = redact_query(
query.as_str(),
&self.policy,
&mut reasons,
&mut components,
&mut rendered,
) {
return invalid_result(reason);
}
}
if let Some(fragment) = parsed.fragment() {
rendered.write_str("#");
if self.policy.fragment_policy() == UriFragmentPolicy::Redact
&& !fragment.as_str().is_empty()
{
mark_component(
UriComponent::Fragment,
&mut reasons,
&mut components,
);
write_opaque_mask(
&self.policy,
Sensitivity::High,
&mut rendered,
);
} else {
rendered.write_str(fragment.as_str());
}
}
let status = if components.is_empty() {
UriRedactionStatus::PassedThrough
} else {
UriRedactionStatus::Redacted
};
let (safe, truncated) = rendered.finish();
if truncated {
reasons.push(UriRedactionReason::OutputTruncated);
}
UriRedaction {
text: safe_text(safe),
status,
reasons,
components,
truncated,
}
}
#[must_use = "use the structured URI inspection result"]
pub fn inspect_uri_str(&self, input: &str) -> UriInspection {
let budget = self.policy.limits().diagnostic_event();
if input.len() > budget.max_input_bytes() {
return invalid_inspection(UriRedactionReason::InputLimitExceeded);
}
let parsed = match Uri::<&str>::parse(input) {
Ok(parsed) => parsed,
Err(_) => {
return invalid_inspection(UriRedactionReason::InvalidUri);
}
};
let mut reasons = Vec::new();
let mut components = Vec::new();
if let Some(authority) = parsed.authority()
&& inspect_authority(
authority.as_str(),
&self.policy,
&mut reasons,
&mut components,
)
.is_err()
{
return invalid_inspection(UriRedactionReason::InvalidUri);
}
let path = parsed.path().as_str();
if self.policy.path_policy() == UriPathPolicy::Redact
&& !path.is_empty()
&& path != "/"
{
mark_component(UriComponent::Path, &mut reasons, &mut components);
}
if let Some(query) = parsed.query()
&& let Err(reason) = inspect_query(
query.as_str(),
&self.policy,
&mut reasons,
&mut components,
)
{
return invalid_inspection(reason);
}
if let Some(fragment) = parsed.fragment()
&& self.policy.fragment_policy() == UriFragmentPolicy::Redact
&& !fragment.as_str().is_empty()
{
mark_component(
UriComponent::Fragment,
&mut reasons,
&mut components,
);
}
let status = if components.is_empty() {
UriRedactionStatus::PassedThrough
} else {
UriRedactionStatus::Redacted
};
UriInspection {
status,
reasons,
components,
}
}
#[must_use = "use the session-bounded URI result"]
pub fn redact_uri_str_with_session(
&self,
input: &str,
session: &RedactionSession<'_>,
) -> UriRedaction {
if !session.consume_input(input.len()) {
return session_invalid_result(
session,
UriRedactionReason::InputLimitExceeded,
);
}
let result = self.redact_uri_str(input);
match session.charge_output_or_fallback(
result.log_safe_text().as_str().len(),
INVALID_URI.len(),
) {
OutputCharge::Complete => result,
OutputCharge::Fallback => {
invalid_result(UriRedactionReason::OutputTruncated)
}
OutputCharge::Exhausted => {
empty_invalid_result(UriRedactionReason::OutputTruncated)
}
}
}
#[must_use = "use the session-bounded URI inspection"]
pub fn inspect_uri_str_with_session(
&self,
input: &str,
session: &RedactionSession<'_>,
) -> UriInspection {
if !session.consume_input(input.len()) {
return invalid_inspection(UriRedactionReason::InputLimitExceeded);
}
self.inspect_uri_str(input)
}
}
impl Default for UriRedactor {
#[inline]
fn default() -> Self {
Self::new(RedactionPolicy::default())
}
}
fn redact_authority(
authority: &str,
policy: &RedactionPolicy,
reasons: &mut Vec<UriRedactionReason>,
components: &mut Vec<UriComponent>,
rendered: &mut BoundedUriWriter,
) -> Result<(), ()> {
let Some((userinfo, host)) = authority.rsplit_once('@') else {
rendered.write_str(authority);
return Ok(());
};
let Some((username, password)) = userinfo.split_once(':') else {
redact_userinfo_value(
userinfo,
"username",
UriComponent::Username,
policy,
reasons,
components,
rendered,
)?;
rendered.write_str("@");
rendered.write_str(host);
return Ok(());
};
redact_userinfo_value(
username,
"username",
UriComponent::Username,
policy,
reasons,
components,
rendered,
)?;
rendered.write_str(":");
redact_userinfo_value(
password,
"password",
UriComponent::Password,
policy,
reasons,
components,
rendered,
)?;
rendered.write_str("@");
rendered.write_str(host);
Ok(())
}
fn inspect_authority(
authority: &str,
policy: &RedactionPolicy,
reasons: &mut Vec<UriRedactionReason>,
components: &mut Vec<UriComponent>,
) -> Result<(), ()> {
let Some((userinfo, _host)) = authority.rsplit_once('@') else {
return Ok(());
};
let Some((username, password)) = userinfo.split_once(':') else {
inspect_userinfo_value(
userinfo,
"username",
UriComponent::Username,
policy,
reasons,
components,
)?;
return Ok(());
};
inspect_userinfo_value(
username,
"username",
UriComponent::Username,
policy,
reasons,
components,
)?;
inspect_userinfo_value(
password,
"password",
UriComponent::Password,
policy,
reasons,
components,
)?;
Ok(())
}
fn inspect_userinfo_value(
raw: &str,
field: &str,
component: UriComponent,
policy: &RedactionPolicy,
reasons: &mut Vec<UriRedactionReason>,
components: &mut Vec<UriComponent>,
) -> Result<(), ()> {
decode_uri_component(raw).map_err(|_| ())?;
if matches!(policy.resolve_field(field), ResolvedField::Sensitive { .. }) {
mark_component(component, reasons, components);
}
Ok(())
}
fn redact_userinfo_value(
raw: &str,
field: &str,
component: UriComponent,
policy: &RedactionPolicy,
reasons: &mut Vec<UriRedactionReason>,
components: &mut Vec<UriComponent>,
rendered: &mut BoundedUriWriter,
) -> Result<(), ()> {
let decoded = decode_uri_component(raw).map_err(|_| ())?;
match policy.resolve_field(field) {
ResolvedField::Sensitive { sensitivity } => {
mark_component(component, reasons, components);
write_sensitive_value(policy, sensitivity, &decoded, rendered);
}
ResolvedField::PassThrough => {
rendered.write_str(raw);
}
}
Ok(())
}
fn redact_query(
query: &str,
policy: &RedactionPolicy,
reasons: &mut Vec<UriRedactionReason>,
components: &mut Vec<UriComponent>,
rendered: &mut BoundedUriWriter,
) -> Result<(), UriRedactionReason> {
for (index, pair) in query.split('&').enumerate() {
if index != 0 {
rendered.write_str("&");
}
let Some((raw_key, raw_value)) = pair.split_once('=') else {
decode_uri_component(pair)
.map_err(|_| UriRedactionReason::UndecodableQueryKey)?;
rendered.write_str(pair);
continue;
};
let key = decode_uri_component(raw_key)
.map_err(|_| UriRedactionReason::UndecodableQueryKey)?;
let value = decode_uri_component(raw_value)
.map_err(|_| UriRedactionReason::UndecodableQueryValue)?;
match policy.resolve_field(&key) {
ResolvedField::Sensitive { sensitivity } => {
mark_component(UriComponent::Query, reasons, components);
rendered.write_str(raw_key);
rendered.write_str("=");
write_sensitive_value(policy, sensitivity, &value, rendered);
}
ResolvedField::PassThrough => {
rendered.write_str(pair);
}
}
}
Ok(())
}
fn inspect_query(
query: &str,
policy: &RedactionPolicy,
reasons: &mut Vec<UriRedactionReason>,
components: &mut Vec<UriComponent>,
) -> Result<(), UriRedactionReason> {
for pair in query.split('&') {
let Some((raw_key, raw_value)) = pair.split_once('=') else {
decode_uri_component(pair)
.map_err(|_| UriRedactionReason::UndecodableQueryKey)?;
continue;
};
let key = decode_uri_component(raw_key)
.map_err(|_| UriRedactionReason::UndecodableQueryKey)?;
decode_uri_component(raw_value)
.map_err(|_| UriRedactionReason::UndecodableQueryValue)?;
if matches!(policy.resolve_field(&key), ResolvedField::Sensitive { .. })
{
mark_component(UriComponent::Query, reasons, components);
}
}
Ok(())
}
fn decode_uri_component(raw: &str) -> Result<String, ()> {
let bytes = raw.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'%' {
if index + 2 >= bytes.len() {
return Err(());
}
let high = hex_value(bytes[index + 1]).ok_or(())?;
let low = hex_value(bytes[index + 2]).ok_or(())?;
decoded.push((high << 4) | low);
index += 3;
} else {
decoded.push(bytes[index]);
index += 1;
}
}
String::from_utf8(decoded).map_err(|_| ())
}
fn write_sensitive_value(
policy: &RedactionPolicy,
sensitivity: Sensitivity,
value: &str,
rendered: &mut BoundedUriWriter,
) {
if value.is_empty() || rendered.is_full() {
return;
}
let mut writer = UriComponentWriter::new(rendered);
let _ = policy
.masking()
.for_level(sensitivity)
.write_masked(value, &mut writer);
}
fn write_opaque_mask(
policy: &RedactionPolicy,
sensitivity: Sensitivity,
rendered: &mut BoundedUriWriter,
) {
if rendered.is_full() {
return;
}
let mut writer = UriComponentWriter::new(rendered);
let _ =
writer.write_str(policy.masking().for_level(sensitivity).opaque_mask());
}
const fn hex_value(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
fn mark_component(
component: UriComponent,
reasons: &mut Vec<UriRedactionReason>,
components: &mut Vec<UriComponent>,
) {
if !components.contains(&component) {
components.push(component);
}
let reason = UriRedactionReason::SensitiveComponent(component);
if !reasons.contains(&reason) {
reasons.push(reason);
}
}
fn invalid_result(reason: UriRedactionReason) -> UriRedaction {
UriRedaction {
text: safe_text(INVALID_URI.to_owned()),
status: UriRedactionStatus::Invalid,
reasons: vec![reason],
components: Vec::new(),
truncated: false,
}
}
fn session_invalid_result(
session: &RedactionSession<'_>,
reason: UriRedactionReason,
) -> UriRedaction {
match session
.charge_output_or_fallback(INVALID_URI.len(), INVALID_URI.len())
{
OutputCharge::Complete => invalid_result(reason),
OutputCharge::Fallback | OutputCharge::Exhausted => {
empty_invalid_result(reason)
}
}
}
fn empty_invalid_result(reason: UriRedactionReason) -> UriRedaction {
UriRedaction {
text: safe_text(String::new()),
status: UriRedactionStatus::Invalid,
reasons: vec![reason],
components: Vec::new(),
truncated: true,
}
}
fn invalid_inspection(reason: UriRedactionReason) -> UriInspection {
UriInspection {
status: UriRedactionStatus::Invalid,
reasons: vec![reason],
components: Vec::new(),
}
}
fn safe_text(value: String) -> crate::LogSafeText<'static> {
RedactedText::new(Cow::Owned(value)).escape_for_log()
}
impl fmt::Display for UriRedactor {
#[inline]
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("UriRedactor")
}
}