qubit-redact 0.8.1

Rule-driven redaction for fields, diagnostics, HTTP data, and Rust domain objects
Documentation
// =============================================================================
//    Copyright (c) 2025 - 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! Non-rendering sensitivity inspection for URI components.

use fluent_uri::Uri;

use super::UriFragmentPolicy;
use super::UriPathPolicy;
use super::redaction::decode_uri_component;
use super::uri_redaction_writer::admit_uri_structure;
use crate::RedactionReason;
use crate::Sensitivity;
use crate::policy::ResolvedField;
use crate::runtime::InspectionSession;
use crate::runtime::runtime_session::RuntimeSession;

/// Parses and completely classifies one URI under the active policy.
pub(crate) fn inspect_uri(session: &mut InspectionSession, input: &str) {
    if !session.admit_input(input.len()) || !admit_uri_structure(session, input) {
        return;
    }
    let Ok(parsed) = Uri::<&str>::parse(input) else {
        session.fail_inspection(RedactionReason::InvalidUri);
        return;
    };
    if let Some(authority) = parsed.authority()
        && inspect_authority(session, authority.as_str()).is_err()
    {
        session.fail_inspection(RedactionReason::InvalidUri);
        return;
    }
    let path = parsed.path().as_str();
    if session.policy().uri().path_policy() == UriPathPolicy::Redact && !path.is_empty() && path != "/" {
        session.observe_sensitivity(Sensitivity::High);
    }
    if let Some(query) = parsed.query()
        && inspect_query(session, query.as_str()).is_err()
    {
        session.fail_inspection(RedactionReason::InvalidUri);
        return;
    }
    if let Some(fragment) = parsed.fragment()
        && session.policy().uri().fragment_policy() == UriFragmentPolicy::Redact
        && !fragment.as_str().is_empty()
    {
        session.observe_sensitivity(Sensitivity::High);
    }
}

/// Classifies `authority` userinfo and records sensitivity in `session`.
///
/// Returns success when no userinfo exists or all components decode. Returns
/// value-free `Err(())` for malformed percent escapes or decoded non-UTF-8
/// bytes.
fn inspect_authority(session: &mut InspectionSession, authority: &str) -> Result<(), ()> {
    let Some((userinfo, _)) = authority.rsplit_once('@') else {
        return Ok(());
    };
    let (username, password) = userinfo
        .split_once(':')
        .map_or((userinfo, None), |(username, password)| (username, Some(password)));
    inspect_named_component(session, "username", username)?;
    if let Some(password) = password {
        inspect_named_component(session, "password", password)?;
    }
    Ok(())
}

/// Decodes `query` keys and values and records classified sensitivity in
/// `session`.
///
/// Returns success after traversal, or value-free `Err(())` for malformed
/// percent escapes or decoded non-UTF-8 bytes. Earlier sensitivity observations
/// remain.
fn inspect_query(session: &mut InspectionSession, query: &str) -> Result<(), ()> {
    for pair in query.split('&') {
        let Some((raw_key, raw_value)) = pair.split_once('=') else {
            let _ = decode_uri_component(pair)?;
            continue;
        };
        let key = decode_uri_component(raw_key)?;
        let _ = decode_uri_component(raw_value)?;
        if let ResolvedField::Sensitive { sensitivity } = session.policy().resolve_field(&key) {
            session.observe_sensitivity(sensitivity);
        }
    }
    Ok(())
}

/// Validates `raw` and records the policy decision for `field` in `session`.
///
/// Returns success after observation, or value-free `Err(())` for malformed
/// percent escapes or decoded non-UTF-8 bytes before observing the field.
fn inspect_named_component(session: &mut InspectionSession, field: &str, raw: &str) -> Result<(), ()> {
    let _ = decode_uri_component(raw)?;
    if let ResolvedField::Sensitive { sensitivity } = session.policy().resolve_field(field) {
        session.observe_sensitivity(sensitivity);
    }
    Ok(())
}