Skip to main content

freeswitch_types/
lossy_values.rs

1//! Types for tracking event headers whose values contained invalid UTF-8.
2
3use std::fmt;
4
5/// A header whose percent-decoded value was not valid UTF-8. Carries the
6/// unparsed on-wire value (the percent-encoded source text, always ASCII) so
7/// the app can re-decode it (e.g. as Latin-1) or audit it instead of being
8/// stuck with the U+FFFD-substituted string in `headers`.
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct LossyValue {
12    key: String,
13    raw_value: String,
14}
15
16impl LossyValue {
17    /// Create a new lossy value entry.
18    pub fn new(key: String, raw_value: String) -> Self {
19        Self { key, raw_value }
20    }
21
22    /// The header key.
23    pub fn key(&self) -> &str {
24        &self.key
25    }
26
27    /// The on-wire percent-encoded value before decode.
28    pub fn raw_value(&self) -> &str {
29        &self.raw_value
30    }
31}
32
33/// Header keys whose percent-decoded value contained invalid UTF-8 and was
34/// decoded lossily (U+FFFD substituted). Empty in the normal case.
35///
36/// `Display` renders the affected keys comma-separated for log/error context;
37/// it never includes the values, which may carry PII (e.g. a dialed number).
38#[derive(Debug, Clone, Default, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[cfg_attr(feature = "serde", serde(transparent))]
41pub struct LossyValues(Vec<LossyValue>);
42
43impl LossyValues {
44    /// Returns `true` if no headers had lossy decoding.
45    pub fn is_empty(&self) -> bool {
46        self.0
47            .is_empty()
48    }
49
50    /// Iterator over lossy values.
51    pub fn iter(&self) -> impl Iterator<Item = &LossyValue> {
52        self.0
53            .iter()
54    }
55
56    /// Add a lossy value entry.
57    pub fn push(&mut self, v: LossyValue) {
58        self.0
59            .push(v);
60    }
61}
62
63impl fmt::Display for LossyValues {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        let keys: Vec<&str> = self
66            .0
67            .iter()
68            .map(|v| v.key())
69            .collect();
70        write!(f, "{}", keys.join(", "))
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn lossy_values_display_keys_only() {
80        let mut lossy = LossyValues::default();
81        lossy.push(LossyValue::new(
82            "variable_dp_match".to_string(),
83            "%E9foo".to_string(),
84        ));
85        lossy.push(LossyValue::new(
86            "another_key".to_string(),
87            "%FFbar".to_string(),
88        ));
89
90        let display = lossy.to_string();
91        assert_eq!(display, "variable_dp_match, another_key");
92        assert!(!display.contains("%E9"));
93        assert!(!display.contains("foo"));
94    }
95
96    #[test]
97    fn lossy_values_empty() {
98        let lossy = LossyValues::default();
99        assert!(lossy.is_empty());
100        assert_eq!(
101            lossy
102                .iter()
103                .count(),
104            0
105        );
106    }
107
108    #[cfg(feature = "serde")]
109    #[test]
110    fn serde_roundtrip() {
111        let mut lossy = LossyValues::default();
112        lossy.push(LossyValue::new("key1".to_string(), "%E9value".to_string()));
113
114        let json = serde_json::to_string(&lossy).unwrap();
115        let deserialized: LossyValues = serde_json::from_str(&json).unwrap();
116
117        assert_eq!(lossy, deserialized);
118        assert_eq!(
119            deserialized
120                .iter()
121                .next()
122                .unwrap()
123                .key(),
124            "key1"
125        );
126        assert_eq!(
127            deserialized
128                .iter()
129                .next()
130                .unwrap()
131                .raw_value(),
132            "%E9value"
133        );
134    }
135
136    #[cfg(feature = "serde")]
137    #[test]
138    fn serde_old_json_without_field() {
139        let json = "[]";
140        let lossy: LossyValues = serde_json::from_str(json).unwrap();
141        assert!(lossy.is_empty());
142    }
143}