qubit_redact/lib.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8#![cfg_attr(
9 all(doctest, feature = "http", feature = "serde"),
10 doc = include_str!("../README.md")
11)]
12#![cfg_attr(
13 all(doctest, feature = "http", feature = "serde"),
14 doc = include_str!("../README.zh_CN.md")
15)]
16#![cfg_attr(
17 all(doctest, feature = "http", feature = "serde"),
18 doc = include_str!("../doc/user_guide.md")
19)]
20#![cfg_attr(
21 all(doctest, feature = "http", feature = "serde"),
22 doc = include_str!("../doc/user_guide.zh_CN.md")
23)]
24//! # Qubit Redact
25//!
26//! Provides immutable, policy-driven redaction for scalar fields, maps,
27//! process diagnostics, and optionally HTTP data. Safe result types separate
28//! redacted text from text that has also been escaped for logs.
29//!
30//! # Core values and maps
31//!
32//! ```
33//! use std::collections::HashMap;
34//! use qubit_redact::{RedactionPolicy, Redactor, Sensitivity};
35//!
36//! let policy = RedactionPolicy::builder()
37//! .raise("tenant_secret", Sensitivity::Secret)
38//! .build()?;
39//! let source = HashMap::from([
40//! ("tenant_secret".to_owned(), "raw".to_owned()),
41//! ("display_name".to_owned(), "Alice".to_owned()),
42//! ]);
43//! let redacted = Redactor::new(policy).redact_map(&source);
44//! assert_eq!(redacted["tenant_secret"], "<redacted>");
45//! assert_eq!(source["tenant_secret"], "raw");
46//! # Ok::<(), Box<dyn std::error::Error>>(())
47//! ```
48//!
49//! A process-wide default can be installed exactly once.
50//! [`RedactionPolicy::builder`] starts without field rules; use
51//! [`RedactionPolicy::builder_from_default`] to copy the current default
52//! snapshot. Existing policy snapshots never change.
53//!
54//! ```
55//! use qubit_redact::{RedactionPolicy, Sensitivity};
56//!
57//! let application_default = RedactionPolicy::builder()
58//! .raise("tenant_secret", Sensitivity::Secret)
59//! .build()?;
60//! RedactionPolicy::set_global_default(application_default)?;
61//! let snapshot = RedactionPolicy::builder_from_default().build()?;
62//! assert_eq!(snapshot.sensitivity_for("tenant_secret"), Some(Sensitivity::Secret));
63//! # Ok::<(), Box<dyn std::error::Error>>(())
64//! ```
65//!
66//! [`RedactedText`] is not directly displayable. Explicitly cross a plain-text
67//! logging boundary with [`RedactedText::escape_for_log`].
68//!
69//! ```
70//! use qubit_redact::Redactor;
71//!
72//! let safe = Redactor::default()
73//! .redact("message", "line one\nline two")
74//! .escape_for_log();
75//! assert_eq!(safe.to_string(), "line one\\nline two");
76//! ```
77//!
78//! # Domain objects
79//!
80//! Add the companion `qubit-redact-derive` crate to annotate fields explicitly.
81//! Plain fields are never recursively redacted, `nested` is the recursion
82//! boundary, `map` classifies each value by its runtime key, and `skip` omits a
83//! field only from the redacted representation.
84//!
85//! ```
86//! use std::collections::HashMap;
87//! use qubit_redact::{Redact as _, RedactionPolicy, Sensitivity};
88//! use qubit_redact_derive::Redact;
89//!
90//! #[derive(Redact)]
91//! struct Account {
92//! id: u64,
93//! #[redact(level = "secret")]
94//! password: String,
95//! #[redact(map)]
96//! metadata: HashMap<String, String>,
97//! }
98//!
99//! let policy = RedactionPolicy::builder()
100//! .raise("api_key", Sensitivity::Secret)
101//! .build()?;
102//! let account = Account {
103//! id: 1,
104//! password: "raw-password".to_owned(),
105//! metadata: HashMap::from([
106//! ("api_key".to_owned(), "raw-key".to_owned()),
107//! ]),
108//! };
109//! let output = format!("{:?}", account.redacted_with(&policy));
110//! assert!(!output.contains("raw-password"));
111//! assert!(!output.contains("raw-key"));
112//! # Ok::<(), Box<dyn std::error::Error>>(())
113//! ```
114//!
115//! `RedactMut` is an explicit logical in-place redaction contract. The skipped
116//! field below remains unchanged, while `nested` uses the same policy for the
117//! child. It does not zeroize released allocations or affect aliases, existing
118//! copies, or borrowed backing data. Clone-based `to_redacted` temporarily
119//! retains a second raw copy. Use a separately designed zeroization strategy
120//! when memory erasure is required.
121//!
122//! ```
123//! use qubit_redact::{Redact as _, RedactMut as _};
124//! use qubit_redact_derive::{Redact, RedactMut};
125//!
126//! #[derive(Clone, Redact, RedactMut)]
127//! struct Secret {
128//! #[redact(level = "secret")]
129//! value: String,
130//! }
131//!
132//! #[derive(Clone, Redact, RedactMut)]
133//! struct Envelope {
134//! #[redact(nested)]
135//! secret: Secret,
136//! #[redact(skip)]
137//! internal_note: String,
138//! }
139//!
140//! let mut envelope = Envelope {
141//! secret: Secret { value: "raw".to_owned() },
142//! internal_note: "unchanged".to_owned(),
143//! };
144//! envelope.redact_in_place();
145//! assert_eq!(envelope.secret.value, "<redacted>");
146//! assert_eq!(envelope.internal_note, "unchanged");
147//! ```
148//!
149//! With the `serde` feature, a direct `serde` dependency, and the companion
150//! derive crate, `#[redact(serde)]` opts the redacted view into serialization.
151//! [`Redacted`] intentionally does not implement `Deserialize`.
152//!
153//! ```
154//! # #[cfg(feature = "serde")]
155//! # {
156//! use qubit_redact::Redact as _;
157//! use qubit_redact_derive::Redact;
158//!
159//! #[derive(Redact)]
160//! #[redact(debug, display, serde)]
161//! struct Credentials {
162//! #[redact(level = "secret")]
163//! token: String,
164//! #[redact(skip)]
165//! internal_note: String,
166//! }
167//!
168//! let value = Credentials {
169//! token: "raw-token".to_owned(),
170//! internal_note: "not serialized".to_owned(),
171//! };
172//! let json = serde_json::to_string(&value.redacted())?;
173//! assert!(!json.contains("raw-token"));
174//! assert!(!json.contains("internal_note"));
175//! assert!(!format!("{value:?}").contains("raw-token"));
176//! assert!(!format!("{value}").contains("raw-token"));
177//! # }
178//! # Ok::<(), Box<dyn std::error::Error>>(())
179//! ```
180//!
181//! `debug` and `display` are opt-in implementations on the original type and
182//! use the process-wide default policy. Plain fields remain ordinary `Debug`
183//! values. Do not request an implementation already supplied by the type,
184//! such as combining `#[derive(Debug)]` with `#[redact(debug)]`.
185//!
186//! Derives support named, tuple, and unit structs, plus enums with named,
187//! tuple, and unit variants. With `#[redact(serde)]`, redacted serialization
188//! supports Serde's external, internal, adjacent, and untagged enum
189//! representations through a structure-preserving attribute allowlist.
190//!
191//! ```
192//! use qubit_redact::Redact as _;
193//! use qubit_redact_derive::Redact;
194//!
195//! #[derive(Redact)]
196//! struct Token(#[redact(level = "secret")] String);
197//!
198//! #[derive(Redact)]
199//! enum Event {
200//! Credential(#[redact(level = "secret")] String),
201//! Ready,
202//! }
203//!
204//! assert_eq!(
205//! format!("{:?}", Token("raw".into()).redacted()),
206//! "Token(\"<redacted>\")",
207//! );
208//! assert_eq!(
209//! format!("{:?}", Event::Credential("raw".into()).redacted()),
210//! "Credential(\"<redacted>\")",
211//! );
212//! assert_eq!(format!("{:?}", Event::Ready.redacted()), "Ready");
213//! ```
214//!
215//! `redacted()` snapshots the process default; `redacted_with` snapshots an
216//! explicit policy, which every nested and map field reuses. Field-specific
217//! map policies are not supported in the first version; use a domain newtype
218//! plus `nested` for a separate policy boundary.
219//!
220//! # Process diagnostics
221//!
222//! Process adapters use the [`DiagnosticBudget`] in their [`RedactionPolicy`]
223//! snapshot. They stop before inspecting argv or environment input beyond the
224//! input limit and truncate their final log-safe list at the output limit.
225//!
226//! ```
227//! use std::ffi::OsStr;
228//! use qubit_redact::{ArgvRedactor, EnvRedactor, argv::ArgvItem};
229//!
230//! let argv = [
231//! ArgvItem::plain(OsStr::new("client")),
232//! ArgvItem::plain(OsStr::new("--password")),
233//! ArgvItem::plain(OsStr::new("raw")),
234//! ];
235//! assert!(!ArgvRedactor::default()
236//! .redact_heuristically(argv)
237//! .to_string()
238//! .contains("raw"));
239//! assert_eq!(
240//! EnvRedactor::default().redact_pair("PASSWORD", "raw").to_string(),
241//! "PASSWORD=<redacted>",
242//! );
243//! ```
244//!
245//! # HTTP bodies
246//!
247//! Enable this API with `qubit-redact = { version = "0.3", features = ["http"]
248//! }`. `http::BodyCapture` makes completeness explicit, and the returned
249//! `http::BodyRedaction` implements [`std::fmt::Display`] with bounded,
250//! log-safe output.
251//!
252//! ```
253//! # #[cfg(feature = "http")]
254//! # {
255//! use http::HeaderValue;
256//! use qubit_redact::http::{BodyCapture, BodyRedaction, HttpRedactor};
257//!
258//! let content_type = HeaderValue::from_static("application/json");
259//! let result: BodyRedaction = HttpRedactor::default().redact_body(
260//! BodyCapture::complete(br#"{"password":"raw","mode":"debug"}"#),
261//! Some(&content_type),
262//! );
263//! assert!(!format!("{result}").contains("raw"));
264//! # }
265//! ```
266
267extern crate self as qubit_redact;
268
269pub mod argv;
270pub mod domain;
271pub mod env;
272#[cfg(feature = "http")]
273pub mod http;
274#[cfg(feature = "json")]
275pub mod json;
276mod json_feature_gate;
277pub mod policy;
278#[cfg(feature = "serde")]
279mod private;
280mod redactor;
281mod serde_feature_gate;
282pub mod text;
283
284pub use argv::ArgvRedactor;
285pub use domain::{
286 BoundedRedactedDisplay,
287 Redact,
288 RedactMapValue,
289 RedactMapValueMut,
290 RedactMut,
291 RedactValue,
292 RedactValueMut,
293 Redacted,
294 RedactedKeyedMap,
295 RedactedKeyedValue,
296 RedactedMap,
297 RedactedValue,
298};
299pub use env::EnvRedactor;
300#[cfg(feature = "json")]
301pub use json::{
302 RedactedJson,
303 RedactedJsonText,
304 redact_json_text_in_place,
305};
306pub use policy::{
307 AllowRule,
308 DiagnosticBudget,
309 DiagnosticBudgetError,
310 DiagnosticInputBudget,
311 FieldClassification,
312 FieldMatchKind,
313 FieldNameMatching,
314 GlobalDefaultAlreadySet,
315 MaskPolicy,
316 MaskingPolicy,
317 PolicyError,
318 RedactionPolicy,
319 RedactionPolicyBuilder,
320 SensitiveFieldPreset,
321 SensitiveFieldRule,
322 Sensitivity,
323 UnknownFieldPolicy,
324};
325pub use redactor::Redactor;
326pub use text::{
327 BoundedLogSafeDisplay,
328 LogOutputLimit,
329 LogOutputLimitError,
330 LogSafeText,
331 RedactedDebug,
332 RedactedText,
333 redacted_debug,
334};
335
336#[cfg(feature = "serde")]
337#[doc(hidden)]
338pub use private::__private;