Skip to main content

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;