Skip to main content

android_abx/
event.rs

1use base64::Engine as _;
2
3// Signed, like Java's `Integer.toString(v, 16)`: 0xCAFEBABE -> "-35014542".
4fn format_signed_hex(v: i64) -> String {
5    if v < 0 {
6        format!("-{:x}", v.unsigned_abs())
7    } else {
8        format!("{v:x}")
9    }
10}
11
12/// A typed attribute value.
13///
14/// Each variant is one of the value types of the wire format. Use
15/// [`as_str`](Self::as_str) to render any variant as text, or a typed accessor
16/// such as [`as_int`](Self::as_int) to read a specific one.
17///
18/// # Examples
19///
20/// ```
21/// use android_abx::AttributeValue;
22///
23/// let value = AttributeValue::Int(42);
24/// assert_eq!(value.as_int(), Some(42));
25/// assert_eq!(value.as_bool(), None);
26/// assert_eq!(value.as_str(), "42");
27/// ```
28#[derive(Debug, Clone, PartialEq)]
29pub enum AttributeValue {
30    /// No value.
31    Null,
32    /// A string.
33    String(String),
34    /// Bytes, rendered as lowercase hex.
35    BytesHex(Vec<u8>),
36    /// Bytes, rendered as Base64.
37    BytesBase64(Vec<u8>),
38    /// A 32-bit integer, rendered in decimal.
39    Int(i32),
40    /// A 32-bit integer, rendered in signed hex.
41    IntHex(u32),
42    /// A 64-bit integer, rendered in decimal.
43    Long(i64),
44    /// A 64-bit integer, rendered in signed hex.
45    LongHex(u64),
46    /// A 32-bit float.
47    Float(f32),
48    /// A 64-bit float.
49    Double(f64),
50    /// A boolean.
51    Boolean(bool),
52}
53
54impl AttributeValue {
55    /// Renders the value as text, the same way AOSP's `BinaryXmlPullParser` does.
56    ///
57    /// - `Null` is the empty string.
58    /// - Bytes are lowercase hex or standard Base64.
59    /// - `IntHex` and `LongHex` are signed, like Java's `Integer.toString(v, 16)`:
60    ///   `0xCAFEBABE` renders as `-35014542`.
61    /// - Whole `Float` and `Double` values keep a `.0` suffix.
62    ///
63    /// The result is not XML-escaped.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use android_abx::AttributeValue;
69    ///
70    /// assert_eq!(AttributeValue::IntHex(0xff).as_str(), "ff");
71    /// assert_eq!(AttributeValue::IntHex(0xCAFEBABE).as_str(), "-35014542");
72    /// assert_eq!(AttributeValue::Double(1.0).as_str(), "1.0");
73    /// assert_eq!(AttributeValue::BytesHex(vec![0xab, 0x01]).as_str(), "ab01");
74    /// ```
75    pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
76        use std::borrow::Cow;
77        match self {
78            AttributeValue::Null => Cow::Borrowed(""),
79            AttributeValue::String(s) => Cow::Borrowed(s.as_str()),
80            AttributeValue::BytesHex(b) => Cow::Owned(faster_hex::hex_string(b)),
81            AttributeValue::BytesBase64(b) => {
82                Cow::Owned(base64::engine::general_purpose::STANDARD.encode(b))
83            }
84            AttributeValue::Int(v) => Cow::Owned(v.to_string()),
85            AttributeValue::IntHex(v) => Cow::Owned(format_signed_hex(*v as i32 as i64)),
86            AttributeValue::Long(v) => Cow::Owned(v.to_string()),
87            AttributeValue::LongHex(v) => Cow::Owned(format_signed_hex(*v as i64)),
88            AttributeValue::Float(v) => {
89                if v.fract() == 0.0 && v.is_finite() {
90                    Cow::Owned(format!("{:.1}", v))
91                } else {
92                    Cow::Owned(v.to_string())
93                }
94            }
95            AttributeValue::Double(v) => {
96                if v.fract() == 0.0 && v.is_finite() {
97                    Cow::Owned(format!("{:.1}", v))
98                } else {
99                    Cow::Owned(v.to_string())
100                }
101            }
102            AttributeValue::Boolean(b) => {
103                if *b {
104                    Cow::Borrowed("true")
105                } else {
106                    Cow::Borrowed("false")
107                }
108            }
109        }
110    }
111
112    /// Returns the string if this is [`String`](Self::String), otherwise `None`.
113    pub fn as_string(&self) -> Option<&str> {
114        if let AttributeValue::String(s) = self {
115            Some(s)
116        } else {
117            None
118        }
119    }
120    /// Returns the value if this is [`Int`](Self::Int), otherwise `None`.
121    pub fn as_int(&self) -> Option<i32> {
122        if let AttributeValue::Int(v) = self {
123            Some(*v)
124        } else {
125            None
126        }
127    }
128    /// Returns the value if this is [`IntHex`](Self::IntHex), otherwise `None`.
129    pub fn as_int_hex(&self) -> Option<u32> {
130        if let AttributeValue::IntHex(v) = self {
131            Some(*v)
132        } else {
133            None
134        }
135    }
136    /// Returns the value if this is [`Long`](Self::Long), otherwise `None`.
137    pub fn as_long(&self) -> Option<i64> {
138        if let AttributeValue::Long(v) = self {
139            Some(*v)
140        } else {
141            None
142        }
143    }
144    /// Returns the value if this is [`LongHex`](Self::LongHex), otherwise `None`.
145    pub fn as_long_hex(&self) -> Option<u64> {
146        if let AttributeValue::LongHex(v) = self {
147            Some(*v)
148        } else {
149            None
150        }
151    }
152    /// Returns the value if this is [`Float`](Self::Float), otherwise `None`.
153    pub fn as_float(&self) -> Option<f32> {
154        if let AttributeValue::Float(v) = self {
155            Some(*v)
156        } else {
157            None
158        }
159    }
160    /// Returns the value if this is [`Double`](Self::Double), otherwise `None`.
161    pub fn as_double(&self) -> Option<f64> {
162        if let AttributeValue::Double(v) = self {
163            Some(*v)
164        } else {
165            None
166        }
167    }
168    /// Returns the value if this is [`Boolean`](Self::Boolean), otherwise `None`.
169    pub fn as_bool(&self) -> Option<bool> {
170        if let AttributeValue::Boolean(b) = self {
171            Some(*b)
172        } else {
173            None
174        }
175    }
176    /// Returns the bytes if this is [`BytesHex`](Self::BytesHex) or
177    /// [`BytesBase64`](Self::BytesBase64), otherwise `None`.
178    pub fn as_bytes(&self) -> Option<&[u8]> {
179        match self {
180            AttributeValue::BytesHex(b) | AttributeValue::BytesBase64(b) => Some(b),
181            _ => None,
182        }
183    }
184}
185
186/// A tag or attribute name.
187///
188/// An alias of [`SmolStr`](smol_str::SmolStr): names up to 23 bytes are stored
189/// inline, and cloning never allocates.
190pub type InternedStr = smol_str::SmolStr;
191
192/// An attribute: a name and a typed value.
193#[derive(Debug, Clone, PartialEq)]
194pub struct Attribute {
195    /// The attribute name.
196    pub name: InternedStr,
197    /// The attribute value.
198    pub value: AttributeValue,
199}
200
201impl Attribute {
202    /// Renders the value as text. See [`AttributeValue::as_str`].
203    pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
204        self.value.as_str()
205    }
206}
207
208/// A parse event, one per token in the document.
209///
210/// The parsers produce events, and [`AbxWriter`](crate::AbxWriter) encodes them.
211/// The variants follow the token types of Java's `XmlPullParser`.
212#[derive(Debug, Clone, PartialEq)]
213pub enum Event {
214    /// Start of the document.
215    StartDocument,
216    /// End of the document.
217    EndDocument,
218    /// An opening tag with its attributes.
219    StartTag {
220        /// The element's tag name.
221        name: InternedStr,
222        /// Attributes in document order.
223        attributes: Vec<Attribute>,
224    },
225    /// A closing tag.
226    EndTag {
227        /// The element's tag name, matching the corresponding [`Event::StartTag`].
228        name: InternedStr,
229    },
230    /// Character data, not escaped.
231    Text(String),
232    /// The content of a `<![CDATA[...]]>` section.
233    CdataSection(String),
234    /// The content of a `<!--...-->` comment.
235    Comment(String),
236    /// The content of a `<?...?>` processing instruction.
237    ProcessingInstruction(String),
238    /// An entity reference by name: `"amp"` for `&amp;`, not the resolved `&`.
239    EntityReference(String),
240    /// Whitespace marked as ignorable.
241    IgnorableWhitespace(String),
242    /// The content of a `<!DOCTYPE ...>` declaration.
243    DocDecl(String),
244}
245
246pub(crate) fn xml_escape(s: &str) -> std::borrow::Cow<'_, str> {
247    match s.find(['<', '>', '&', '"', '\'']) {
248        None => std::borrow::Cow::Borrowed(s),
249        Some(first) => {
250            let mut out = String::with_capacity(s.len() + 8);
251            out.push_str(&s[..first]);
252            for c in s[first..].chars() {
253                match c {
254                    '<' => out.push_str("&lt;"),
255                    '>' => out.push_str("&gt;"),
256                    '&' => out.push_str("&amp;"),
257                    '"' => out.push_str("&quot;"),
258                    '\'' => out.push_str("&apos;"),
259                    other => out.push(other),
260                }
261            }
262            std::borrow::Cow::Owned(out)
263        }
264    }
265}
266
267fn push_attr_value(buf: &mut String, value: &AttributeValue) {
268    use std::fmt::Write as _;
269    match value {
270        AttributeValue::Null => {}
271        AttributeValue::String(s) => buf.push_str(&xml_escape(s)),
272        AttributeValue::BytesHex(b) => buf.push_str(&faster_hex::hex_string(b)),
273        AttributeValue::BytesBase64(b) => {
274            base64::engine::general_purpose::STANDARD.encode_string(b, buf);
275        }
276        AttributeValue::Int(v) => {
277            let _ = write!(buf, "{v}");
278        }
279        AttributeValue::IntHex(v) => {
280            let _ = write!(buf, "{}", format_signed_hex(*v as i32 as i64));
281        }
282        AttributeValue::Long(v) => {
283            let _ = write!(buf, "{v}");
284        }
285        AttributeValue::LongHex(v) => {
286            let _ = write!(buf, "{}", format_signed_hex(*v as i64));
287        }
288        AttributeValue::Float(v) => {
289            if v.fract() == 0.0 && v.is_finite() {
290                let _ = write!(buf, "{v:.1}");
291            } else {
292                let _ = write!(buf, "{v}");
293            }
294        }
295        AttributeValue::Double(v) => {
296            if v.fract() == 0.0 && v.is_finite() {
297                let _ = write!(buf, "{v:.1}");
298            } else {
299                let _ = write!(buf, "{v}");
300            }
301        }
302        AttributeValue::Boolean(b) => buf.push_str(if *b { "true" } else { "false" }),
303    }
304}
305
306pub(crate) fn render_event(ev: &Event, buf: &mut String) {
307    match ev {
308        Event::StartDocument | Event::EndDocument => {}
309        Event::StartTag { name, attributes } => {
310            buf.push('<');
311            buf.push_str(name);
312            for attr in attributes {
313                buf.push(' ');
314                buf.push_str(&attr.name);
315                buf.push_str("=\"");
316                push_attr_value(buf, &attr.value);
317                buf.push('"');
318            }
319            buf.push('>');
320        }
321        Event::EndTag { name } => {
322            buf.push_str("</");
323            buf.push_str(name);
324            buf.push('>');
325        }
326        Event::Text(t) if !t.is_empty() => buf.push_str(&xml_escape(t)),
327        Event::CdataSection(t) => {
328            buf.push_str("<![CDATA[");
329            buf.push_str(t);
330            buf.push_str("]]>");
331        }
332        Event::Comment(t) => {
333            buf.push_str("<!--");
334            buf.push_str(t);
335            buf.push_str("-->");
336        }
337        Event::ProcessingInstruction(t) => {
338            buf.push_str("<?");
339            buf.push_str(t);
340            buf.push_str("?>");
341        }
342        Event::EntityReference(t) => {
343            buf.push('&');
344            buf.push_str(t);
345            buf.push(';');
346        }
347        Event::IgnorableWhitespace(t) => buf.push_str(t),
348        Event::DocDecl(t) => {
349            buf.push_str("<!DOCTYPE ");
350            buf.push_str(t);
351            buf.push('>');
352        }
353        _ => {}
354    }
355}