use regex::Regex;
use std::sync::OnceLock;
const DEFAULT_PREVIEW_LEN: usize = 4000;
const ESC: u8 = 0x1b;
const BEL: u8 = 0x07;
const CSI_FINAL_LOW: u8 = 0x40;
const CSI_FINAL_HIGH: u8 = 0x7e;
pub fn preview_len() -> usize {
std::env::var("LOOPRS_PREVIEW_LEN")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|n| *n > 0)
.unwrap_or(DEFAULT_PREVIEW_LEN)
}
pub fn allow_raw_output() -> bool {
if !cfg!(debug_assertions) {
return false;
}
let explicitly_enabled = std::env::var("LOOPRS_ALLOW_RAW_OUTPUT")
.ok()
.is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true"));
if !explicitly_enabled {
return false;
}
let env = std::env::var("LOOPRS_ENV")
.unwrap_or_default()
.to_ascii_lowercase();
!matches!(env.as_str(), "prod" | "production")
}
pub fn strip_ansi(input: &str) -> String {
let bytes = input.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == ESC {
if i + 1 >= bytes.len() {
break;
}
match bytes[i + 1] {
b'[' => {
i += 2;
while i < bytes.len() {
let b = bytes[i];
i += 1;
if (CSI_FINAL_LOW..=CSI_FINAL_HIGH).contains(&b) {
break;
}
}
continue;
}
b']' => {
i += 2;
while i < bytes.len() {
if bytes[i] == BEL {
i += 1;
break;
}
if bytes[i] == ESC && i + 1 < bytes.len() && bytes[i + 1] == b'\\' {
i += 2;
break;
}
i += 1;
}
continue;
}
b'P' | b'_' | b'^' | b'X' => {
i += 2;
while i < bytes.len() {
if bytes[i] == ESC && i + 1 < bytes.len() && bytes[i + 1] == b'\\' {
i += 2;
break;
}
i += 1;
}
continue;
}
_ => {
i += 2;
continue;
}
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).to_string()
}
pub fn sanitize_for_console(input: &str) -> String {
if allow_raw_output() {
return input.to_string();
}
let no_ansi = strip_ansi(input);
redact(&no_ansi)
}
pub fn sanitize_preview_for_console(input: &str) -> String {
let sanitized = sanitize_for_console(input);
truncate_chars(&sanitized, preview_len())
}
fn truncate_chars(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
return s.to_string();
}
let truncated: String = s.chars().take(max_chars).collect();
format!(
"{truncated}... [truncated {} chars]",
s.chars().count().saturating_sub(max_chars)
)
}
fn redact(input: &str) -> String {
static PEM_RE: OnceLock<Regex> = OnceLock::new();
static SK_RE: OnceLock<Regex> = OnceLock::new();
static URL_CREDS_RE: OnceLock<Regex> = OnceLock::new();
static AUTH_RE: OnceLock<Regex> = OnceLock::new();
static JWT_RE: OnceLock<Regex> = OnceLock::new();
static PROVIDER_TOKEN_RE: OnceLock<Regex> = OnceLock::new();
static KV_RE: OnceLock<Regex> = OnceLock::new();
let pem_re = PEM_RE.get_or_init(|| {
Regex::new(
r"(?s)-----BEGIN (?:[A-Z0-9 ]*PRIVATE KEY|OPENSSH PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----.*?-----END (?:[A-Z0-9 ]*PRIVATE KEY|OPENSSH PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----",
)
.expect("pem regex")
});
let sk_re = SK_RE.get_or_init(|| Regex::new(r"\bsk-[A-Za-z0-9_\-]{10,}\b").expect("sk regex"));
let url_creds_re = URL_CREDS_RE
.get_or_init(|| Regex::new(r"(https?://)([^/\s@]+)@").expect("url creds regex"));
let auth_re = AUTH_RE.get_or_init(|| {
Regex::new(r"(?im)(\bauthorization\b\s*[:=]\s*)([^\r\n]+)").expect("auth regex")
});
let jwt_re = JWT_RE.get_or_init(|| {
Regex::new(r"\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b")
.expect("jwt regex")
});
let provider_token_re = PROVIDER_TOKEN_RE.get_or_init(|| {
Regex::new(
r"\b(?:ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9\-]{10,}|AKIA[0-9A-Z]{16})\b",
)
.expect("provider token regex")
});
let kv_re = KV_RE.get_or_init(|| {
Regex::new(
r#"(?im)(\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|secret|password|passwd|pwd|client[_-]?secret)\b\s*[:=]\s*)(?:\"[^\"\r\n]*\"|'[^'\r\n]*'|[^\r\n,}]+)"#,
)
.expect("kv regex")
});
let mut s = input.to_string();
s = pem_re.replace_all(&s, "[REDACTED PRIVATE KEY]").to_string();
s = url_creds_re.replace_all(&s, "$1[REDACTED]@").to_string();
s = auth_re.replace_all(&s, "$1[REDACTED]").to_string();
s = sk_re.replace_all(&s, "sk-[REDACTED]").to_string();
s = jwt_re.replace_all(&s, "[REDACTED JWT]").to_string();
s = provider_token_re
.replace_all(&s, "[REDACTED TOKEN]")
.to_string();
s = kv_re.replace_all(&s, "$1[REDACTED]").to_string();
s
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
#[kani::unwind(12)]
fn strip_ansi_no_oob() {
let len: usize = kani::any();
kani::assume(len <= 8);
let mut buf = [0u8; 8];
for i in 0..len {
buf[i] = kani::any();
}
let s = match std::str::from_utf8(&buf[..len]) {
Ok(s) => s,
Err(_) => return, };
let result = strip_ansi(s);
assert!(!result.as_bytes().iter().any(|&b| b == ESC));
}
#[kani::proof]
#[kani::unwind(10)]
fn truncate_chars_no_overflow() {
let max: usize = kani::any();
kani::assume(max <= 5);
let s = "abcde";
let result = truncate_chars(s, max);
if max >= 5 {
assert!(result.len() == s.len());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn strip_ansi_never_panics(s in "\\PC*") {
let _ = strip_ansi(&s);
}
#[test]
fn strip_ansi_is_idempotent(s in "\\PC*") {
let once = strip_ansi(&s);
let twice = strip_ansi(&once);
prop_assert_eq!(&once, &twice);
}
#[test]
fn strip_ansi_output_contains_no_esc(s in "\\PC*") {
let out = strip_ansi(&s);
prop_assert!(
!out.contains('\x1b'),
"output still contains ESC: {:?}", out
);
}
#[test]
fn redact_is_idempotent(s in "\\PC{0,500}") {
let once = redact(&s);
let twice = redact(&once);
prop_assert_eq!(&once, &twice);
}
#[test]
fn redact_never_panics(s in "\\PC{0,500}") {
let _ = redact(&s);
}
#[test]
fn truncate_respects_limit(s in "\\PC{0,200}", max in 0usize..300) {
let out = truncate_chars(&s, max);
if s.chars().count() <= max {
prop_assert_eq!(&out, &s);
} else {
let prefix: String = s.chars().take(max).collect();
prop_assert!(out.starts_with(&prefix));
prop_assert!(out.contains("[truncated"));
}
}
#[test]
fn sanitize_for_console_never_panics(s in "\\PC{0,500}") {
let _ = sanitize_for_console(&s);
}
}
#[test]
fn redacts_sk_tokens() {
let s = "token sk-abcdefghijklmnopqrstuvwxyz12345 end";
let out = sanitize_for_console(s);
assert!(out.contains("sk-[REDACTED]"));
assert!(!out.contains("sk-abcdefghijklmnopqrstuvwxyz"));
}
#[test]
fn redacts_key_value_pairs() {
let s = "OPENAI_API_KEY=sk-abcdef1234567890\nauthorization: Bearer abcdef\n";
let out = sanitize_for_console(s);
assert!(out.to_lowercase().contains("api_key"));
assert!(out.contains("[REDACTED]"));
}
#[test]
fn redacts_pem_private_key() {
let s = "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----";
let out = sanitize_for_console(s);
assert_eq!(out, "[REDACTED PRIVATE KEY]");
}
#[test]
fn redacts_url_credentials() {
let s = "https://user:pass@example.com/path";
let out = sanitize_for_console(s);
assert_eq!(out, "https://[REDACTED]@example.com/path");
}
#[test]
fn strips_ansi_sequences() {
let s = "\u{1b}[31mred\u{1b}[0m";
let out = strip_ansi(s);
assert_eq!(out, "red");
}
#[test]
fn strips_osc_sequences() {
let s = "before\u{1b}]0;window-title\u{7}after";
let out = strip_ansi(s);
assert_eq!(out, "beforeafter");
}
#[test]
fn redacts_authorization_header() {
let s = "Authorization: Bearer super-secret-token";
let out = sanitize_for_console(s);
assert_eq!(out, "Authorization: [REDACTED]");
}
#[test]
fn redacts_jwt_tokens() {
let s = "jwt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.abcdefghijklmno1234567890.pqrstuvwxyzABCDE1234567890";
let out = sanitize_for_console(s);
assert!(out.contains("[REDACTED JWT]"));
}
#[test]
fn redacts_provider_tokens() {
let s = "ghp_abcdefghijklmnopqrstuvwxyz123456 and AKIA1234567890ABCDEF";
let out = sanitize_for_console(s);
assert!(!out.contains("ghp_abcdefghijklmnopqrstuvwxyz123456"));
assert!(!out.contains("AKIA1234567890ABCDEF"));
assert!(out.contains("[REDACTED TOKEN]"));
}
}