qubit-redact 0.9.0

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.
// =============================================================================
//! Internal escaping for typed log-safe text.

use std::borrow::Cow;
use std::fmt;
use std::str;

/// Escapes characters that can alter log structure or visual ordering.
///
/// # Type Parameters
///
/// * `'a` - Lifetime of the input and any borrowed escaped result.
///
/// # Parameters
///
/// * `value` - Redacted text crossing a plain-text log boundary.
///
/// # Returns
///
/// The original `Cow` when every character is safe, preserving either its
/// borrowed or owned form; otherwise, a newly allocated escaped string.
#[must_use]
pub(crate) fn escape_log_control_characters<'a>(value: Cow<'a, str>) -> Cow<'a, str> {
    let Some((index, first_unsafe)) = value
        .char_indices()
        .find(|(_, character)| is_log_unsafe_character(*character))
    else {
        return value;
    };

    let mut escaped = String::with_capacity(value.len());
    escaped.push_str(&value[..index]);
    escaped.extend(first_unsafe.escape_debug());
    for character in value[index + first_unsafe.len_utf8()..].chars() {
        if is_log_unsafe_character(character) {
            escaped.extend(character.escape_debug());
        } else {
            escaped.push(character);
        }
    }
    Cow::Owned(escaped)
}

/// Reports whether a character can alter log structure or visual ordering.
///
/// # Parameters
///
/// * `character` - Character to classify at a text log boundary.
///
/// # Returns
///
/// `true` for control characters, Unicode line and paragraph separators, and
/// Unicode bidirectional formatting controls; otherwise, `false`.
#[must_use]
#[inline]
pub(crate) fn is_log_unsafe_character(character: char) -> bool {
    character.is_control()
        || matches!(
            character,
            '\u{061c}'
                | '\u{200e}'
                | '\u{200f}'
                | '\u{2028}'..='\u{202e}'
                | '\u{2066}'..='\u{2069}'
        )
}

/// Encodes one input character as one atomic log-safe piece.
///
/// # Parameters
///
/// * `character` - Input character to encode.
/// * `buffer` - Scratch storage large enough for any debug escape.
///
/// # Returns
///
/// A UTF-8 slice containing either the original character or its complete
/// debug escape.
///
/// # Errors
///
/// Returns [`fmt::Error`] if the internally generated bytes are not UTF-8.
pub(crate) fn encode_log_safe_character(character: char, buffer: &mut [u8; 12]) -> Result<&str, fmt::Error> {
    let length = if is_log_unsafe_character(character) {
        let mut length = 0;
        for escaped in character.escape_debug() {
            let encoded = escaped.encode_utf8(&mut buffer[length..]);
            length += encoded.len();
        }
        length
    } else {
        character.encode_utf8(buffer).len()
    };
    str::from_utf8(&buffer[..length]).map_err(|_| fmt::Error)
}