Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/ore/src/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,18 +445,27 @@ struct Redacting<A: Debug> {
debug_mode: bool,
}

/// The character masking applied by [redact]: digits become `#`, alphabetic
/// characters become `X`, everything else is unchanged.
///
/// Exposed so that renderings that cannot go through [redact]'s `Debug`
/// wrapper (e.g. JSON values) can apply the identical masking.
pub fn redact_char(c: char) -> char {
if c.is_digit(10) {
'#'
} else if c.is_alphabetic() {
'X'
} else {
c
}
}

struct RedactingWriter<'a, 'b>(&'a mut Formatter<'b>);

impl<'a, 'b> Write for RedactingWriter<'a, 'b> {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.chars() {
self.0.write_char(if c.is_digit(10) {
'#'
} else if c.is_alphabetic() {
'X'
} else {
c
})?;
self.0.write_char(redact_char(c))?;
}
Ok(())
}
Expand Down
60 changes: 60 additions & 0 deletions src/persist-types/src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -801,3 +801,63 @@ pub(crate) fn any_columnar_stats() -> impl Strategy<Value = ColumnarStats> {
})
})
}

/// Redacts scalar values in a [DynStats::debug_json] rendering.
///
/// Mirrors [mz_ore::str::redact]: when soft assertions are enabled the value
/// is returned unchanged. Otherwise scalar leaves are rendered as strings
/// with each character masked by [mz_ore::str::redact_char] and wrapped in
/// `<>`. Object keys are left as is, they are column names, not data.
pub(crate) fn redact_json(v: serde_json::Value) -> serde_json::Value {
if mz_ore::assert::soft_assertions_enabled() {
return v;
}
redact_json_masked(v)
}

fn redact_json_masked(v: serde_json::Value) -> serde_json::Value {
use serde_json::Value;
fn mask(s: String) -> Value {
let mut out = String::with_capacity(s.len() + 2);
out.push('<');
out.extend(s.chars().map(mz_ore::str::redact_char));
out.push('>');
Value::String(out)
}
match v {
Value::Null => Value::Null,
Value::Bool(x) => mask(x.to_string()),
Value::Number(x) => mask(x.to_string()),
Value::String(x) => mask(x),
Value::Array(xs) => Value::Array(xs.into_iter().map(redact_json_masked).collect()),
Value::Object(kvs) => Value::Object(
kvs.into_iter()
.map(|(k, v)| (k, redact_json_masked(v)))
.collect(),
),
}
}

#[cfg(test)]
mod tests {
use serde_json::json;

use super::redact_json_masked;

// Tests the masking directly: `redact_json` itself is a passthrough in
// test builds because soft assertions are enabled.
#[mz_ore::test]
fn redact_json_masks_scalars() {
assert_eq!(
redact_json_masked(json!("TEST_STRING")),
json!("<XXXX_XXXXXX>")
);
assert_eq!(redact_json_masked(json!(1.234)), json!("<#.###>"));
assert_eq!(redact_json_masked(json!(true)), json!("<XXXX>"));
assert_eq!(redact_json_masked(json!(null)), json!(null));
assert_eq!(
redact_json_masked(json!({"lower": "abc1", "upper": [2, "d"]})),
json!({"lower": "<XXX#>", "upper": ["<#>", "<X>"]})
);
}
}
10 changes: 5 additions & 5 deletions src/persist-types/src/stats/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::stats::primitive::{PrimitiveStats, any_primitive_vec_u8_stats};
use crate::stats::{
ColumnStatKinds, ColumnStats, ColumnarStats, DynStats, OptionStats, ProtoAtomicBytesStats,
ProtoBytesStats, ProtoFixedSizeBytesStats, TrimStats, proto_bytes_stats,
proto_fixed_size_bytes_stats,
proto_fixed_size_bytes_stats, redact_json,
};

/// `PrimitiveStats<Vec<u8>>` that cannot safely be trimmed.
Expand All @@ -43,8 +43,8 @@ impl Debug for AtomicBytesStats {
impl AtomicBytesStats {
fn debug_json(&self) -> serde_json::Value {
serde_json::json!({
"lower": hex::encode(&self.lower),
"upper": hex::encode(&self.upper),
"lower": redact_json(hex::encode(&self.lower).into()),
"upper": redact_json(hex::encode(&self.upper).into()),
})
}
}
Expand Down Expand Up @@ -92,8 +92,8 @@ impl Debug for FixedSizeBytesStats {
impl FixedSizeBytesStats {
fn debug_json(&self) -> serde_json::Value {
serde_json::json!({
"lower": hex::encode(&self.lower),
"upper": hex::encode(&self.upper),
"lower": redact_json(hex::encode(&self.lower).into()),
"upper": redact_json(hex::encode(&self.upper).into()),
"kind": self.kind,
})
}
Expand Down
8 changes: 4 additions & 4 deletions src/persist-types/src/stats/primitive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use serde::Serialize;

use crate::stats::{
BytesStats, ColumnStatKinds, ColumnStats, ColumnarStats, DynStats, OptionStats,
ProtoPrimitiveBytesStats, ProtoPrimitiveStats, TrimStats, proto_primitive_stats,
ProtoPrimitiveBytesStats, ProtoPrimitiveStats, TrimStats, proto_primitive_stats, redact_json,
};
use crate::timestamp::try_parse_monotonic_iso8601_timestamp;

Expand Down Expand Up @@ -160,7 +160,7 @@ where
fn debug_json(&self) -> serde_json::Value {
let l = serde_json::to_value(&self.lower).expect("valid json");
let u = serde_json::to_value(&self.upper).expect("valid json");
serde_json::json!({"lower": l, "upper": u})
serde_json::json!({"lower": redact_json(l), "upper": redact_json(u)})
}

fn into_columnar_stats(self) -> ColumnarStats {
Expand All @@ -174,8 +174,8 @@ where
impl DynStats for PrimitiveStats<Vec<u8>> {
fn debug_json(&self) -> serde_json::Value {
serde_json::json!({
"lower": hex::encode(&self.lower),
"upper": hex::encode(&self.upper),
"lower": redact_json(hex::encode(&self.lower).into()),
"upper": redact_json(hex::encode(&self.upper).into()),
})
}

Expand Down
Loading