symbolic_il2cpp/line_mapping/
from_object.rs1use std::collections::BTreeMap;
2use std::io::Write;
3use std::iter::Enumerate;
4use std::str::Lines;
5
6use symbolic_common::{ByteView, DebugId};
7use symbolic_debuginfo::{DebugSession, ObjectLike};
8
9pub struct ObjectLineMapping {
14 mapping: BTreeMap<String, BTreeMap<String, BTreeMap<u32, u32>>>,
15 debug_id: DebugId,
16}
17
18impl ObjectLineMapping {
19 pub fn from_object<'data, 'object, O, E>(object: &'object O) -> Result<Self, E>
25 where
26 O: ObjectLike<'data, 'object, Error = E>,
27 {
28 Self::from_object_with_provider(object, |path| ByteView::open(path).ok())
30 }
31
32 pub fn from_object_with_provider<'data, 'object, O, E, B, P>(
42 object: &'object O,
43 mut provider: P,
44 ) -> Result<Self, E>
45 where
46 O: ObjectLike<'data, 'object, Error = E>,
47 B: AsRef<[u8]>,
48 P: FnMut(&str) -> Option<B>,
49 {
50 let session = object.debug_session()?;
51 let debug_id = object.debug_id();
52
53 let mut mapping = BTreeMap::new();
54
55 for cpp_file in session.files() {
56 let cpp_file_path = cpp_file?.abs_path_str();
57 if mapping.contains_key(&cpp_file_path) {
58 continue;
59 }
60
61 if let Some(cpp_source) = provider(&cpp_file_path) {
62 let cpp_mapping = Self::parse_source_file(cpp_source.as_ref());
63 if !cpp_mapping.is_empty() {
64 mapping.insert(cpp_file_path, cpp_mapping);
65 }
66 }
67 }
68
69 Ok(Self { mapping, debug_id })
70 }
71
72 pub(crate) fn parse_source_file(cpp_source: &[u8]) -> BTreeMap<String, BTreeMap<u32, u32>> {
76 let mut cpp_mapping = BTreeMap::new();
77
78 for SourceInfo {
79 cpp_line,
80 cs_file,
81 cs_line,
82 } in SourceInfos::new(cpp_source)
83 {
84 let cs_mapping = cpp_mapping
85 .entry(cs_file.to_string())
86 .or_insert_with(BTreeMap::new);
87 cs_mapping.insert(cpp_line, cs_line);
88 }
89
90 cpp_mapping
91 }
92
93 pub fn to_writer<W: Write>(mut self, writer: &mut W) -> std::io::Result<bool> {
100 let is_empty = self.mapping.is_empty();
101
102 let value = BTreeMap::from([(self.debug_id.to_string(), Default::default())]);
109 self.mapping.insert("__debug-id__".to_owned(), value);
110
111 serde_json::to_writer(writer, &self.mapping)?;
112 Ok(!is_empty)
113 }
114}
115
116#[derive(Debug, PartialEq, Eq)]
118pub(crate) struct SourceInfo<'data> {
119 pub(crate) cpp_line: u32,
121 cs_file: &'data str,
123 pub(crate) cs_line: u32,
125}
126
127pub(crate) struct SourceInfos<'data> {
131 lines: Enumerate<Lines<'data>>,
132 current: Option<(&'data str, u32)>,
133}
134
135impl<'data> SourceInfos<'data> {
136 pub(crate) fn new(source: &'data [u8]) -> Self {
138 let lines = std::str::from_utf8(source)
139 .ok()
140 .unwrap_or_default()
141 .lines()
142 .enumerate();
143 Self {
144 lines,
145 current: None,
146 }
147 }
148}
149
150impl<'data> Iterator for SourceInfos<'data> {
151 type Item = SourceInfo<'data>;
152
153 fn next(&mut self) -> Option<Self::Item> {
154 for (cpp_line_nr, cpp_src_line) in &mut self.lines {
155 match parse_line(cpp_src_line) {
156 Some((cs_file, cs_line)) => {
158 if let Some((cs_file, cs_line)) = self.current.replace((cs_file, cs_line)) {
159 return Some(SourceInfo {
160 cpp_line: cpp_line_nr as u32,
161 cs_file,
162 cs_line,
163 });
164 }
165 }
166
167 None if cpp_src_line.trim_start().starts_with("//") => continue,
169 None => {
171 if let Some((cs_file, cs_line)) = self.current.take() {
172 return Some(SourceInfo {
173 cpp_line: (cpp_line_nr + 1) as u32,
174 cs_file,
175 cs_line,
176 });
177 }
178 }
179 }
180 }
181 None
182 }
183}
184
185fn parse_line(line: &str) -> Option<(&str, u32)> {
190 let line = line.trim();
191 let source_ref = line.strip_prefix("//<source_info:")?;
192 let source_ref = source_ref.strip_suffix('>')?;
193 let (file, line) = source_ref.rsplit_once(':')?;
194 let line = line.parse().ok()?;
195 Some((file, line))
196}
197
198#[cfg(test)]
199mod tests {
200 use symbolic_common::ByteView;
201 use symbolic_debuginfo::Object;
202 use symbolic_testutils::fixture;
203
204 use super::*;
205
206 #[test]
207 fn one_mapping() {
208 let cpp_source = b"
209 Lorem ipsum dolor sit amet
210 //<source_info:main.cs:17>
211 // some
212 // more
213 // comments
214 actual source code";
215
216 let source_infos: Vec<_> = SourceInfos::new(cpp_source).collect();
217
218 assert_eq!(
219 source_infos,
220 vec![SourceInfo {
221 cpp_line: 7,
222 cs_file: "main.cs",
223 cs_line: 17,
224 }]
225 )
226 }
227
228 #[test]
229 fn several_mappings() {
230 let cpp_source = b"
231 Lorem ipsum dolor sit amet
232 //<source_info:main.cs:17>
233 // some
234 // comments
235 actual source code 1
236 actual source code 2
237
238 //<source_info:main.cs:29>
239 actual source code 3
240
241 //<source_info:main.cs:46>
242 // more
243 // comments
244 actual source code 4";
245
246 let source_infos: Vec<_> = SourceInfos::new(cpp_source).collect();
247
248 assert_eq!(
249 source_infos,
250 vec![
251 SourceInfo {
252 cpp_line: 6,
253 cs_file: "main.cs",
254 cs_line: 17,
255 },
256 SourceInfo {
257 cpp_line: 10,
258 cs_file: "main.cs",
259 cs_line: 29,
260 },
261 SourceInfo {
262 cpp_line: 15,
263 cs_file: "main.cs",
264 cs_line: 46,
265 }
266 ]
267 )
268 }
269
270 #[test]
271 fn missing_source_line() {
272 let cpp_source = b"
273 Lorem ipsum dolor sit amet
274 //<source_info:main.cs:17>
275 // some
276 // comments
277 //<source_info:main.cs:29>
278 actual source code";
279
280 let source_infos: Vec<_> = SourceInfos::new(cpp_source).collect();
281
282 assert_eq!(
285 source_infos,
286 vec![
287 SourceInfo {
288 cpp_line: 5,
289 cs_file: "main.cs",
290 cs_line: 17,
291 },
292 SourceInfo {
293 cpp_line: 7,
294 cs_file: "main.cs",
295 cs_line: 29,
296 },
297 ]
298 )
299 }
300
301 #[test]
302 fn broken() {
303 let cpp_source = b"
304 Lorem ipsum dolor sit amet
305 //<source_info:main.cs:17>
306 // some
307 // more
308 // comments";
309
310 assert_eq!(SourceInfos::new(cpp_source).count(), 0);
313 }
314
315 const SYNTHETIC_SOURCE: &[u8] = b"//<source_info:Game.cs:42>\nint generated = 0;\n";
318
319 #[test]
320 fn test_object_line_mapping_parses_source_info() {
321 let data = ByteView::open(fixture("windows/Sentry.Samples.Console.Basic.pdb")).unwrap();
322 let object = Object::parse(&data).unwrap();
323
324 let mut calls = 0usize;
325 let mapping = ObjectLineMapping::from_object_with_provider(&object, |path| {
326 assert!(!path.is_empty());
327 calls += 1;
328 Some(SYNTHETIC_SOURCE.to_vec())
329 })
330 .unwrap();
331
332 assert!(calls > 0);
333
334 let mut buf = Vec::new();
335 assert!(mapping.to_writer(&mut buf).unwrap());
336
337 let json: serde_json::Value = serde_json::from_slice(&buf).unwrap();
338 insta::assert_json_snapshot!(json, @r#"
339 {
340 "C:\\dev\\sentry-dotnet\\samples\\Sentry.Samples.Console.Basic\\Program.cs": {
341 "Game.cs": {
342 "2": 42
343 }
344 },
345 "C:\\dev\\sentry-dotnet\\samples\\Sentry.Samples.Console.Basic\\obj\\release\\net6.0\\.NETCoreApp,Version=v6.0.AssemblyAttributes.cs": {
346 "Game.cs": {
347 "2": 42
348 }
349 },
350 "C:\\dev\\sentry-dotnet\\samples\\Sentry.Samples.Console.Basic\\obj\\release\\net6.0\\Sentry.Samples.Console.Basic.AssemblyInfo.cs": {
351 "Game.cs": {
352 "2": 42
353 }
354 },
355 "C:\\dev\\sentry-dotnet\\samples\\Sentry.Samples.Console.Basic\\obj\\release\\net6.0\\Sentry.Samples.Console.Basic.GlobalUsings.g.cs": {
356 "Game.cs": {
357 "2": 42
358 }
359 },
360 "__debug-id__": {
361 "526f365f-4d8d-4fa8-b370-eae9a9136de4-a39453e5": {}
362 }
363 }
364 "#);
365 }
366
367 #[test]
368 fn test_object_line_mapping_no_sources() {
369 let view = ByteView::open(fixture("windows/Sentry.Samples.Console.Basic.pdb")).unwrap();
370 let object = Object::parse(&view).unwrap();
371
372 let mapping =
373 ObjectLineMapping::from_object_with_provider(&object, |_| None::<Vec<u8>>).unwrap();
374
375 let mut buf = Vec::new();
376 assert!(!mapping.to_writer(&mut buf).unwrap());
377 }
378}