1use crate::error::{Error, Result};
2
3pub fn find_dict_key_span(data: &[u8], key: &str) -> Result<std::ops::Range<usize>> {
24 let mut pos = 0;
25
26 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 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 let parsed_key = parse_byte_string(data, &mut pos)?;
51
52 let value_start = pos;
54
55 skip_value(data, &mut pos)?;
57
58 if parsed_key == key_bytes {
60 return Ok(value_start..pos);
61 }
62 }
63}
64
65fn parse_byte_string<'a>(data: &'a [u8], pos: &mut usize) -> Result<&'a [u8]> {
67 let start = *pos;
68
69 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
106fn 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; 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)?; skip_value(data, pos)?; }
147 *pos += 1; 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}