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;