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 mut builder = RedactionPolicy::builder();
37//! builder
38//!     .fields()
39//!     .raise("tenant_secret", Sensitivity::Secret)?;
40//! let policy = builder.build()?;
41//! let source = HashMap::from([
42//!     ("tenant_secret".to_owned(), "raw".to_owned()),
43//!     ("display_name".to_owned(), "Alice".to_owned()),
44//! ]);
45//! let redacted = Redactor::new(policy).redact_map(&source);
46//! assert_eq!(redacted["tenant_secret"], "<redacted>");
47//! assert_eq!(source["tenant_secret"], "raw");
48//! # Ok::<(), Box<dyn std::error::Error>>(())
49//! ```
50//!
51//! An application can install one process-wide [`RedactionPolicy`] during
52//! assembly or initialization. Builders are deterministic and never read
53//! process-wide state; use `RedactionPolicy::default().to_builder()` when an
54//! explicit extension of the installed snapshot is needed. Existing policy
55//! snapshots never change. Before an application installs a global policy,
56//! `RedactionPolicy::global()` and `RedactionPolicy::default()` return the
57//! fixed standard policy without preventing later installation.
58//! This fallback supports dependency construction during application assembly;
59//! it is not runtime reconfiguration. The executable, never a library, owns the
60//! single installation and should complete it before starting concurrent work.
61//! Anything created earlier keeps its standard-policy snapshot. Construct
62//! policy-sensitive objects afterward or inject the application policy.
63//!
64//! ```
65//! use qubit_redact::{RedactionPolicy, Sensitivity};
66//!
67//! let mut builder = RedactionPolicy::builder();
68//! builder
69//!     .fields()
70//!     .raise("tenant_secret", Sensitivity::Secret)?;
71//! let application_default = builder.build()?;
72//! RedactionPolicy::install_global(application_default)?;
73//! let snapshot = RedactionPolicy::default();
74//! assert_eq!(snapshot.sensitivity_for("tenant_secret"), Some(Sensitivity::Secret));
75//! # Ok::<(), Box<dyn std::error::Error>>(())
76//! ```
77//!
78//! [`RedactedText`] is not directly displayable. Explicitly cross a plain-text
79//! logging boundary with [`RedactedText::escape_for_log`].
80//!
81//! ```
82//! use qubit_redact::Redactor;
83//!
84//! let safe = Redactor::default()
85//!     .redact_field("message", "line one\nline two")
86//!     .escape_for_log();
87//! assert_eq!(safe.to_string(), "line one\\nline two");
88//! ```
89//!
90//! # Domain objects
91//!
92//! Add the companion `qubit-redact-derive` crate to annotate fields explicitly.
93//! Plain fields are never recursively redacted, `nested` is the recursion
94//! boundary, `map` classifies each value by its runtime key, and `skip` omits a
95//! field only from the redacted representation.
96//!
97//! ```ignore
98//! use std::collections::HashMap;
99//! use qubit_redact::{Redact as _, RedactionPolicy, Sensitivity};
100//! use qubit_redact_derive::Redact;
101//!
102//! #[derive(Redact)]
103//! struct Account {
104//!     id: u64,
105//!     #[redact(level = "secret")]
106//!     password: String,
107//!     #[redact(map)]
108//!     metadata: HashMap<String, String>,
109//! }
110//!
111//! let mut builder = RedactionPolicy::builder();
112//! builder.fields().raise("api_key", Sensitivity::Secret)?;
113//! let policy = builder.build()?;
114//! let account = Account {
115//!     id: 1,
116//!     password: "raw-password".to_owned(),
117//!     metadata: HashMap::from([
118//!         ("api_key".to_owned(), "raw-key".to_owned()),
119//!     ]),
120//! };
121//! let output = format!("{:?}", account.redacted_with(&policy));
122//! assert!(!output.contains("raw-password"));
123//! assert!(!output.contains("raw-key"));
124//! # Ok::<(), Box<dyn std::error::Error>>(())
125//! ```
126//!
127//! `RedactMut` is an explicit logical in-place redaction contract. The skipped
128//! field below remains unchanged, while `nested` uses the same policy for the
129//! child. It does not zeroize released allocations or affect aliases, existing
130//! copies, or borrowed backing data. Clone-based `to_redacted` temporarily
131//! retains a second raw copy. Use a separately designed zeroization strategy
132//! when memory erasure is required.
133//!
134//! ```ignore
135//! use qubit_redact::{Redact as _, RedactMut as _};
136//! use qubit_redact_derive::{Redact, RedactMut};
137//!
138//! #[derive(Clone, Redact, RedactMut)]
139//! struct Secret {
140//!     #[redact(level = "secret")]
141//!     value: String,
142//! }
143//!
144//! #[derive(Clone, Redact, RedactMut)]
145//! struct Envelope {
146//!     #[redact(nested)]
147//!     secret: Secret,
148//!     #[redact(skip)]
149//!     internal_note: String,
150//! }
151//!
152//! let mut envelope = Envelope {
153//!     secret: Secret { value: "raw".to_owned() },
154//!     internal_note: "unchanged".to_owned(),
155//! };
156//! envelope.redact_in_place();
157//! assert_eq!(envelope.secret.value, "<redacted>");
158//! assert_eq!(envelope.internal_note, "unchanged");
159//! ```
160//!
161//! With the `serde` feature, a direct `serde` dependency, and the companion
162//! derive crate, `#[redact(serde)]` opts the redacted view into serialization.
163//! [`Redacted`] intentionally does not implement `Deserialize`.
164//!
165//! ```ignore
166//! # #[cfg(feature = "serde")]
167//! # {
168//! use qubit_redact::Redact as _;
169//! use qubit_redact_derive::Redact;
170//!
171//! #[derive(Redact)]
172//! #[redact(debug, display, serde)]
173//! struct Credentials {
174//!     #[redact(level = "secret")]
175//!     token: String,
176//!     #[redact(skip)]
177//!     internal_note: String,
178//! }
179//!
180//! let value = Credentials {
181//!     token: "raw-token".to_owned(),
182//!     internal_note: "not serialized".to_owned(),
183//! };
184//! let json = serde_json::to_string(&value.redacted())?;
185//! assert!(!json.contains("raw-token"));
186//! assert!(!json.contains("internal_note"));
187//! assert!(!format!("{value:?}").contains("raw-token"));
188//! assert!(!format!("{value}").contains("raw-token"));
189//! # }
190//! # Ok::<(), Box<dyn std::error::Error>>(())
191//! ```
192//!
193//! `debug` and `display` are opt-in implementations on the original type and
194//! use the process-wide default policy. Plain fields remain ordinary `Debug`
195//! values. Redacted `Debug` and `Display` output use the policy's diagnostic
196//! output budget by default. Use `with_output_limit()` to select a different
197//! explicit limit. Do not request an
198//! implementation already supplied by the type, such as combining
199//! `#[derive(Debug)]` with `#[redact(debug)]`.
200//!
201//! Derives support named, tuple, and unit structs, plus enums with named,
202//! tuple, and unit variants. With `#[redact(serde)]`, redacted serialization
203//! supports Serde's external, internal, adjacent, and untagged enum
204//! representations through a structure-preserving attribute allowlist.
205//!
206//! ```ignore
207//! use qubit_redact::Redact as _;
208//! use qubit_redact_derive::Redact;
209//!
210//! #[derive(Redact)]
211//! struct Token(#[redact(level = "secret")] String);
212//!
213//! #[derive(Redact)]
214//! enum Event {
215//!     Credential(#[redact(level = "secret")] String),
216//!     Ready,
217//! }
218//!
219//! assert_eq!(
220//!     format!("{:?}", Token("raw".into()).redacted()),
221//!     "Token(\"<redacted>\")",
222//! );
223//! assert_eq!(
224//!     format!("{:?}", Event::Credential("raw".into()).redacted()),
225//!     "Credential(\"<redacted>\")",
226//! );
227//! assert_eq!(format!("{:?}", Event::Ready.redacted()), "Ready");
228//! ```
229//!
230//! `redacted()` snapshots the process default; `redacted_with` snapshots an
231//! explicit policy, which every nested and map field reuses. Field-specific
232//! map policies are not supported in the first version; use a domain newtype
233//! plus `nested` for a separate policy boundary.
234//!
235//! # Process diagnostics
236//!
237//! Process adapters use the [`InputOutputLimit`] in their [`RedactionPolicy`]
238//! snapshot. They stop before inspecting argv or environment input beyond the
239//! input limit and truncate their final log-safe list at the output limit.
240//!
241//! ```
242//! use std::ffi::OsStr;
243//! use qubit_redact::{ArgvRedactor, EnvRedactor, argv::ArgvItem};
244//!
245//! let argv = [
246//!     ArgvItem::plain(OsStr::new("client")),
247//!     ArgvItem::plain(OsStr::new("--password")),
248//!     ArgvItem::plain(OsStr::new("raw")),
249//! ];
250//! assert!(!ArgvRedactor::default()
251//!     .redact_heuristically(argv)
252//!     .to_string()
253//!     .contains("raw"));
254//! assert_eq!(
255//!     EnvRedactor::default().redact_pair("PASSWORD", "raw").to_string(),
256//!     "PASSWORD=<redacted>",
257//! );
258//! ```
259//!
260//! # JSON values
261//!
262//! With the `json` feature, `RedactedJson`, `RedactedJsonText`, and
263//! `redact_json_text_in_place` share the `JsonDepthBudget` stored in their
264//! immutable [`RedactionPolicy`] snapshot. The default maximum depth is 128;
265//! an over-depth object or array is replaced with the policy's opaque Secret
266//! mask without visiting its descendants.
267//!
268//! # HTTP bodies
269//!
270//! Enable this API with `qubit-redact = { version = "0.4", features = ["http"]
271//! }`. `http::BodyCapture` makes completeness explicit, and the returned
272//! `http::BodyRedaction` implements [`std::fmt::Display`] with bounded,
273//! log-safe output.
274//!
275//! ```
276//! # #[cfg(feature = "http")]
277//! # {
278//! use http::HeaderValue;
279//! use qubit_redact::http::{BodyCapture, BodyRedaction, HttpRedactor};
280//!
281//! let content_type = HeaderValue::from_static("application/json");
282//! let result: BodyRedaction = HttpRedactor::default().redact_body(
283//!     BodyCapture::complete(br#"{"password":"raw","mode":"debug"}"#),
284//!     Some(&content_type),
285//! );
286//! assert!(!format!("{result}").contains("raw"));
287//! # }
288//! ```
289
290extern crate self as qubit_redact;
291
292pub mod argv;
293pub mod domain;
294pub mod env;
295mod field_redaction;
296#[cfg(feature = "http")]
297pub mod http;
298mod install_global_policy_error;
299#[cfg(feature = "json")]
300pub mod json;
301mod json_feature_gate;
302mod pass_through_reason;
303pub mod policy;
304#[cfg(feature = "serde")]
305mod private;
306mod redactor;
307mod serde_feature_gate;
308pub mod text;
309#[cfg(feature = "uri")]
310pub mod uri;
311
312pub use argv::ArgvRedactor;
313pub use domain::{
314    BoundedRedactedDisplay,
315    Redact,
316    RedactMapValue,
317    RedactMapValueMut,
318    RedactMut,
319    RedactValue,
320    RedactValueMut,
321    Redacted,
322    RedactedKeyedMap,
323    RedactedKeyedMapSession,
324    RedactedKeyedValue,
325    RedactedKeyedValueSession,
326    RedactedMap,
327    RedactedMapSession,
328    RedactedSessionView,
329    RedactedValue,
330};
331pub use env::EnvRedactor;
332pub use field_redaction::{
333    FieldRedaction,
334    PassThroughReason,
335};
336pub use install_global_policy_error::InstallGlobalPolicyError;
337#[cfg(feature = "json")]
338pub use json::{
339    RedactedJson,
340    RedactedJsonSession,
341    RedactedJsonText,
342    RedactedJsonTextSession,
343    redact_json_text_in_place,
344};
345pub use policy::{
346    AllowRule,
347    DiagnosticBudgetError,
348    FieldClassification,
349    FieldMatchKind,
350    FieldNameMatching,
351    InputOutputLimit,
352    MaskPolicy,
353    MaskingPolicy,
354    PolicyError,
355    PolicyLocation,
356    RedactionFloor,
357    RedactionFloorBuilder,
358    RedactionLimits,
359    RedactionPolicy,
360    RedactionPolicyBuilder,
361    RedactionRules,
362    RedactionSession,
363    RedactionSessionKind,
364    SensitiveFieldPreset,
365    SensitiveFieldRule,
366    Sensitivity,
367    UnknownFieldPolicy,
368};
369#[cfg(feature = "json")]
370pub use policy::{
371    JsonDepthBudget,
372    JsonDepthBudgetError,
373    UnkeyedJsonValuePolicy,
374};
375pub use redactor::Redactor;
376pub use text::{
377    BoundedLogSafeDisplay,
378    DiagnosticLogBuilder,
379    DiagnosticWriteStatus,
380    LogOutputLimit,
381    LogOutputLimitError,
382    LogSafeText,
383    RedactedDebug,
384    RedactedText,
385    redacted_debug,
386};
387#[cfg(feature = "uri")]
388pub use uri::{
389    UriComponent,
390    UriFragmentPolicy,
391    UriInspection,
392    UriPathPolicy,
393    UriPolicy,
394    UriRedaction,
395    UriRedactionReason,
396    UriRedactionStatus,
397    UriRedactor,
398};
399
400#[cfg(feature = "serde")]
401#[doc(hidden)]
402pub use private::__private;