Skip to main content

mz_ore/
str.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! String utilities.
17
18#[cfg(feature = "assert-no-tracing")]
19use crate::assert::soft_assertions_enabled;
20use std::fmt::{self, Debug, Formatter, Write};
21use std::ops::Deref;
22
23// Without the soft-assertion machinery available, treat soft assertions as
24// disabled so that `redact` always redacts. Callers that need the
25// debug-mode behavior opt into the `assert-no-tracing` feature.
26#[cfg(not(feature = "assert-no-tracing"))]
27fn soft_assertions_enabled() -> bool {
28    false
29}
30
31/// Extension methods for [`str`].
32pub trait StrExt {
33    /// Wraps the string slice in a type whose display implementation renders
34    /// the string surrounded by double quotes with any inner double quote
35    /// characters escaped.
36    ///
37    /// # Examples
38    ///
39    /// In the standard case, when the wrapped string does not contain any
40    /// double quote characters:
41    ///
42    /// ```
43    /// use mz_ore::str::StrExt;
44    ///
45    /// let name = "bob";
46    /// let message = format!("unknown user {}", name.quoted());
47    /// assert_eq!(message, r#"unknown user "bob""#);
48    /// ```
49    ///
50    /// In a pathological case:
51    ///
52    /// ```
53    /// use mz_ore::str::StrExt;
54    ///
55    /// let name = r#"b@d"inp!t""#;
56    /// let message = format!("unknown user {}", name.quoted());
57    /// assert_eq!(message, r#"unknown user "b@d\"inp!t\"""#);
58    /// ```
59    fn quoted(&self) -> QuotedStr<'_>;
60    /// Same as [`StrExt::quoted`], but also escapes new lines and tabs.
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use mz_ore::str::StrExt;
66    ///
67    /// let name = "b@d\"\tinp!t\"\r\n";
68    /// let message = format!("unknown user {}", name.escaped());
69    /// assert_eq!(message, r#"unknown user "b@d\"\tinp!t\"\r\n""#);
70    /// ```
71    fn escaped(&self) -> EscapedStr<'_>;
72}
73
74impl StrExt for str {
75    fn quoted(&self) -> QuotedStr<'_> {
76        QuotedStr(self)
77    }
78    fn escaped(&self) -> EscapedStr<'_> {
79        EscapedStr(self)
80    }
81}
82
83/// Displays a string slice surrounded by double quotes with any inner double
84/// quote characters escaped.
85///
86/// Constructed by [`StrExt::quoted`].
87#[derive(Debug)]
88pub struct QuotedStr<'a>(&'a str);
89
90impl<'a> fmt::Display for QuotedStr<'a> {
91    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
92        f.write_char('"')?;
93        for c in self.chars() {
94            match c {
95                '"' => f.write_str("\\\"")?,
96                _ => f.write_char(c)?,
97            }
98        }
99        f.write_char('"')
100    }
101}
102
103impl<'a> Deref for QuotedStr<'a> {
104    type Target = str;
105
106    fn deref(&self) -> &str {
107        self.0
108    }
109}
110
111/// Same as [`QuotedStr`], but also escapes new lines and tabs.
112///
113/// Constructed by [`StrExt::escaped`].
114#[derive(Debug)]
115pub struct EscapedStr<'a>(&'a str);
116
117impl<'a> fmt::Display for EscapedStr<'a> {
118    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
119        f.write_char('"')?;
120        for c in self.chars() {
121            // We don't use `char::escape_default()` here to prevent escaping Unicode chars.
122            match c {
123                '"' => f.write_str("\\\"")?,
124                '\n' => f.write_str("\\n")?,
125                '\r' => f.write_str("\\r")?,
126                '\t' => f.write_str("\\t")?,
127                _ => f.write_char(c)?,
128            }
129        }
130        f.write_char('"')
131    }
132}
133
134impl<'a> Deref for EscapedStr<'a> {
135    type Target = str;
136
137    fn deref(&self) -> &str {
138        self.0
139    }
140}
141
142/// Creates a type whose [`fmt::Display`] implementation outputs item preceded
143/// by `open` and followed by `close`.
144pub fn bracketed<'a, D>(open: &'a str, close: &'a str, contents: D) -> impl fmt::Display + 'a
145where
146    D: fmt::Display + 'a,
147{
148    struct Bracketed<'a, D> {
149        open: &'a str,
150        close: &'a str,
151        contents: D,
152    }
153
154    impl<'a, D> fmt::Display for Bracketed<'a, D>
155    where
156        D: fmt::Display,
157    {
158        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
159            write!(f, "{}{}{}", self.open, self.contents, self.close)
160        }
161    }
162
163    Bracketed {
164        open,
165        close,
166        contents,
167    }
168}
169
170/// Given a closure, it creates a Display that simply calls the given closure when fmt'd.
171pub fn closure_to_display<F>(fun: F) -> impl fmt::Display
172where
173    F: Fn(&mut fmt::Formatter) -> fmt::Result,
174{
175    struct Mapped<F> {
176        fun: F,
177    }
178
179    impl<F> fmt::Display for Mapped<F>
180    where
181        F: Fn(&mut fmt::Formatter) -> fmt::Result,
182    {
183        fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
184            (self.fun)(formatter)
185        }
186    }
187
188    Mapped { fun }
189}
190
191/// Creates a type whose [`fmt::Display`] implementation outputs each item in
192/// `iter` separated by `separator`.
193pub fn separated<'a, I>(separator: &'a str, iter: I) -> impl fmt::Display + 'a
194where
195    I: IntoIterator,
196    I::IntoIter: Clone + 'a,
197    I::Item: fmt::Display + 'a,
198{
199    struct Separated<'a, I> {
200        separator: &'a str,
201        iter: I,
202    }
203
204    impl<'a, I> fmt::Display for Separated<'a, I>
205    where
206        I: Iterator + Clone,
207        I::Item: fmt::Display,
208    {
209        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
210            for (i, item) in self.iter.clone().enumerate() {
211                if i != 0 {
212                    write!(f, "{}", self.separator)?;
213                }
214                write!(f, "{}", item)?;
215            }
216            Ok(())
217        }
218    }
219
220    Separated {
221        separator,
222        iter: iter.into_iter(),
223    }
224}
225
226/// A helper struct to keep track of indentation levels.
227///
228/// This will be most often used as part of the rendering context
229/// type for various `Display$Format` implementation.
230#[derive(Debug, Clone)]
231pub struct Indent {
232    unit: String,
233    buff: String,
234    mark: Vec<usize>,
235}
236
237impl AsMut<Indent> for Indent {
238    fn as_mut(&mut self) -> &mut Indent {
239        self
240    }
241}
242
243impl Indent {
244    /// Construct a new `Indent` where one level is represented
245    /// by the given `unit` repeated `step` times.
246    pub fn new(unit: char, step: usize) -> Indent {
247        Indent {
248            unit: std::iter::repeat(unit).take(step).collect::<String>(),
249            buff: String::with_capacity(unit.len_utf8()),
250            mark: vec![],
251        }
252    }
253
254    fn inc(&mut self, rhs: usize) {
255        for _ in 0..rhs {
256            self.buff += &self.unit;
257        }
258    }
259
260    fn dec(&mut self, rhs: usize) {
261        let tail = rhs.saturating_mul(self.unit.len());
262        let head = self.buff.len().saturating_sub(tail);
263        self.buff.truncate(head);
264    }
265
266    /// Remember the current state.
267    pub fn set(&mut self) {
268        self.mark.push(self.buff.len());
269    }
270
271    /// Reset `buff` to the last marked state.
272    pub fn reset(&mut self) {
273        if let Some(len) = self.mark.pop() {
274            while self.buff.len() < len {
275                self.buff += &self.unit;
276            }
277            self.buff.truncate(len);
278        }
279    }
280}
281
282/// Convenience methods for pretty-printing based on indentation
283/// that are automatically available for context objects that can
284/// be mutably referenced as an [`Indent`] instance.
285pub trait IndentLike {
286    /// Print a block of code defined in `f` one step deeper
287    /// from the current [`Indent`].
288    fn indented<F>(&mut self, f: F) -> fmt::Result
289    where
290        F: FnMut(&mut Self) -> fmt::Result;
291
292    /// Same as [`IndentLike::indented`], but the `f` only going to be printed
293    /// in an indented context if `guard` is `true`.
294    fn indented_if<F>(&mut self, guard: bool, f: F) -> fmt::Result
295    where
296        F: FnMut(&mut Self) -> fmt::Result;
297}
298
299impl<T: AsMut<Indent>> IndentLike for T {
300    fn indented<F>(&mut self, mut f: F) -> fmt::Result
301    where
302        F: FnMut(&mut Self) -> fmt::Result,
303    {
304        *self.as_mut() += 1;
305        let result = f(self);
306        *self.as_mut() -= 1;
307        result
308    }
309
310    fn indented_if<F>(&mut self, guard: bool, mut f: F) -> fmt::Result
311    where
312        F: FnMut(&mut Self) -> fmt::Result,
313    {
314        if guard {
315            *self.as_mut() += 1;
316        }
317        let result = f(self);
318
319        if guard {
320            *self.as_mut() -= 1;
321        }
322        result
323    }
324}
325
326impl Default for Indent {
327    fn default() -> Self {
328        Indent::new(' ', 2)
329    }
330}
331
332impl std::ops::AddAssign<usize> for Indent {
333    fn add_assign(&mut self, rhs: usize) {
334        self.inc(rhs)
335    }
336}
337
338impl std::ops::SubAssign<usize> for Indent {
339    fn sub_assign(&mut self, rhs: usize) {
340        self.dec(rhs)
341    }
342}
343
344impl fmt::Display for Indent {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        f.write_str(&self.buff)
347    }
348}
349
350/// Newtype wrapper around [`String`] whose _byte_ length is guaranteed to be less than or equal to
351/// the provided `MAX`.
352#[derive(Debug, Clone, PartialEq)]
353pub struct MaxLenString<const MAX: usize>(String);
354
355impl<const MAX: usize> MaxLenString<MAX> {
356    /// Creates a new [`MaxLenString`] returning an error if `s` is more than `MAX` bytes long.
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use mz_ore::str::MaxLenString;
362    ///
363    /// type ShortString = MaxLenString<30>;
364    ///
365    /// let good = ShortString::new("hello".to_string()).unwrap();
366    /// assert_eq!(good.as_str(), "hello");
367    ///
368    /// // Note: this is only 8 characters, but each character requires 4 bytes.
369    /// let too_long = "😊😊😊😊😊😊😊😊";
370    /// let smol = ShortString::new(too_long.to_string());
371    /// assert!(smol.is_err());
372    /// ```
373    ///
374    pub fn new(s: String) -> Result<Self, String> {
375        if s.len() > MAX {
376            return Err(s);
377        }
378
379        Ok(MaxLenString(s))
380    }
381
382    /// Consume self, returning the inner [`String`].
383    pub fn into_inner(self) -> String {
384        self.0
385    }
386
387    /// Returns a reference to the underlying string.
388    pub fn as_str(&self) -> &str {
389        self
390    }
391}
392
393impl<const MAX: usize> Deref for MaxLenString<MAX> {
394    type Target = str;
395
396    fn deref(&self) -> &Self::Target {
397        &self.0
398    }
399}
400
401impl<const MAX: usize> fmt::Display for MaxLenString<MAX> {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        f.write_str(&self.0)
404    }
405}
406
407impl<const MAX: usize> TryFrom<String> for MaxLenString<MAX> {
408    type Error = String;
409
410    fn try_from(s: String) -> Result<Self, Self::Error> {
411        Self::new(s)
412    }
413}
414
415impl<'a, const MAX: usize> TryFrom<&'a String> for MaxLenString<MAX> {
416    type Error = String;
417
418    fn try_from(s: &'a String) -> Result<Self, Self::Error> {
419        Self::try_from(s.clone())
420    }
421}
422
423impl<'a, const MAX: usize> TryFrom<&'a str> for MaxLenString<MAX> {
424    type Error = String;
425
426    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
427        Self::try_from(String::from(s))
428    }
429}
430
431/// Returns a "redacted" debug implementation. When running with soft assertions
432/// enabled or with the alternate / `#` flag specified, this prints identically
433/// to the underlying value; otherwise, we print the basic debug representation with
434/// alphanumeric characters replaced. (For example, the number `-3.6` will print as
435/// `<-#.#>`.)
436pub fn redact(s: impl Debug) -> impl Debug {
437    Redacting {
438        value: s,
439        debug_mode: soft_assertions_enabled(),
440    }
441}
442
443struct Redacting<A: Debug> {
444    value: A,
445    debug_mode: bool,
446}
447
448struct RedactingWriter<'a, 'b>(&'a mut Formatter<'b>);
449
450impl<'a, 'b> Write for RedactingWriter<'a, 'b> {
451    fn write_str(&mut self, s: &str) -> fmt::Result {
452        for c in s.chars() {
453            self.0.write_char(if c.is_digit(10) {
454                '#'
455            } else if c.is_alphabetic() {
456                'X'
457            } else {
458                c
459            })?;
460        }
461        Ok(())
462    }
463}
464
465impl<A: Debug> Debug for Redacting<A> {
466    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
467        if self.debug_mode || f.alternate() {
468            self.value.fmt(f)
469        } else {
470            f.write_char('<')?;
471            let mut write = RedactingWriter(f);
472            write!(&mut write, "{:?}", &self.value)?;
473            f.write_char('>')
474        }
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[crate::test]
483    fn test_indent() {
484        let mut indent = Indent::new('~', 3);
485        indent += 1;
486        assert_eq!(indent.to_string(), "~~~".to_string());
487        indent += 3;
488        assert_eq!(indent.to_string(), "~~~~~~~~~~~~".to_string());
489        indent -= 2;
490        assert_eq!(indent.to_string(), "~~~~~~".to_string());
491        indent -= 4;
492        assert_eq!(indent.to_string(), "".to_string());
493        indent += 1;
494        assert_eq!(indent.to_string(), "~~~".to_string());
495    }
496
497    #[crate::test]
498    fn test_redact() {
499        // Don't pick up the soft-assert flag in this test, to emulate prod behaviour.
500        pub fn redact(s: impl Debug) -> impl Debug {
501            Redacting {
502                value: s,
503                debug_mode: false,
504            }
505        }
506
507        assert_eq!(
508            r#"<"XXXX_XXXXXX">"#,
509            format!("{:?}", &redact(&"TEST_STRING"))
510        );
511        assert_eq!(
512            r#""TEST_STRING""#,
513            format!("{:#?}", &redact(&"TEST_STRING"))
514        );
515        assert_eq!("<#.###>", format!("{:?}", &redact(&1.234f32)));
516    }
517}