Skip to main content

wacore_binary/
attrs.rs

1use std::borrow::Cow;
2use std::str::FromStr;
3
4use crate::error::{BinaryError, Result};
5use crate::jid::Jid;
6use crate::node::{Attrs, Node, NodeRef, NodeStr, NodeValue, ValueRef};
7
8/// Coerces a wire boolean. WhatsApp/XMPP serialize flags as `"1"`/`"0"` as well
9/// as `"true"`/`"false"`; `str::parse::<bool>()` only accepts the latter and
10/// would silently reject `"1"`. Mirrors whatsmeow's `strconv.ParseBool` and WA
11/// Web's gating coercion. Returns `None` for any other value.
12fn coerce_protocol_bool(s: &str) -> Option<bool> {
13    match s {
14        "1" | "true" | "True" | "t" | "T" | "TRUE" => Some(true),
15        "0" | "false" | "False" | "f" | "F" | "FALSE" => Some(false),
16        _ => None,
17    }
18}
19
20pub struct AttrParser<'a> {
21    pub attrs: &'a Attrs,
22    pub errors: Vec<BinaryError>,
23}
24
25pub struct AttrParserRef<'a> {
26    pub(crate) attrs: &'a [(NodeStr<'a>, ValueRef<'a>)],
27    pub errors: Vec<BinaryError>,
28}
29
30impl<'a> AttrParserRef<'a> {
31    pub fn new(node: &'a NodeRef<'a>) -> Self {
32        Self {
33            attrs: node.attrs.as_slice(),
34            errors: Vec::new(),
35        }
36    }
37
38    pub fn ok(&self) -> bool {
39        self.errors.is_empty()
40    }
41
42    pub fn finish(&self) -> Result<()> {
43        if self.ok() {
44            Ok(())
45        } else {
46            Err(BinaryError::AttrList(self.errors.clone()))
47        }
48    }
49
50    fn get_raw(&mut self, key: &str, require: bool) -> Option<&'a ValueRef<'a>> {
51        let val = self.attrs.iter().find(|(k, _)| **k == *key).map(|(_, v)| v);
52
53        if require && val.is_none() {
54            self.errors.push(BinaryError::AttrParse(format!(
55                "Required attribute '{key}' not found"
56            )));
57        }
58
59        val
60    }
61
62    /// Get string from the value. Works for both String and JID variants.
63    /// - String variant: Cow::Borrowed — zero copy
64    /// - JID variant: Cow::Owned — allocates only when needed
65    pub fn optional_string(&mut self, key: &str) -> Option<Cow<'a, str>> {
66        self.get_raw(key, false).map(|v| v.as_str())
67    }
68
69    /// Get a required string attribute, returning an error if missing.
70    ///
71    /// Prefer this over `string()` for required attributes as it makes
72    /// the error explicit rather than silently defaulting to empty string.
73    pub fn required_string(&mut self, key: &str) -> Result<Cow<'a, str>> {
74        self.optional_string(key)
75            .ok_or_else(|| BinaryError::MissingAttr(key.to_string()))
76    }
77
78    /// Get JID from the value.
79    /// If the value is a JidRef, returns it directly without parsing (zero allocation).
80    /// If the value is a string, parses it as a JID.
81    pub fn optional_jid(&mut self, key: &str) -> Option<Jid> {
82        self.get_jid(key, false)
83    }
84
85    /// Shared by `optional_jid` and `jid`, so the required variant can record a
86    /// missing attribute and read the present one from the same lookup instead
87    /// of scanning the attribute list a second time to fetch what it just
88    /// proved was there.
89    fn get_jid(&mut self, key: &str, require: bool) -> Option<Jid> {
90        self.get_raw(key, require).and_then(|v| match v.to_jid() {
91            Some(jid) => Some(jid),
92            None => {
93                // to_jid() only returns None if it's a String that failed to parse
94                if let ValueRef::String(s) = v {
95                    self.errors
96                        .push(BinaryError::AttrParse(format!("Invalid JID: {s}")));
97                }
98                None
99            }
100        })
101    }
102
103    /// Get an optional JID attribute, failing when a present value is invalid.
104    pub fn optional_jid_result(&mut self, key: &str) -> Result<Option<Jid>> {
105        match self.get_raw(key, false) {
106            None => Ok(None),
107            Some(ValueRef::Jid(jid)) => Ok(Some(jid.to_owned())),
108            Some(ValueRef::String(value)) => {
109                Jid::from_str(value).map(Some).map_err(BinaryError::from)
110            }
111        }
112    }
113
114    /// Get a required JID attribute, failing immediately when it is missing or invalid.
115    ///
116    /// Structured JIDs are converted directly from their decoded representation;
117    /// string attributes are parsed exactly once.
118    pub fn required_jid(&mut self, key: &str) -> Result<Jid> {
119        self.optional_jid_result(key)?
120            .ok_or_else(|| BinaryError::MissingAttr(key.to_string()))
121    }
122
123    pub fn jid(&mut self, key: &str) -> Jid {
124        self.get_jid(key, true).unwrap_or_default()
125    }
126
127    pub fn non_ad_jid(&mut self, key: &str) -> Jid {
128        self.jid(key).to_non_ad()
129    }
130
131    fn get_string_value(&mut self, key: &str, require: bool) -> Option<Cow<'a, str>> {
132        self.get_raw(key, require).map(|v| v.as_str())
133    }
134
135    fn get_bool(&mut self, key: &str, require: bool) -> Option<bool> {
136        self.get_string_value(key, require)
137            .and_then(|s| match coerce_protocol_bool(&s) {
138                Some(val) => Some(val),
139                None => {
140                    self.errors.push(BinaryError::AttrParse(format!(
141                        "Failed to parse bool from '{s}' for key '{key}'"
142                    )));
143                    None
144                }
145            })
146    }
147
148    /// Parse an optional protocol boolean while preserving absence.
149    pub fn optional_bool_value(&mut self, key: &str) -> Option<bool> {
150        self.get_bool(key, false)
151    }
152
153    pub fn optional_bool(&mut self, key: &str) -> bool {
154        self.optional_bool_value(key).unwrap_or(false)
155    }
156
157    pub fn bool(&mut self, key: &str) -> bool {
158        self.get_bool(key, true).unwrap_or(false)
159    }
160
161    pub fn optional_u64(&mut self, key: &str) -> Option<u64> {
162        self.get_string_value(key, false)
163            .and_then(|s| match s.parse::<u64>() {
164                Ok(val) => Some(val),
165                Err(e) => {
166                    self.errors.push(BinaryError::AttrParse(format!(
167                        "Failed to parse u64 from '{s}' for key '{key}': {e}"
168                    )));
169                    None
170                }
171            })
172    }
173
174    pub fn unix_time(&mut self, key: &str) -> i64 {
175        self.get_i64(key, true).unwrap_or_default()
176    }
177
178    pub fn optional_unix_time(&mut self, key: &str) -> Option<i64> {
179        self.get_i64(key, false)
180    }
181
182    pub fn unix_milli(&mut self, key: &str) -> i64 {
183        self.get_i64(key, true).unwrap_or_default()
184    }
185
186    pub fn optional_unix_milli(&mut self, key: &str) -> Option<i64> {
187        self.get_i64(key, false)
188    }
189
190    fn get_i64(&mut self, key: &str, require: bool) -> Option<i64> {
191        self.get_string_value(key, require)
192            .and_then(|s| match s.parse::<i64>() {
193                Ok(val) => Some(val),
194                Err(e) => {
195                    self.errors.push(BinaryError::AttrParse(format!(
196                        "Failed to parse i64 from '{s}' for key '{key}': {e}"
197                    )));
198                    None
199                }
200            })
201    }
202}
203
204impl<'a> AttrParser<'a> {
205    pub fn new(node: &'a Node) -> Self {
206        Self {
207            attrs: &node.attrs,
208            errors: Vec::new(),
209        }
210    }
211
212    pub fn ok(&self) -> bool {
213        self.errors.is_empty()
214    }
215
216    pub fn finish(&self) -> Result<()> {
217        if self.ok() {
218            Ok(())
219        } else {
220            Err(BinaryError::AttrList(self.errors.clone()))
221        }
222    }
223
224    fn get_raw(&mut self, key: &str, require: bool) -> Option<&'a NodeValue> {
225        let val = self.attrs.get(key);
226        if require && val.is_none() {
227            self.errors.push(BinaryError::AttrParse(format!(
228                "Required attribute '{key}' not found"
229            )));
230        }
231        val
232    }
233
234    /// Get the string representation of the value (for numeric parsing, etc.)
235    fn get_string_value(&mut self, key: &str, require: bool) -> Option<Cow<'a, str>> {
236        self.get_raw(key, require).map(|v| match v {
237            NodeValue::String(s) => Cow::Borrowed(s.as_str()),
238            NodeValue::Jid(j) => Cow::Owned(j.to_string()),
239        })
240    }
241
242    // --- String ---
243    /// Get string from the value. Works for both String and JID variants.
244    /// - String variant: Cow::Borrowed — zero copy
245    /// - JID variant: Cow::Owned — allocates only when needed
246    pub fn optional_string(&mut self, key: &str) -> Option<Cow<'a, str>> {
247        self.get_raw(key, false).map(|v| v.as_str())
248    }
249
250    /// Get a required string attribute, returning an error if missing.
251    ///
252    /// Prefer this over `string()` for required attributes as it makes
253    /// the error explicit rather than silently defaulting to empty string.
254    pub fn required_string(&mut self, key: &str) -> Result<Cow<'a, str>> {
255        self.optional_string(key)
256            .ok_or_else(|| BinaryError::MissingAttr(key.to_string()))
257    }
258
259    // --- JID ---
260    /// Get JID from the value.
261    /// If the value is a JID variant, returns it directly without parsing (zero allocation clone).
262    /// If the value is a string, parses it as a JID.
263    pub fn optional_jid(&mut self, key: &str) -> Option<Jid> {
264        self.get_jid(key, false)
265    }
266
267    /// Shared by `optional_jid` and `jid`, so the required variant can record a
268    /// missing attribute and read the present one from the same lookup instead
269    /// of scanning the attribute list a second time to fetch what it just
270    /// proved was there.
271    fn get_jid(&mut self, key: &str, require: bool) -> Option<Jid> {
272        self.get_raw(key, require).and_then(|v| match v {
273            NodeValue::Jid(j) => Some(j.clone()),
274            NodeValue::String(s) => match Jid::from_str(s) {
275                Ok(jid) => Some(jid),
276                Err(e) => {
277                    self.errors.push(BinaryError::from(e));
278                    None
279                }
280            },
281        })
282    }
283
284    /// Get a required JID attribute, failing immediately when it is missing or invalid.
285    pub fn required_jid(&mut self, key: &str) -> Result<Jid> {
286        match self
287            .get_raw(key, false)
288            .ok_or_else(|| BinaryError::MissingAttr(key.to_string()))?
289        {
290            NodeValue::Jid(jid) => Ok(jid.clone()),
291            NodeValue::String(value) => Jid::from_str(value).map_err(BinaryError::from),
292        }
293    }
294
295    pub fn jid(&mut self, key: &str) -> Jid {
296        self.get_jid(key, true).unwrap_or_default()
297    }
298
299    pub fn non_ad_jid(&mut self, key: &str) -> Jid {
300        self.jid(key).to_non_ad()
301    }
302
303    // --- Boolean ---
304    fn get_bool(&mut self, key: &str, require: bool) -> Option<bool> {
305        self.get_string_value(key, require)
306            .and_then(|s| match coerce_protocol_bool(&s) {
307                Some(val) => Some(val),
308                None => {
309                    self.errors.push(BinaryError::AttrParse(format!(
310                        "Failed to parse bool from '{s}' for key '{key}'"
311                    )));
312                    None
313                }
314            })
315    }
316
317    /// Parse an optional protocol boolean while preserving absence.
318    pub fn optional_bool_value(&mut self, key: &str) -> Option<bool> {
319        self.get_bool(key, false)
320    }
321
322    pub fn optional_bool(&mut self, key: &str) -> bool {
323        self.optional_bool_value(key).unwrap_or(false)
324    }
325
326    pub fn bool(&mut self, key: &str) -> bool {
327        self.get_bool(key, true).unwrap_or(false)
328    }
329
330    // --- u64 ---
331    pub fn optional_u64(&mut self, key: &str) -> Option<u64> {
332        self.get_string_value(key, false)
333            .and_then(|s| match s.parse::<u64>() {
334                Ok(val) => Some(val),
335                Err(e) => {
336                    self.errors.push(BinaryError::AttrParse(format!(
337                        "Failed to parse u64 from '{s}' for key '{key}': {e}"
338                    )));
339                    None
340                }
341            })
342    }
343
344    pub fn unix_time(&mut self, key: &str) -> i64 {
345        self.get_i64(key, true).unwrap_or_default()
346    }
347
348    pub fn optional_unix_time(&mut self, key: &str) -> Option<i64> {
349        self.get_i64(key, false)
350    }
351
352    pub fn unix_milli(&mut self, key: &str) -> i64 {
353        self.get_i64(key, true).unwrap_or_default()
354    }
355
356    pub fn optional_unix_milli(&mut self, key: &str) -> Option<i64> {
357        self.get_i64(key, false)
358    }
359
360    fn get_i64(&mut self, key: &str, require: bool) -> Option<i64> {
361        self.get_string_value(key, require)
362            .and_then(|s| match s.parse::<i64>() {
363                Ok(val) => Some(val),
364                Err(e) => {
365                    self.errors.push(BinaryError::AttrParse(format!(
366                        "Failed to parse i64 from '{s}' for key '{key}': {e}"
367                    )));
368                    None
369                }
370            })
371    }
372}
373
374#[cfg(test)]
375mod required_getter_tests {
376    use super::*;
377    use crate::builder::NodeBuilder;
378
379    /// The required getters used to look the attribute up once to report it
380    /// missing and again to read it. Folding that into one lookup must not move
381    /// which error comes out: absent still reports "not found", and a present
382    /// but unparsable value still reports the parse failure rather than being
383    /// swallowed as absent.
384    #[test]
385    fn required_getters_keep_reporting_missing_and_invalid_separately() {
386        let node = NodeBuilder::new("message")
387            .attr("from", "5511999998888@s.whatsapp.net")
388            .attr("bad_jid", "@@@")
389            .attr("t", "1700000000")
390            .attr("bad_t", "not-a-number")
391            .build();
392
393        // Present and valid: no error, value read.
394        let mut p = AttrParser::new(&node);
395        assert_eq!(p.jid("from").user, "5511999998888");
396        assert_eq!(p.unix_time("t"), 1700000000);
397        assert!(p.ok(), "clean parse should not record errors");
398
399        // Absent: reported as missing, not as a parse failure.
400        let mut p = AttrParser::new(&node);
401        assert_eq!(p.jid("nope"), Jid::default());
402        assert!(format!("{:?}", p.errors).contains("not found"));
403
404        let mut p = AttrParser::new(&node);
405        assert_eq!(p.unix_time("nope"), 0);
406        assert!(format!("{:?}", p.errors).contains("not found"));
407
408        // Present but invalid: reported as a parse failure, not as missing.
409        let mut p = AttrParser::new(&node);
410        assert_eq!(p.jid("bad_jid"), Jid::default());
411        assert!(!p.ok());
412        assert!(!format!("{:?}", p.errors).contains("not found"));
413
414        let mut p = AttrParser::new(&node);
415        assert_eq!(p.unix_time("bad_t"), 0);
416        assert!(!p.ok());
417        assert!(!format!("{:?}", p.errors).contains("not found"));
418    }
419}
420
421#[cfg(test)]
422mod bool_coercion_tests {
423    use super::coerce_protocol_bool;
424
425    #[test]
426    fn coerce_protocol_bool_accepts_wire_forms() {
427        // "1"/"0" are the WhatsApp wire forms str::parse::<bool> used to reject.
428        for t in ["1", "true", "True", "t", "T", "TRUE"] {
429            assert_eq!(coerce_protocol_bool(t), Some(true), "{t}");
430        }
431        for f in ["0", "false", "False", "f", "F", "FALSE"] {
432            assert_eq!(coerce_protocol_bool(f), Some(false), "{f}");
433        }
434        // Matches whatsmeow strconv.ParseBool: on/off and junk are not booleans.
435        for bad in ["", "yes", "no", "2", "on", "off"] {
436            assert_eq!(coerce_protocol_bool(bad), None, "{bad}");
437        }
438    }
439}