Skip to main content

abx/encode/
writer.rs

1//! [`AbxWriter`] — encodes [`Event`]s to the ABX wire format, the mechanical
2//! reverse of [`crate::AbxParser`]/[`crate::AbxStreamParser`]'s decoding.
3
4use std::collections::HashMap;
5use std::io::Write;
6
7use crate::{AbxError, Attribute, AttributeValue, Event, InternedStr, MAGIC, Result};
8use crate::{
9    CMD_ATTRIBUTE, CMD_CDSECT, CMD_COMMENT, CMD_DOCDECL, CMD_END_DOCUMENT, CMD_END_TAG,
10    CMD_ENTITY_REF, CMD_IGNORABLE_WHITESPACE, CMD_PROCESSING_INSTRUCTION, CMD_START_DOCUMENT,
11    CMD_START_TAG, CMD_TEXT,
12};
13use crate::{
14    INTERNED_NEW, TYPE_BOOLEAN_FALSE, TYPE_BOOLEAN_TRUE, TYPE_BYTES_BASE64, TYPE_BYTES_HEX,
15    TYPE_DOUBLE, TYPE_FLOAT, TYPE_INT, TYPE_INT_HEX, TYPE_LONG, TYPE_LONG_HEX, TYPE_NULL,
16    TYPE_STRING, TYPE_STRING_INTERNED,
17};
18
19/// Tracks tag/attribute names already written, so repeats become a `u16`
20/// back-reference instead of a fresh string. Covers names only, never
21/// attribute values — matches AOSP's `attribute()`, which never
22/// auto-interns a value.
23///
24/// Below `LINEAR_SCAN_LIMIT` unique names, scans `names` linearly instead
25/// of hashing — real documents repeat a small, bounded vocabulary of
26/// names, so scanning a short `Vec` beats hash overhead. A linear scan is
27/// O(n²) in the number of unique names though, so past the limit this
28/// switches to a `HashMap`.
29const LINEAR_SCAN_LIMIT: usize = 32;
30
31/// Largest length a `u16` wire length-prefix can express. Matches AOSP's
32/// `FastDataOutput.MAX_UNSIGNED_SHORT` — `writeUTF()` throws past this, and
33/// `BinaryXmlSerializer.attributeBytesHex`/`attributeBytesBase64` check it
34/// explicitly before writing anything.
35const MAX_UNSIGNED_SHORT: usize = 65_535;
36
37struct InternedPool {
38    names: Vec<InternedStr>,
39    index: Option<HashMap<InternedStr, u16>>,
40}
41
42impl InternedPool {
43    fn new() -> Self {
44        InternedPool {
45            names: Vec::new(),
46            index: None,
47        }
48    }
49
50    fn find(&self, s: &InternedStr) -> Option<u16> {
51        match &self.index {
52            Some(index) => index.get(s).copied(),
53            None => self.names.iter().position(|n| n == s).map(|i| i as u16),
54        }
55    }
56
57    fn write(&mut self, out: &mut impl Write, s: &InternedStr) -> Result<()> {
58        if let Some(idx) = self.find(s) {
59            out.write_all(&idx.to_be_bytes())?;
60        } else {
61            out.write_all(&INTERNED_NEW.to_be_bytes())?;
62            write_utf(out, s)?;
63
64            // 0xFFFF is the INTERNED_NEW sentinel, so indices only go up
65            // to 0xFFFE -- past that, stop caching. Matches real AOSP,
66            // which also keeps working past its cap instead of erroring;
67            // only uncached names lose the back-reference.
68            if self.names.len() < INTERNED_NEW as usize {
69                let idx = self.names.len() as u16;
70                self.names.push(s.clone());
71                if let Some(index) = &mut self.index {
72                    index.insert(s.clone(), idx);
73                } else if self.names.len() > LINEAR_SCAN_LIMIT {
74                    self.index = Some(self.names.iter().cloned().zip(0u16..).collect());
75                }
76            }
77        }
78        Ok(())
79    }
80}
81
82/// Errors (rather than truncating the `u16` length prefix, which would
83/// silently corrupt the stream for anything written after) when `bytes` is
84/// longer than a `u16` length can express — same boundary and same
85/// reject-don't-truncate behavior as AOSP's `writeUTF()`.
86fn write_utf(out: &mut impl Write, s: &str) -> Result<()> {
87    let bytes = s.as_bytes();
88    if bytes.len() > MAX_UNSIGNED_SHORT {
89        return Err(AbxError::ValueTooLong {
90            len: bytes.len(),
91            max: MAX_UNSIGNED_SHORT,
92        });
93    }
94    out.write_all(&(bytes.len() as u16).to_be_bytes())?;
95    out.write_all(bytes)?;
96    Ok(())
97}
98
99/// See [`write_utf`] — same overflow check, for raw byte blobs
100/// (`BytesHex`/`BytesBase64`), matching AOSP's `attributeBytesHex`/
101/// `attributeBytesBase64` length checks.
102fn write_bytes_blob(out: &mut impl Write, bytes: &[u8]) -> Result<()> {
103    if bytes.len() > MAX_UNSIGNED_SHORT {
104        return Err(AbxError::ValueTooLong {
105            len: bytes.len(),
106            max: MAX_UNSIGNED_SHORT,
107        });
108    }
109    out.write_all(&(bytes.len() as u16).to_be_bytes())?;
110    out.write_all(bytes)?;
111    Ok(())
112}
113
114/// Encodes [`Event`]s to any `W: Write` sink. `Vec<u8>` covers the
115/// in-memory case (it implements `Write`); a file/socket/`BufWriter` covers
116/// streaming — unlike the decode side, writing has no ring-buffer/refill
117/// complexity to split across two types.
118pub struct AbxWriter<W: Write> {
119    writer: W,
120    pool: InternedPool,
121}
122
123impl<W: Write> AbxWriter<W> {
124    /// Create a writer, writing the 4-byte magic header immediately.
125    pub fn new(mut writer: W) -> Result<Self> {
126        writer.write_all(&MAGIC)?;
127        Ok(AbxWriter {
128            writer,
129            pool: InternedPool::new(),
130        })
131    }
132
133    /// Encode and write a single [`Event`].
134    pub fn write_event(&mut self, ev: &Event) -> Result<()> {
135        match ev {
136            Event::StartDocument => self.writer.write_all(&[CMD_START_DOCUMENT | TYPE_NULL])?,
137            Event::EndDocument => self.writer.write_all(&[CMD_END_DOCUMENT | TYPE_NULL])?,
138            Event::StartTag { name, attributes } => {
139                self.writer
140                    .write_all(&[TYPE_STRING_INTERNED | CMD_START_TAG])?;
141                self.pool.write(&mut self.writer, name)?;
142                for attr in attributes {
143                    self.write_attribute(attr)?;
144                }
145            }
146            Event::EndTag { name } => {
147                self.writer
148                    .write_all(&[TYPE_STRING_INTERNED | CMD_END_TAG])?;
149                self.pool.write(&mut self.writer, name)?;
150            }
151            Event::Text(s) => self.write_text_token(CMD_TEXT, s)?,
152            Event::CdataSection(s) => self.write_text_token(CMD_CDSECT, s)?,
153            Event::Comment(s) => self.write_text_token(CMD_COMMENT, s)?,
154            Event::ProcessingInstruction(s) => {
155                self.write_text_token(CMD_PROCESSING_INSTRUCTION, s)?
156            }
157            Event::EntityReference(s) => self.write_text_token(CMD_ENTITY_REF, s)?,
158            Event::IgnorableWhitespace(s) => self.write_text_token(CMD_IGNORABLE_WHITESPACE, s)?,
159            Event::DocDecl(s) => self.write_text_token(CMD_DOCDECL, s)?,
160        }
161        Ok(())
162    }
163
164    /// Shared shape for the seven text-bearing events: always `TYPE_STRING`
165    /// plus length-prefixed UTF-8, even for an empty string. Never
166    /// `TYPE_NULL` — real AOSP's own parser can't correctly read that form
167    /// back (a confirmed bug: it calls `readUTF()` unconditionally here,
168    /// unlike the `ATTRIBUTE` branch), so `TYPE_STRING` with an empty
169    /// payload is the only safe choice.
170    fn write_text_token(&mut self, cmd: u8, s: &str) -> Result<()> {
171        self.writer.write_all(&[TYPE_STRING | cmd])?;
172        write_utf(&mut self.writer, s)?;
173        Ok(())
174    }
175
176    /// Write one attribute: `type_nibble|CMD_ATTRIBUTE` + interned name +
177    /// the value's payload. `String` values are never interned — matches
178    /// AOSP's `attribute()`, which only interns the name.
179    fn write_attribute(&mut self, attr: &Attribute) -> Result<()> {
180        let type_nibble = match &attr.value {
181            AttributeValue::Null => TYPE_NULL,
182            AttributeValue::String(_) => TYPE_STRING,
183            AttributeValue::BytesHex(_) => TYPE_BYTES_HEX,
184            AttributeValue::BytesBase64(_) => TYPE_BYTES_BASE64,
185            AttributeValue::Int(_) => TYPE_INT,
186            AttributeValue::IntHex(_) => TYPE_INT_HEX,
187            AttributeValue::Long(_) => TYPE_LONG,
188            AttributeValue::LongHex(_) => TYPE_LONG_HEX,
189            AttributeValue::Float(_) => TYPE_FLOAT,
190            AttributeValue::Double(_) => TYPE_DOUBLE,
191            AttributeValue::Boolean(true) => TYPE_BOOLEAN_TRUE,
192            AttributeValue::Boolean(false) => TYPE_BOOLEAN_FALSE,
193        };
194        self.writer.write_all(&[type_nibble | CMD_ATTRIBUTE])?;
195        self.pool.write(&mut self.writer, &attr.name)?;
196        match &attr.value {
197            AttributeValue::Null | AttributeValue::Boolean(_) => {}
198            AttributeValue::String(s) => write_utf(&mut self.writer, s)?,
199            AttributeValue::BytesHex(b) | AttributeValue::BytesBase64(b) => {
200                write_bytes_blob(&mut self.writer, b)?
201            }
202            AttributeValue::Int(v) => self.writer.write_all(&v.to_be_bytes())?,
203            AttributeValue::IntHex(v) => self.writer.write_all(&v.to_be_bytes())?,
204            AttributeValue::Long(v) => self.writer.write_all(&v.to_be_bytes())?,
205            AttributeValue::LongHex(v) => self.writer.write_all(&v.to_be_bytes())?,
206            AttributeValue::Float(v) => self.writer.write_all(&v.to_be_bytes())?,
207            AttributeValue::Double(v) => self.writer.write_all(&v.to_be_bytes())?,
208        }
209        Ok(())
210    }
211
212    /// Unwrap the underlying writer.
213    pub fn into_inner(self) -> W {
214        self.writer
215    }
216}