Skip to main content

irontide_bencode/
span.rs

1use crate::error::{Error, Result};
2
3/// Find the raw byte span of a value for a given key in a bencoded dictionary.
4///
5/// This is critical for info-hash computation: the info-hash is the SHA1 of the
6/// *original raw bytes* of the "info" dictionary value, not a re-serialized copy.
7///
8/// Returns the byte range `start..end` of the value associated with `key`.
9///
10/// # Example
11///
12/// ```
13/// use irontide_bencode::find_dict_key_span;
14///
15/// let data = b"d4:infod4:name4:test12:piece lengthi1024eee";
16/// let span = find_dict_key_span(data, "info").unwrap();
17/// assert_eq!(&data[span.clone()], b"d4:name4:test12:piece lengthi1024ee");
18/// ```
19///
20/// # Errors
21///
22/// Returns an error if the data is not a valid bencoded dictionary or the key is not found.
23pub fn find_dict_key_span(data: &[u8], key: &str) -> Result<std::ops::Range<usize>> {
24    let mut pos = 0;
25
26    // Expect dict start
27    if data.get(pos) != Some(&b'd') {
28        return Err(Error::NotADictionary { position: pos });
29    }
30    pos += 1;
31
32    let key_bytes = key.as_bytes();
33
34    loop {
35        // Check for dict end
36        if data.get(pos) == Some(&b'e') {
37            return Err(Error::KeyNotFound {
38                key: key.to_string(),
39            });
40        }
41
42        if pos >= data.len() {
43            return Err(Error::UnexpectedEof {
44                position: pos,
45                context: "while scanning dict for key".into(),
46            });
47        }
48
49        // Parse key (byte string)
50        let parsed_key = parse_byte_string(data, &mut pos)?;
51
52        // Record value start
53        let value_start = pos;
54
55        // Skip value
56        skip_value(data, &mut pos)?;
57
58        // Check if this was our target key
59        if parsed_key == key_bytes {
60            return Ok(value_start..pos);
61        }
62    }
63}
64
65/// Parse a bencode byte string, returning the string data and advancing `pos`.
66fn parse_byte_string<'a>(data: &'a [u8], pos: &mut usize) -> Result<&'a [u8]> {
67    let start = *pos;
68
69    // Find colon
70    let colon = data[*pos..]
71        .iter()
72        .position(|&b| b == b':')
73        .ok_or_else(|| Error::InvalidByteString {
74            position: start,
75            detail: "missing ':'".into(),
76        })?;
77
78    let len_str =
79        std::str::from_utf8(&data[*pos..*pos + colon]).map_err(|_| Error::InvalidByteString {
80            position: start,
81            detail: "non-ASCII length".into(),
82        })?;
83
84    let len: usize =
85        len_str
86            .parse()
87            .map_err(|e: std::num::ParseIntError| Error::InvalidByteString {
88                position: start,
89                detail: e.to_string(),
90            })?;
91
92    *pos += colon + 1;
93
94    if *pos + len > data.len() {
95        return Err(Error::UnexpectedEof {
96            position: *pos,
97            context: format!("byte string needs {len} bytes"),
98        });
99    }
100
101    let result = &data[*pos..*pos + len];
102    *pos += len;
103    Ok(result)
104}
105
106/// Skip over a complete bencode value, advancing `pos` past it.
107fn skip_value(data: &[u8], pos: &mut usize) -> Result<()> {
108    match data.get(*pos) {
109        Some(b'i') => {
110            *pos += 1;
111            let end = data[*pos..]
112                .iter()
113                .position(|&b| b == b'e')
114                .ok_or_else(|| Error::UnexpectedEof {
115                    position: *pos,
116                    context: "unterminated integer".into(),
117                })?;
118            *pos += end + 1;
119            Ok(())
120        }
121        Some(b'l') => {
122            *pos += 1;
123            while data.get(*pos) != Some(&b'e') {
124                if *pos >= data.len() {
125                    return Err(Error::UnexpectedEof {
126                        position: *pos,
127                        context: "unterminated list".into(),
128                    });
129                }
130                skip_value(data, pos)?;
131            }
132            *pos += 1; // skip 'e'
133            Ok(())
134        }
135        Some(b'd') => {
136            *pos += 1;
137            while data.get(*pos) != Some(&b'e') {
138                if *pos >= data.len() {
139                    return Err(Error::UnexpectedEof {
140                        position: *pos,
141                        context: "unterminated dict".into(),
142                    });
143                }
144                parse_byte_string(data, pos)?; // key
145                skip_value(data, pos)?; // value
146            }
147            *pos += 1; // skip 'e'
148            Ok(())
149        }
150        Some(b'0'..=b'9') => {
151            parse_byte_string(data, pos)?;
152            Ok(())
153        }
154        Some(&byte) => Err(Error::UnexpectedByte {
155            byte,
156            position: *pos,
157            expected: "bencode value",
158        }),
159        None => Err(Error::UnexpectedEof {
160            position: *pos,
161            context: "expected value".into(),
162        }),
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn find_info_key() {
172        let data = b"d4:infod4:name4:test12:piece lengthi1024ee8:url-list4:httpe";
173        let span = find_dict_key_span(data, "info").unwrap();
174        assert_eq!(&data[span], b"d4:name4:test12:piece lengthi1024ee");
175    }
176
177    #[test]
178    fn find_last_key() {
179        let data = b"d1:ai1e1:bi2e1:ci3ee";
180        let span = find_dict_key_span(data, "c").unwrap();
181        assert_eq!(&data[span], b"i3e");
182    }
183
184    #[test]
185    fn key_not_found() {
186        let data = b"d1:ai1ee";
187        assert!(matches!(
188            find_dict_key_span(data, "z"),
189            Err(Error::KeyNotFound { .. })
190        ));
191    }
192
193    #[test]
194    fn not_a_dict() {
195        assert!(matches!(
196            find_dict_key_span(b"i42e", "info"),
197            Err(Error::NotADictionary { .. })
198        ));
199    }
200
201    #[test]
202    fn nested_dict_value() {
203        let data = b"d5:outerd5:inner3:valee";
204        let span = find_dict_key_span(data, "outer").unwrap();
205        assert_eq!(&data[span], b"d5:inner3:vale");
206    }
207}