Skip to main content

bencode/
lib.rs

1//! Bencode — the BitTorrent encoding ([BEP-3]) as a typed AST.
2//!
3//! Bencode is the wire format under every BitTorrent surface: `.torrent`
4//! metainfo, tracker announces, the DHT's KRPC, the extension protocol.
5//! This crate is the **keystone** of the pleme-io ENXAME suite
6//! (`theory/ENXAME.md`): nothing else parses without it.
7//!
8//! It is built in the pleme-io idiom — the **TYPED-SPEC + INTERPRETER
9//! TRIPLET** (the same shape as mado's `CsiCommand`, engawa's IR,
10//! sui-spec's domains):
11//!
12//! * **Typed border** — [`Bencode`] is the parse-don't-validate AST.
13//!   Bytes become one of four typed shapes; an ill-formed stream is a
14//!   typed [`Error`], never a panic.
15//! * **Parser** — [`parse`] / [`parse_prefix`]: bytes → [`Bencode`].
16//! * **Emitter** — [`Bencode::to_bytes`]: AST → bytes through a typed
17//!   builder, never `format!()` of the wire syntax (the ★★ TYPED
18//!   EMISSION rule). Dict keys serialize in sorted order — the
19//!   canonical contract BitTorrent hashing depends on — guaranteed by
20//!   the [`std::collections::BTreeMap`] backing [`Bencode::Dict`].
21//!
22//! The round-trip law `parse(x.to_bytes()) == x` and the canonical law
23//! `parse(b).map(to_bytes) == b` for canonical input are both pinned by
24//! property tests.
25//!
26//! [BEP-3]: https://www.bittorrent.org/beps/bep_0003.html
27//! [bencoding]: https://en.wikipedia.org/wiki/Bencode
28
29#![forbid(unsafe_code)]
30
31use std::collections::BTreeMap;
32
33/// A bencoded value — the four shapes the grammar admits.
34///
35/// `Bytes` is the only string type: bencode has no text/binary
36/// distinction on the wire, so the AST keeps raw bytes and offers
37/// [`Bencode::as_str`] for the UTF-8 view when a field is known textual.
38/// `Dict` is a [`BTreeMap`] so iteration — and therefore
39/// [`Bencode::to_bytes`] — is always in the sorted-key canonical order
40/// the protocol requires.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum Bencode {
43    /// `i<decimal>e` — a 64-bit signed integer.
44    Int(i64),
45    /// `<len>:<bytes>` — a length-prefixed byte string.
46    Bytes(Vec<u8>),
47    /// `l<elements>e` — an ordered list.
48    List(Vec<Bencode>),
49    /// `d<key><value>…e` — keys are byte strings, sorted + unique.
50    Dict(BTreeMap<Vec<u8>, Bencode>),
51}
52
53/// A typed parse failure. Every malformed-stream condition is one of
54/// these — the AST is never built from invalid bytes.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum Error {
57    /// The stream ended mid-value.
58    UnexpectedEnd,
59    /// A byte that can't begin a value (not `i`/`l`/`d`/digit), with its
60    /// offset.
61    UnexpectedByte { offset: usize, byte: u8 },
62    /// An integer with a leading zero (`i03e`), a bare/negative zero
63    /// (`i-0e`), or non-digit body.
64    InvalidInteger { offset: usize },
65    /// An integer that overflows `i64`.
66    IntegerOverflow { offset: usize },
67    /// A byte-string length that isn't valid base-10 or overruns the
68    /// buffer.
69    InvalidLength { offset: usize },
70    /// A dict whose keys are out of order or duplicated (canonical
71    /// parsing requires strictly-ascending unique keys).
72    UnsortedOrDuplicateKey { offset: usize },
73    /// A dict key that wasn't a byte string.
74    NonStringKey { offset: usize },
75    /// [`parse`] found valid trailing bytes after a complete value.
76    TrailingData { offset: usize },
77}
78
79impl std::fmt::Display for Error {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        match self {
82            Error::UnexpectedEnd => write!(f, "unexpected end of bencode stream"),
83            Error::UnexpectedByte { offset, byte } => {
84                write!(f, "unexpected byte {byte:#04x} at offset {offset}")
85            }
86            Error::InvalidInteger { offset } => write!(f, "invalid integer at offset {offset}"),
87            Error::IntegerOverflow { offset } => write!(f, "integer overflows i64 at offset {offset}"),
88            Error::InvalidLength { offset } => write!(f, "invalid byte-string length at offset {offset}"),
89            Error::UnsortedOrDuplicateKey { offset } => {
90                write!(f, "dict keys not strictly ascending at offset {offset}")
91            }
92            Error::NonStringKey { offset } => write!(f, "non-string dict key at offset {offset}"),
93            Error::TrailingData { offset } => write!(f, "trailing data after value at offset {offset}"),
94        }
95    }
96}
97
98impl std::error::Error for Error {}
99
100/// Parse exactly one bencoded value from `input`, requiring that the
101/// value consumes the **entire** buffer.
102///
103/// # Errors
104/// [`Error::TrailingData`] if bytes remain after the value; any parse
105/// error otherwise.
106pub fn parse(input: &[u8]) -> Result<Bencode, Error> {
107    let (value, rest) = parse_prefix(input)?;
108    if rest.is_empty() {
109        Ok(value)
110    } else {
111        Err(Error::TrailingData { offset: input.len() - rest.len() })
112    }
113}
114
115/// Parse one bencoded value from the front of `input`, returning the
116/// value and the unconsumed tail. Used when bencoded values are framed
117/// inside a larger stream (the peer-wire extension handshake, KRPC).
118///
119/// # Errors
120/// Any of [`Error`].
121pub fn parse_prefix(input: &[u8]) -> Result<(Bencode, &[u8]), Error> {
122    Parser { full: input, pos: 0 }.value().map(|v| {
123        // SAFETY-free: pos is always ≤ len.
124        (v.0, &input[v.1..])
125    })
126}
127
128struct Parser<'a> {
129    full: &'a [u8],
130    pos: usize,
131}
132
133impl Parser<'_> {
134    /// Parse one value starting at `self.pos`; return it + the new pos.
135    fn value(mut self) -> Result<(Bencode, usize), Error> {
136        let v = self.parse_value()?;
137        Ok((v, self.pos))
138    }
139
140    fn peek(&self) -> Result<u8, Error> {
141        self.full.get(self.pos).copied().ok_or(Error::UnexpectedEnd)
142    }
143
144    fn bump(&mut self) -> Result<u8, Error> {
145        let b = self.peek()?;
146        self.pos += 1;
147        Ok(b)
148    }
149
150    fn expect(&mut self, byte: u8) -> Result<(), Error> {
151        let at = self.pos;
152        if self.bump()? == byte {
153            Ok(())
154        } else {
155            Err(Error::UnexpectedByte { offset: at, byte: self.full[at] })
156        }
157    }
158
159    fn parse_value(&mut self) -> Result<Bencode, Error> {
160        match self.peek()? {
161            b'i' => self.parse_int(),
162            b'l' => self.parse_list(),
163            b'd' => self.parse_dict(),
164            b'0'..=b'9' => self.parse_bytes().map(Bencode::Bytes),
165            byte => Err(Error::UnexpectedByte { offset: self.pos, byte }),
166        }
167    }
168
169    fn parse_int(&mut self) -> Result<Bencode, Error> {
170        let start = self.pos;
171        self.expect(b'i')?;
172        let body_start = self.pos;
173        // Read up to the terminating 'e'.
174        let mut end = self.pos;
175        while self.full.get(end).is_some_and(|&b| b != b'e') {
176            end += 1;
177        }
178        if end >= self.full.len() {
179            return Err(Error::UnexpectedEnd);
180        }
181        let body = &self.full[body_start..end];
182        validate_integer_body(body).map_err(|()| Error::InvalidInteger { offset: start })?;
183        let s = std::str::from_utf8(body).map_err(|_| Error::InvalidInteger { offset: start })?;
184        let n = s.parse::<i64>().map_err(|_| Error::IntegerOverflow { offset: start })?;
185        self.pos = end + 1; // consume 'e'
186        Ok(Bencode::Int(n))
187    }
188
189    fn parse_bytes(&mut self) -> Result<Vec<u8>, Error> {
190        let start = self.pos;
191        let mut end = self.pos;
192        while self.full.get(end).is_some_and(u8::is_ascii_digit) {
193            end += 1;
194        }
195        let digits = &self.full[start..end];
196        // No leading zeros except a bare "0"; must be non-empty.
197        if digits.is_empty()
198            || (digits.len() > 1 && digits[0] == b'0')
199        {
200            return Err(Error::InvalidLength { offset: start });
201        }
202        let len: usize = std::str::from_utf8(digits)
203            .ok()
204            .and_then(|s| s.parse().ok())
205            .ok_or(Error::InvalidLength { offset: start })?;
206        if self.full.get(end) != Some(&b':') {
207            return Err(Error::InvalidLength { offset: end });
208        }
209        let data_start = end + 1;
210        let data_end = data_start.checked_add(len).ok_or(Error::InvalidLength { offset: start })?;
211        if data_end > self.full.len() {
212            return Err(Error::UnexpectedEnd);
213        }
214        self.pos = data_end;
215        Ok(self.full[data_start..data_end].to_vec())
216    }
217
218    fn parse_list(&mut self) -> Result<Bencode, Error> {
219        self.expect(b'l')?;
220        let mut items = Vec::new();
221        while self.peek()? != b'e' {
222            items.push(self.parse_value()?);
223        }
224        self.pos += 1; // consume 'e'
225        Ok(Bencode::List(items))
226    }
227
228    fn parse_dict(&mut self) -> Result<Bencode, Error> {
229        self.expect(b'd')?;
230        let mut map: BTreeMap<Vec<u8>, Bencode> = BTreeMap::new();
231        let mut last_key: Option<Vec<u8>> = None;
232        while self.peek()? != b'e' {
233            let key_offset = self.pos;
234            // Keys MUST be byte strings.
235            if !self.peek()?.is_ascii_digit() {
236                return Err(Error::NonStringKey { offset: key_offset });
237            }
238            let key = self.parse_bytes()?;
239            // Canonical: strictly-ascending unique keys.
240            if let Some(prev) = &last_key {
241                if key <= *prev {
242                    return Err(Error::UnsortedOrDuplicateKey { offset: key_offset });
243                }
244            }
245            let value = self.parse_value()?;
246            last_key = Some(key.clone());
247            map.insert(key, value);
248        }
249        self.pos += 1; // consume 'e'
250        Ok(Bencode::Dict(map))
251    }
252}
253
254/// A bencoded integer body is valid iff: non-empty, optional leading
255/// `-`, no leading zeros (except a bare `0`), no `-0`, only digits.
256fn validate_integer_body(body: &[u8]) -> Result<(), ()> {
257    match body {
258        [] => Err(()),
259        [b'0'] => Ok(()),
260        [b'-', b'0', ..] => Err(()),       // "-0", "-012"
261        [b'0', _, ..] => Err(()),          // "0…" with more digits
262        _ => {
263            let digits = body.strip_prefix(b"-").unwrap_or(body);
264            if digits.is_empty() || !digits.iter().all(u8::is_ascii_digit) {
265                Err(())
266            } else {
267                Ok(())
268            }
269        }
270    }
271}
272
273impl Bencode {
274    /// Encode to canonical bencode bytes through a typed builder — never
275    /// a `format!()` of the wire syntax (the ★★ TYPED EMISSION rule).
276    /// Dict keys emit in sorted order ([`BTreeMap`] iteration), the
277    /// canonicalization BitTorrent info-hashing relies on.
278    #[must_use]
279    pub fn to_bytes(&self) -> Vec<u8> {
280        let mut out = Vec::new();
281        self.encode_into(&mut out);
282        out
283    }
284
285    fn encode_into(&self, out: &mut Vec<u8>) {
286        match self {
287            Bencode::Int(n) => {
288                out.push(b'i');
289                push_i64(out, *n);
290                out.push(b'e');
291            }
292            Bencode::Bytes(bytes) => {
293                push_usize(out, bytes.len());
294                out.push(b':');
295                out.extend_from_slice(bytes);
296            }
297            Bencode::List(items) => {
298                out.push(b'l');
299                for item in items {
300                    item.encode_into(out);
301                }
302                out.push(b'e');
303            }
304            Bencode::Dict(map) => {
305                out.push(b'd');
306                for (key, value) in map {
307                    push_usize(out, key.len());
308                    out.push(b':');
309                    out.extend_from_slice(key);
310                    value.encode_into(out);
311                }
312                out.push(b'e');
313            }
314        }
315    }
316
317    // ── Typed accessors (the consume-don't-stringly-match surface) ──
318
319    /// The integer, if this is an [`Bencode::Int`].
320    #[must_use]
321    pub fn as_int(&self) -> Option<i64> {
322        match self {
323            Bencode::Int(n) => Some(*n),
324            _ => None,
325        }
326    }
327
328    /// The raw bytes, if this is a [`Bencode::Bytes`].
329    #[must_use]
330    pub fn as_bytes(&self) -> Option<&[u8]> {
331        match self {
332            Bencode::Bytes(b) => Some(b),
333            _ => None,
334        }
335    }
336
337    /// The UTF-8 string view of a byte string, if valid UTF-8.
338    #[must_use]
339    pub fn as_str(&self) -> Option<&str> {
340        self.as_bytes().and_then(|b| std::str::from_utf8(b).ok())
341    }
342
343    /// The elements, if this is a [`Bencode::List`].
344    #[must_use]
345    pub fn as_list(&self) -> Option<&[Bencode]> {
346        match self {
347            Bencode::List(items) => Some(items),
348            _ => None,
349        }
350    }
351
352    /// The map, if this is a [`Bencode::Dict`].
353    #[must_use]
354    pub fn as_dict(&self) -> Option<&BTreeMap<Vec<u8>, Bencode>> {
355        match self {
356            Bencode::Dict(map) => Some(map),
357            _ => None,
358        }
359    }
360
361    /// Look up a key in a dict (by byte key). `None` if not a dict or the
362    /// key is absent.
363    #[must_use]
364    pub fn get(&self, key: &[u8]) -> Option<&Bencode> {
365        self.as_dict().and_then(|m| m.get(key))
366    }
367}
368
369/// Push the base-10 bytes of an `i64` (TYPED EMISSION — a typed integer
370/// rendered to its decimal value, not a `format!()` of escape syntax).
371fn push_i64(out: &mut Vec<u8>, n: i64) {
372    if n < 0 {
373        out.push(b'-');
374        // Negate into u64 to handle i64::MIN without overflow.
375        push_u64(out, (n as i128).unsigned_abs() as u64);
376    } else {
377        push_u64(out, n as u64);
378    }
379}
380
381fn push_usize(out: &mut Vec<u8>, n: usize) {
382    push_u64(out, n as u64);
383}
384
385fn push_u64(out: &mut Vec<u8>, n: u64) {
386    if n == 0 {
387        out.push(b'0');
388        return;
389    }
390    // Render digits into a small stack buffer, then copy in order. u64
391    // is at most 20 decimal digits.
392    let mut buf = [0u8; 20];
393    let mut i = buf.len();
394    let mut v = n;
395    while v > 0 {
396        i -= 1;
397        buf[i] = b'0' + u8::try_from(v % 10).expect("digit 0..=9");
398        v /= 10;
399    }
400    out.extend_from_slice(&buf[i..]);
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    fn dict(pairs: &[(&str, Bencode)]) -> Bencode {
408        Bencode::Dict(pairs.iter().map(|(k, v)| ((*k).as_bytes().to_vec(), v.clone())).collect())
409    }
410    fn bytes(s: &str) -> Bencode {
411        Bencode::Bytes(s.as_bytes().to_vec())
412    }
413
414    #[test]
415    fn parses_the_bep3_examples() {
416        assert_eq!(parse(b"i42e").unwrap(), Bencode::Int(42));
417        assert_eq!(parse(b"i-42e").unwrap(), Bencode::Int(-42));
418        assert_eq!(parse(b"i0e").unwrap(), Bencode::Int(0));
419        assert_eq!(parse(b"4:spam").unwrap(), bytes("spam"));
420        assert_eq!(parse(b"0:").unwrap(), bytes(""));
421        assert_eq!(
422            parse(b"l4:spami42ee").unwrap(),
423            Bencode::List(vec![bytes("spam"), Bencode::Int(42)])
424        );
425        assert_eq!(
426            parse(b"d3:bar4:spam3:fooi42ee").unwrap(),
427            dict(&[("bar", bytes("spam")), ("foo", Bencode::Int(42))])
428        );
429    }
430
431    #[test]
432    fn rejects_malformed_integers() {
433        assert_eq!(parse(b"i03e"), Err(Error::InvalidInteger { offset: 0 }));
434        assert_eq!(parse(b"i-0e"), Err(Error::InvalidInteger { offset: 0 }));
435        assert_eq!(parse(b"ie"), Err(Error::InvalidInteger { offset: 0 }));
436        assert_eq!(parse(b"i12x3e"), Err(Error::InvalidInteger { offset: 0 }));
437        assert!(matches!(parse(b"i99999999999999999999e"), Err(Error::IntegerOverflow { .. })));
438    }
439
440    #[test]
441    fn rejects_malformed_strings_and_dicts() {
442        // leading-zero length
443        assert!(matches!(parse(b"01:a"), Err(Error::InvalidLength { .. })));
444        // length overruns buffer
445        assert_eq!(parse(b"5:abc"), Err(Error::UnexpectedEnd));
446        // out-of-order dict keys
447        assert!(matches!(
448            parse(b"d3:fooi1e3:bari2ee"),
449            Err(Error::UnsortedOrDuplicateKey { .. })
450        ));
451        // duplicate key
452        assert!(matches!(
453            parse(b"d1:ai1e1:ai2ee"),
454            Err(Error::UnsortedOrDuplicateKey { .. })
455        ));
456        // non-string key
457        assert!(matches!(parse(b"di1ei2ee"), Err(Error::NonStringKey { .. })));
458    }
459
460    #[test]
461    fn rejects_trailing_data() {
462        assert!(matches!(parse(b"i1ei2e"), Err(Error::TrailingData { .. })));
463        // parse_prefix keeps the tail instead
464        let (v, rest) = parse_prefix(b"i1ei2e").unwrap();
465        assert_eq!(v, Bencode::Int(1));
466        assert_eq!(rest, b"i2e");
467    }
468
469    #[test]
470    fn encodes_canonically_with_sorted_keys() {
471        // Insert keys out of order; emission must sort them.
472        let mut map = BTreeMap::new();
473        map.insert(b"foo".to_vec(), Bencode::Int(42));
474        map.insert(b"bar".to_vec(), bytes("spam"));
475        assert_eq!(Bencode::Dict(map).to_bytes(), b"d3:bar4:spam3:fooi42ee");
476        assert_eq!(Bencode::Int(-42).to_bytes(), b"i-42e");
477        assert_eq!(Bencode::Int(0).to_bytes(), b"i0e");
478        assert_eq!(bytes("").to_bytes(), b"0:");
479    }
480
481    #[test]
482    fn encodes_i64_extremes_without_format() {
483        assert_eq!(Bencode::Int(i64::MAX).to_bytes(), b"i9223372036854775807e");
484        assert_eq!(Bencode::Int(i64::MIN).to_bytes(), b"i-9223372036854775808e");
485    }
486
487    proptest::proptest! {
488        /// Round-trip law: parsing what we encoded yields the original.
489        #[test]
490        fn parse_of_encode_is_identity(v in arb_bencode(4)) {
491            let bytes = v.to_bytes();
492            proptest::prop_assert_eq!(parse(&bytes).unwrap(), v);
493        }
494
495        /// Canonical law: re-encoding a parsed canonical stream is a
496        /// byte-for-byte fixed point (what info-hashing relies on).
497        #[test]
498        fn encode_of_parse_is_canonical_fixed_point(v in arb_bencode(4)) {
499            let bytes = v.to_bytes();
500            let reparsed = parse(&bytes).unwrap();
501            proptest::prop_assert_eq!(reparsed.to_bytes(), bytes);
502        }
503    }
504
505    /// A recursive arbitrary `Bencode` with bounded depth.
506    fn arb_bencode(depth: u32) -> impl proptest::strategy::Strategy<Value = Bencode> {
507        use proptest::collection::{btree_map, vec};
508        use proptest::prelude::*;
509        let leaf = prop_oneof![
510            any::<i64>().prop_map(Bencode::Int),
511            vec(any::<u8>(), 0..16).prop_map(Bencode::Bytes),
512        ];
513        leaf.prop_recursive(depth, 64, 8, |inner| {
514            prop_oneof![
515                vec(inner.clone(), 0..6).prop_map(Bencode::List),
516                btree_map(vec(any::<u8>(), 0..8), inner, 0..6).prop_map(Bencode::Dict),
517            ]
518        })
519    }
520}