Skip to main content

ios_core/proto/
opack.rs

1/// opack encoder/decoder.
2///
3/// opack is a compact binary serialization format used by Apple for SRP pairing.
4/// Format:
5///   - Dictionary: 0xE0 | count, then count * (key, value) pairs
6///   - String:     0x40 | len (if len < 0x20), then bytes; or 0x61 + u8 len + bytes
7///   - Bytes:      0x70..0x7F (len 0..15), 0x80..0x8F (len 16..31),
8///     or 0x91 + u8 len + bytes
9///   - Integer:    0x08..0x0F (single byte values), 0x30 (i8), 0x31 (i16 le), etc.
10///   - Bool:       0x01 = true, 0x02 = false
11///   - Null:       0x04
12#[derive(Debug, Clone, PartialEq)]
13pub enum OpackValue {
14    Null,
15    Bool(bool),
16    Int(i64),
17    String(String),
18    Bytes(Vec<u8>),
19    Array(Vec<OpackValue>),
20    Dict(Vec<(OpackValue, OpackValue)>),
21}
22
23#[derive(Debug, thiserror::Error)]
24pub enum OpackError {
25    #[error("unexpected end of buffer")]
26    UnexpectedEof,
27    #[error("unknown opack tag: 0x{0:02X}")]
28    UnknownTag(u8),
29    #[error("invalid UTF-8 in string")]
30    InvalidUtf8,
31    #[error("opack encode error: {0}")]
32    Encode(String),
33}
34
35/// Encode an OpackValue into bytes.
36pub fn encode(value: &OpackValue) -> Result<Vec<u8>, OpackError> {
37    let mut out = Vec::new();
38    encode_value(value, &mut out)?;
39    Ok(out)
40}
41
42fn encode_value(value: &OpackValue, out: &mut Vec<u8>) -> Result<(), OpackError> {
43    match value {
44        OpackValue::Null => out.push(0x04),
45        OpackValue::Bool(true) => out.push(0x01),
46        OpackValue::Bool(false) => out.push(0x02),
47        OpackValue::Int(n) => {
48            if *n >= 0 && *n < 8 {
49                out.push(0x08 + *n as u8);
50            } else if *n >= i8::MIN as i64 && *n <= i8::MAX as i64 {
51                out.push(0x30);
52                out.push(*n as i8 as u8);
53            } else if *n >= i16::MIN as i64 && *n <= i16::MAX as i64 {
54                out.push(0x31);
55                out.extend_from_slice(&(*n as i16).to_le_bytes());
56            } else {
57                out.push(0x33);
58                out.extend_from_slice(&n.to_le_bytes());
59            }
60        }
61        OpackValue::String(s) => {
62            let bytes = s.as_bytes();
63            if bytes.len() < 0x20 {
64                out.push(0x40 | bytes.len() as u8);
65            } else if bytes.len() <= 0xFF {
66                out.push(0x61);
67                out.push(bytes.len() as u8);
68            } else {
69                if bytes.len() > u16::MAX as usize {
70                    return Err(OpackError::Encode(format!(
71                        "string too long: {} bytes (max {})",
72                        bytes.len(),
73                        u16::MAX
74                    )));
75                }
76                out.push(0x62);
77                out.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
78            }
79            out.extend_from_slice(bytes);
80        }
81        OpackValue::Bytes(b) => {
82            let len = b.len();
83            if len <= 0x0F {
84                out.push(0x70 | len as u8);
85            } else if len < 0x20 {
86                out.push(0x80 | (len as u8 & 0x0F));
87            } else if len <= 0xFF {
88                out.push(0x91);
89                out.push(len as u8);
90            } else {
91                return Err(OpackError::Encode(format!(
92                    "bytes too long: {len} bytes (max {})",
93                    u8::MAX
94                )));
95            }
96            out.extend_from_slice(b);
97        }
98        OpackValue::Array(arr) => {
99            if arr.len() > 0x0F {
100                return Err(OpackError::Encode(format!(
101                    "array too large: {} elements (max 15)",
102                    arr.len()
103                )));
104            }
105            out.push(0xD0 | (arr.len() as u8));
106            for v in arr {
107                encode_value(v, out)?;
108            }
109        }
110        OpackValue::Dict(pairs) => {
111            if pairs.len() > 0x0F {
112                return Err(OpackError::Encode(format!(
113                    "dict too large: {} entries (max 15)",
114                    pairs.len()
115                )));
116            }
117            out.push(0xE0 | (pairs.len() as u8));
118            for (k, v) in pairs {
119                encode_value(k, out)?;
120                encode_value(v, out)?;
121            }
122        }
123    }
124    Ok(())
125}
126
127/// Decode an OpackValue from bytes.
128pub fn decode(buf: &[u8]) -> Result<(OpackValue, usize), OpackError> {
129    decode_at(buf, 0)
130}
131
132fn decode_at(buf: &[u8], pos: usize) -> Result<(OpackValue, usize), OpackError> {
133    if pos >= buf.len() {
134        return Err(OpackError::UnexpectedEof);
135    }
136    let tag = buf[pos];
137    match tag {
138        0x04 => Ok((OpackValue::Null, pos + 1)),
139        0x01 => Ok((OpackValue::Bool(true), pos + 1)),
140        0x02 => Ok((OpackValue::Bool(false), pos + 1)),
141        0x08..=0x0F => Ok((OpackValue::Int((tag - 0x08) as i64), pos + 1)),
142        0x30 => {
143            if pos + 2 > buf.len() {
144                return Err(OpackError::UnexpectedEof);
145            }
146            Ok((OpackValue::Int(buf[pos + 1] as i8 as i64), pos + 2))
147        }
148        0x31 => {
149            if pos + 3 > buf.len() {
150                return Err(OpackError::UnexpectedEof);
151            }
152            let n = i16::from_le_bytes([buf[pos + 1], buf[pos + 2]]);
153            Ok((OpackValue::Int(n as i64), pos + 3))
154        }
155        0x33 => {
156            if pos + 9 > buf.len() {
157                return Err(OpackError::UnexpectedEof);
158            }
159            let mut arr = [0u8; 8];
160            arr.copy_from_slice(&buf[pos + 1..pos + 9]);
161            Ok((OpackValue::Int(i64::from_le_bytes(arr)), pos + 9))
162        }
163        0x40..=0x5F => {
164            let len = (tag - 0x40) as usize;
165            if pos + 1 + len > buf.len() {
166                return Err(OpackError::UnexpectedEof);
167            }
168            let s = std::str::from_utf8(&buf[pos + 1..pos + 1 + len])
169                .map_err(|_| OpackError::InvalidUtf8)?;
170            Ok((OpackValue::String(s.to_string()), pos + 1 + len))
171        }
172        0x61 => {
173            if pos + 2 > buf.len() {
174                return Err(OpackError::UnexpectedEof);
175            }
176            let len = buf[pos + 1] as usize;
177            if pos + 2 + len > buf.len() {
178                return Err(OpackError::UnexpectedEof);
179            }
180            let s = std::str::from_utf8(&buf[pos + 2..pos + 2 + len])
181                .map_err(|_| OpackError::InvalidUtf8)?;
182            Ok((OpackValue::String(s.to_string()), pos + 2 + len))
183        }
184        0x62 => {
185            if pos + 3 > buf.len() {
186                return Err(OpackError::UnexpectedEof);
187            }
188            let len = u16::from_le_bytes([buf[pos + 1], buf[pos + 2]]) as usize;
189            if pos + 3 + len > buf.len() {
190                return Err(OpackError::UnexpectedEof);
191            }
192            let s = std::str::from_utf8(&buf[pos + 3..pos + 3 + len])
193                .map_err(|_| OpackError::InvalidUtf8)?;
194            Ok((OpackValue::String(s.to_string()), pos + 3 + len))
195        }
196        0x70..=0x7F => {
197            let len = (tag - 0x70) as usize;
198            if pos + 1 + len > buf.len() {
199                return Err(OpackError::UnexpectedEof);
200            }
201            Ok((
202                OpackValue::Bytes(buf[pos + 1..pos + 1 + len].to_vec()),
203                pos + 1 + len,
204            ))
205        }
206        0x80..=0x8F => {
207            let len = 0x10 + (tag - 0x80) as usize;
208            if pos + 1 + len > buf.len() {
209                return Err(OpackError::UnexpectedEof);
210            }
211            Ok((
212                OpackValue::Bytes(buf[pos + 1..pos + 1 + len].to_vec()),
213                pos + 1 + len,
214            ))
215        }
216        0x91 => {
217            if pos + 2 > buf.len() {
218                return Err(OpackError::UnexpectedEof);
219            }
220            let len = buf[pos + 1] as usize;
221            if pos + 2 + len > buf.len() {
222                return Err(OpackError::UnexpectedEof);
223            }
224            Ok((
225                OpackValue::Bytes(buf[pos + 2..pos + 2 + len].to_vec()),
226                pos + 2 + len,
227            ))
228        }
229        0xD0..=0xDF => {
230            let count = (tag - 0xD0) as usize;
231            let mut items = Vec::with_capacity(count);
232            let mut cur = pos + 1;
233            for _ in 0..count {
234                let (v, next) = decode_at(buf, cur)?;
235                items.push(v);
236                cur = next;
237            }
238            Ok((OpackValue::Array(items), cur))
239        }
240        0xE0..=0xEF => {
241            let count = (tag - 0xE0) as usize;
242            let mut pairs = Vec::with_capacity(count);
243            let mut cur = pos + 1;
244            for _ in 0..count {
245                let (k, next) = decode_at(buf, cur)?;
246                cur = next;
247                let (v, next) = decode_at(buf, cur)?;
248                cur = next;
249                pairs.push((k, v));
250            }
251            Ok((OpackValue::Dict(pairs), cur))
252        }
253        _ => Err(OpackError::UnknownTag(tag)),
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::{decode, encode, OpackValue};
260
261    fn roundtrip(value: OpackValue) -> OpackValue {
262        let encoded = encode(&value).expect("encode should succeed");
263        let (decoded, used) = decode(&encoded).expect("decode should succeed");
264        assert_eq!(used, encoded.len());
265        decoded
266    }
267
268    fn sample_bytes(len: usize) -> Vec<u8> {
269        (0..len).map(|i| i as u8).collect()
270    }
271
272    #[test]
273    fn roundtrips_scalars() {
274        assert_eq!(roundtrip(OpackValue::Null), OpackValue::Null);
275        assert_eq!(roundtrip(OpackValue::Bool(true)), OpackValue::Bool(true));
276        assert_eq!(roundtrip(OpackValue::Bool(false)), OpackValue::Bool(false));
277        assert_eq!(roundtrip(OpackValue::Int(0)), OpackValue::Int(0));
278        assert_eq!(roundtrip(OpackValue::Int(7)), OpackValue::Int(7));
279        assert_eq!(roundtrip(OpackValue::Int(0xff)), OpackValue::Int(0xff));
280        assert_eq!(
281            roundtrip(OpackValue::Int(i64::MIN)),
282            OpackValue::Int(i64::MIN)
283        );
284    }
285
286    #[test]
287    fn roundtrips_strings_and_bytes() {
288        assert_eq!(
289            roundtrip(OpackValue::String(String::new())),
290            OpackValue::String(String::new())
291        );
292        assert_eq!(
293            roundtrip(OpackValue::String("short string".into())),
294            OpackValue::String("short string".into())
295        );
296        assert_eq!(
297            roundtrip(OpackValue::String("x".repeat(32))),
298            OpackValue::String("x".repeat(32))
299        );
300    }
301
302    #[test]
303    fn encodes_bytes_with_reference_tags() {
304        let cases = [
305            (0usize, vec![0x70]),
306            (1, vec![0x71]),
307            (15, vec![0x7F]),
308            (16, vec![0x80]),
309            (31, vec![0x8F]),
310            (32, vec![0x91, 0x20]),
311        ];
312
313        for (len, expected_prefix) in cases {
314            let bytes = sample_bytes(len);
315            let mut expected = expected_prefix;
316            expected.extend_from_slice(&bytes);
317            assert_eq!(encode(&OpackValue::Bytes(bytes)).unwrap(), expected);
318        }
319    }
320
321    #[test]
322    fn decodes_bytes_with_reference_tags() {
323        let cases = [
324            (vec![0x70], 0usize),
325            (
326                {
327                    let bytes = sample_bytes(1);
328                    let mut encoded = vec![0x71];
329                    encoded.extend_from_slice(&bytes);
330                    encoded
331                },
332                1usize,
333            ),
334            (
335                {
336                    let bytes = sample_bytes(15);
337                    let mut encoded = vec![0x7F];
338                    encoded.extend_from_slice(&bytes);
339                    encoded
340                },
341                15usize,
342            ),
343            (
344                {
345                    let bytes = sample_bytes(16);
346                    let mut encoded = vec![0x80];
347                    encoded.extend_from_slice(&bytes);
348                    encoded
349                },
350                16usize,
351            ),
352            (
353                {
354                    let bytes = sample_bytes(31);
355                    let mut encoded = vec![0x8F];
356                    encoded.extend_from_slice(&bytes);
357                    encoded
358                },
359                31usize,
360            ),
361            (
362                {
363                    let bytes = sample_bytes(32);
364                    let mut encoded = vec![0x91, 0x20];
365                    encoded.extend_from_slice(&bytes);
366                    encoded
367                },
368                32usize,
369            ),
370        ];
371
372        for (encoded, len) in cases {
373            let (decoded, used) = decode(&encoded).expect("decode should succeed");
374            assert_eq!(decoded, OpackValue::Bytes(sample_bytes(len)));
375            assert_eq!(used, encoded.len());
376        }
377    }
378
379    #[test]
380    fn roundtrips_collections_with_fifteen_entries() {
381        let array = OpackValue::Array((0..15).map(OpackValue::Int).collect());
382        assert_eq!(roundtrip(array.clone()), array);
383
384        let dict = OpackValue::Dict(
385            (0..15)
386                .map(|i| (OpackValue::String(format!("k{i}")), OpackValue::Int(i)))
387                .collect(),
388        );
389        assert_eq!(roundtrip(dict.clone()), dict);
390    }
391
392    #[test]
393    fn rejects_arrays_and_dicts_larger_than_reference_limit() {
394        let array = OpackValue::Array((0..16).map(OpackValue::Int).collect());
395        let err = encode(&array).unwrap_err();
396        assert!(err.to_string().contains("array too large"));
397
398        let dict = OpackValue::Dict(
399            (0..16)
400                .map(|i| (OpackValue::String(format!("k{i}")), OpackValue::Int(i)))
401                .collect(),
402        );
403        let err = encode(&dict).unwrap_err();
404        assert!(err.to_string().contains("dict too large"));
405    }
406}