symbolic_il2cpp/line_mapping/
mod.rs

1mod from_object;
2
3use indexmap::IndexSet;
4use std::collections::HashMap;
5
6pub use from_object::ObjectLineMapping;
7
8/// An internal line mapping.
9#[derive(Debug)]
10struct LineEntry {
11    /// The C++ line that is being mapped.
12    cpp_line: u32,
13    /// The C# line it corresponds to.
14    cs_line: u32,
15    /// The index into the `cs_files` [`IndexSet`] below for the corresponding C# file.
16    cs_file_idx: usize,
17}
18
19/// A parsed Il2Cpp/Unity Line mapping JSON.
20#[derive(Debug, Default)]
21pub struct LineMapping {
22    /// The set of C# files.
23    cs_files: IndexSet<String>,
24    /// A map of C++ filename to a list of Mappings.
25    cpp_file_map: HashMap<String, Vec<LineEntry>>,
26}
27
28impl LineMapping {
29    /// Parses a JSON buffer into a valid [`LineMapping`].
30    ///
31    /// Returns [`None`] if the JSON was not a valid mapping.
32    pub fn parse(data: &[u8]) -> Option<Self> {
33        let json: serde_json::Value = serde_json::from_slice(data).ok()?;
34        let mut result = Self::default();
35
36        if let serde_json::Value::Object(object) = json {
37            for (cpp_file, file_map) in object {
38                // This is a sentinel value for the originating debug file, which
39                // `ObjectLineMapping::to_writer` writes to the file to make it unique
40                // (and dependent on the originating debug-id).
41                if cpp_file == "__debug-id__" {
42                    continue;
43                }
44                let mut lines = Vec::new();
45                if let serde_json::Value::Object(file_map) = file_map {
46                    for (cs_file, line_map) in file_map {
47                        if let serde_json::Value::Object(line_map) = line_map {
48                            let cs_file_idx = result.cs_files.insert_full(cs_file).0;
49                            for (from, to) in line_map {
50                                let cpp_line = from.parse().ok()?;
51                                let cs_line = to.as_u64().and_then(|n| n.try_into().ok())?;
52                                lines.push(LineEntry {
53                                    cpp_line,
54                                    cs_line,
55                                    cs_file_idx,
56                                });
57                            }
58                        }
59                    }
60                }
61                lines.sort_by_key(|entry| entry.cpp_line);
62                result.cpp_file_map.insert(cpp_file, lines);
63            }
64        }
65
66        Some(result)
67    }
68
69    /// Looks up the corresponding C# file/line for a given C++ file/line.
70    ///
71    /// As these mappings are not exact, this will return an exact match, or a mapping "close-by".
72    pub fn lookup(&self, file: &str, line: u32) -> Option<(&str, u32)> {
73        let lines = self.cpp_file_map.get(file)?;
74
75        let idx = match lines.binary_search_by_key(&line, |entry| entry.cpp_line) {
76            Ok(idx) => idx,
77            Err(0) => return None,
78            Err(idx) => idx - 1,
79        };
80
81        let entry = lines.get(idx)?;
82
83        // We will return mappings at most 50 lines away from the source line they refer to.
84        if line.saturating_sub(entry.cpp_line) > 50 {
85            None
86        } else {
87            Some((self.cs_files.get_index(entry.cs_file_idx)?, entry.cs_line))
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    fn source_to_mapping(cpp_source: &[u8]) -> LineMapping {
97        let mapping =
98            HashMap::from([("main.cpp", ObjectLineMapping::parse_source_file(cpp_source))]);
99        let mapping_json = serde_json::to_string(&mapping).unwrap();
100
101        LineMapping::parse(mapping_json.as_bytes()).unwrap()
102    }
103
104    #[test]
105    fn test_lookup() {
106        let parsed_mapping = source_to_mapping(
107            b"Lorem ipsum dolor sit amet
108            //<source_info:main.cs:17>
109            // some
110            // comments
111            some expression // 5
112            stretching
113            over
114            multiple lines
115
116            // blank lines
117
118            // and stuff
119            // 13
120            //<source_info:main.cs:29>
121            actual source code // 15
122        ",
123        );
124        assert_eq!(parsed_mapping.lookup("main.cpp", 0), None);
125        assert_eq!(parsed_mapping.lookup("main.cpp", 1), None);
126        assert_eq!(parsed_mapping.lookup("main.cpp", 2), None);
127        assert_eq!(parsed_mapping.lookup("main.cpp", 3), None);
128        assert_eq!(parsed_mapping.lookup("main.cpp", 4), None);
129        assert_eq!(parsed_mapping.lookup("main.cpp", 5), Some(("main.cs", 17)));
130        assert_eq!(parsed_mapping.lookup("main.cpp", 6), Some(("main.cs", 17)));
131        assert_eq!(parsed_mapping.lookup("main.cpp", 7), Some(("main.cs", 17)));
132        assert_eq!(parsed_mapping.lookup("main.cpp", 8), Some(("main.cs", 17)));
133        assert_eq!(parsed_mapping.lookup("main.cpp", 9), Some(("main.cs", 17)));
134        assert_eq!(parsed_mapping.lookup("main.cpp", 10), Some(("main.cs", 17)));
135        assert_eq!(parsed_mapping.lookup("main.cpp", 11), Some(("main.cs", 17)));
136        assert_eq!(parsed_mapping.lookup("main.cpp", 12), Some(("main.cs", 17)));
137        assert_eq!(parsed_mapping.lookup("main.cpp", 13), Some(("main.cs", 17)));
138        assert_eq!(parsed_mapping.lookup("main.cpp", 14), Some(("main.cs", 17)));
139        assert_eq!(parsed_mapping.lookup("main.cpp", 15), Some(("main.cs", 29)));
140        assert_eq!(parsed_mapping.lookup("main.cpp", 16), Some(("main.cs", 29)));
141    }
142
143    #[test]
144    fn test_lookup_real_code() {
145        // This is a special format where we've manually added the expected line number info at
146        // the end of each line to make the test easier to read (and write, too).
147        let cpp_source = r#"
148IL_001f:                                                                        |
149    {                                                                           |
150        il2cpp_codegen_runtime_class_init_inline(SentrySdk_t74D2EF9D77AF1E      |
151        SentrySdk_ConfigureScope_m365F1871733F0C48E4B585067AC55DD27E654BC4      |
152        //<source_info:/Scripts/AdditionalButtons.cs:20>                        |
153        // Debug.Log("User set: ant");                                          |
154        il2cpp_codegen_runtime_class_init_inline(Debug_t8394C7EEAECA3689C2      | /Scripts/AdditionalButtons.cs:20
155        Debug_Log_m87A9A3C761FF5C43ED8A53B16190A53D08F818BB(_stringLiteral      | /Scripts/AdditionalButtons.cs:20
156        //<source_info:/Scripts/AdditionalButtons.cs:21>                        | /Scripts/AdditionalButtons.cs:20
157        // }                                                                    | /Scripts/AdditionalButtons.cs:20
158        return;                                                                 | /Scripts/AdditionalButtons.cs:21
159    }                                                                           | /Scripts/AdditionalButtons.cs:21
160}                                                                               | /Scripts/AdditionalButtons.cs:21
161// System.Void AdditionalButtons::CaptureMessageWithContext()                   | /Scripts/AdditionalButtons.cs:21
162IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void AdditionalButtons_CaptureMessageWi      | /Scripts/AdditionalButtons.cs:21
163{                                                                               | /Scripts/AdditionalButtons.cs:21
164    static bool s_Il2CppMethodInitialized;                                      | /Scripts/AdditionalButtons.cs:21
165    if (!s_Il2CppMethodInitialized)                                             | /Scripts/AdditionalButtons.cs:21
166    {                                                                           | /Scripts/AdditionalButtons.cs:21
167        il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&Action_1_t      | /Scripts/AdditionalButtons.cs:21
168        il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&SentrySdk_      | /Scripts/AdditionalButtons.cs:21
169        il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&U3CU3Ec_U3      | /Scripts/AdditionalButtons.cs:21
170        il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&U3CU3Ec_U3      | /Scripts/AdditionalButtons.cs:21
171        il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&U3CU3Ec_t7      | /Scripts/AdditionalButtons.cs:21
172        il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&_stringLit      | /Scripts/AdditionalButtons.cs:21
173        s_Il2CppMethodInitialized = true;                                       | /Scripts/AdditionalButtons.cs:21
174    }                                                                           | /Scripts/AdditionalButtons.cs:21
175    Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* G_B2_0 = NULL;          | /Scripts/AdditionalButtons.cs:21
176    Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* G_B1_0 = NULL;          | /Scripts/AdditionalButtons.cs:21
177    Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* G_B4_0 = NULL;          | /Scripts/AdditionalButtons.cs:21
178    Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* G_B3_0 = NULL;          | /Scripts/AdditionalButtons.cs:21
179    {                                                                           | /Scripts/AdditionalButtons.cs:21
180        //<source_info:/Scripts/AdditionalButtons.cs:32>                        | /Scripts/AdditionalButtons.cs:21
181        // SentrySdk.ConfigureScope(scope =>                                    | /Scripts/AdditionalButtons.cs:32
182        //<source_info:/Scripts/AdditionalButtons.cs:33>                        | /Scripts/AdditionalButtons.cs:32
183        // {                                                                    | /Scripts/AdditionalButtons.cs:33
184        //<source_info:/Scripts/AdditionalButtons.cs:34>                        | /Scripts/AdditionalButtons.cs:33
185        //     scope.Contexts["character"] = new PlayerCharacter                | /Scripts/AdditionalButtons.cs:34
186        //<source_info:/Scripts/AdditionalButtons.cs:35>                        | /Scripts/AdditionalButtons.cs:34
187        //     {                                                                | /Scripts/AdditionalButtons.cs:35
188        //<source_info:/Scripts/AdditionalButtons.cs:36>                        | /Scripts/AdditionalButtons.cs:35
189        //         Name = "Mighty Fighter",                                     | /Scripts/AdditionalButtons.cs:36
190        //<source_info:/Scripts/AdditionalButtons.cs:37>                        | /Scripts/AdditionalButtons.cs:36
191        //         Age = 19,                                                    | /Scripts/AdditionalButtons.cs:37
192        //<source_info:/Scripts/AdditionalButtons.cs:38>                        | /Scripts/AdditionalButtons.cs:37
193        //         AttackType = "melee"                                         | /Scripts/AdditionalButtons.cs:38
194        //<source_info:/Scripts/AdditionalButtons.cs:39>                        | /Scripts/AdditionalButtons.cs:38
195        //     };                                                               | /Scripts/AdditionalButtons.cs:39
196        //<source_info:/Scripts/AdditionalButtons.cs:40>                        | /Scripts/AdditionalButtons.cs:39
197        // });                                                                  | /Scripts/AdditionalButtons.cs:39
198        il2cpp_codegen_runtime_class_init_inline(U3CU3Ec_t79DD2293FFBDE9C9      | /Scripts/AdditionalButtons.cs:40
199        Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* L_0 = ((U3CU3E      | /Scripts/AdditionalButtons.cs:40
200        Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* L_1 = L_0;          | /Scripts/AdditionalButtons.cs:40
201        G_B1_0 = L_1;                                                           | /Scripts/AdditionalButtons.cs:40
202        if (L_1)                                                                | /Scripts/AdditionalButtons.cs:40
203        {                                                                       | /Scripts/AdditionalButtons.cs:40
204            G_B2_0 = L_1;                                                       | /Scripts/AdditionalButtons.cs:40
205            goto IL_001f;                                                       | /Scripts/AdditionalButtons.cs:40
206        }                                                                       | /Scripts/AdditionalButtons.cs:40
207    }                                                                           | /Scripts/AdditionalButtons.cs:40
208    {                                                                           | /Scripts/AdditionalButtons.cs:40
209        il2cpp_codegen_runtime_class_init_inline(U3CU3Ec_t79DD2293FFBDE9C9      | /Scripts/AdditionalButtons.cs:40
210        U3CU3Ec_t79DD2293FFBDE9C9C239DB7D77EA41E31CFCA564* L_2 = ((U3CU3Ec      | /Scripts/AdditionalButtons.cs:40
211        Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* L_3 = (Action_      | /Scripts/AdditionalButtons.cs:40
212        NullCheck(L_3);                                                         | /Scripts/AdditionalButtons.cs:40
213        Action_1__ctor_mCE58979C800E29B3B4B1673D24BDF60161B4A942(L_3, L_2,      | /Scripts/AdditionalButtons.cs:40
214        Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* L_4 = L_3;          | /Scripts/AdditionalButtons.cs:40
215        ((U3CU3Ec_t79DD2293FFBDE9C9C239DB7D77EA41E31CFCA564_StaticFields*)      | /Scripts/AdditionalButtons.cs:40
216        Il2CppCodeGenWriteBarrier((void**)(&((U3CU3Ec_t79DD2293FFBDE9C9C23      | /Scripts/AdditionalButtons.cs:40
217        G_B2_0 = L_4;                                                           | /Scripts/AdditionalButtons.cs:40
218    }                                                                           | /Scripts/AdditionalButtons.cs:40
219                                                                                | /Scripts/AdditionalButtons.cs:40
220IL_001f:                                                                        | /Scripts/AdditionalButtons.cs:40
221    {                                                                           | /Scripts/AdditionalButtons.cs:40
222        il2cpp_codegen_runtime_class_init_inline(SentrySdk_t74D2EF9D77AF1E      | /Scripts/AdditionalButtons.cs:40
223        SentrySdk_ConfigureScope_m365F1871733F0C48E4B585067AC55DD27E654BC4      | /Scripts/AdditionalButtons.cs:40
224        //<source_info:/Scripts/AdditionalButtons.cs:42>                        | /Scripts/AdditionalButtons.cs:40
225        // SentrySdk.CaptureMessage("Capturing with player character conte      | /Scripts/AdditionalButtons.cs:40
226        SentryId_t0035EA63F72CBEBE7C342CD0C3A1D80C91A77940 L_5;                 | /Scripts/AdditionalButtons.cs:42
227        L_5 = SentrySdk_CaptureMessage_mEB25F3DA889DE8DC87DB25178C01ADE78F      | /Scripts/AdditionalButtons.cs:42
228        //<source_info:/Scripts/AdditionalButtons.cs:43>                        | /Scripts/AdditionalButtons.cs:42
229        // SentrySdk.ConfigureScope(scope => scope.Contexts = null);            | /Scripts/AdditionalButtons.cs:42
230        il2cpp_codegen_runtime_class_init_inline(U3CU3Ec_t79DD2293FFBDE9C9      | /Scripts/AdditionalButtons.cs:43
231        Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* L_6 = ((U3CU3E      | /Scripts/AdditionalButtons.cs:43
232        Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* L_7 = L_6;          | /Scripts/AdditionalButtons.cs:43
233        G_B3_0 = L_7;                                                           | /Scripts/AdditionalButtons.cs:43
234        if (L_7)                                                                | /Scripts/AdditionalButtons.cs:43
235        {                                                                       | /Scripts/AdditionalButtons.cs:43
236            G_B4_0 = L_7;                                                       | /Scripts/AdditionalButtons.cs:43
237            goto IL_004f;                                                       | /Scripts/AdditionalButtons.cs:43
238        }                                                                       | /Scripts/AdditionalButtons.cs:43
239    }                                                                           | /Scripts/AdditionalButtons.cs:43
240    {                                                                           | /Scripts/AdditionalButtons.cs:43
241        il2cpp_codegen_runtime_class_init_inline(U3CU3Ec_t79DD2293FFBDE9C9      | /Scripts/AdditionalButtons.cs:43
242        U3CU3Ec_t79DD2293FFBDE9C9C239DB7D77EA41E31CFCA564* L_8 = ((U3CU3Ec      | /Scripts/AdditionalButtons.cs:43
243        Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* L_9 = (Action_      | /Scripts/AdditionalButtons.cs:43
244        NullCheck(L_9);                                                         | /Scripts/AdditionalButtons.cs:43
245        Action_1__ctor_mCE58979C800E29B3B4B1673D24BDF60161B4A942(L_9, L_8,      | /Scripts/AdditionalButtons.cs:43
246        Action_1_t1C1C5545B1F9348689F2EE7364FC9ACC425526EC* L_10 = L_9;         | /Scripts/AdditionalButtons.cs:43
247        ((U3CU3Ec_t79DD2293FFBDE9C9C239DB7D77EA41E31CFCA564_StaticFields*)      | /Scripts/AdditionalButtons.cs:43
248        Il2CppCodeGenWriteBarrier((void**)(&((U3CU3Ec_t79DD2293FFBDE9C9C23      | /Scripts/AdditionalButtons.cs:43
249        G_B4_0 = L_10;                                                          | /Scripts/AdditionalButtons.cs:43
250    }                                                                           | /Scripts/AdditionalButtons.cs:43
251                                                                                | /Scripts/AdditionalButtons.cs:43
252IL_004f:                                                                        | /Scripts/AdditionalButtons.cs:43
253    {                                                                           | /Scripts/AdditionalButtons.cs:43
254        il2cpp_codegen_runtime_class_init_inline(SentrySdk_t74D2EF9D77AF1E      | /Scripts/AdditionalButtons.cs:43
255        SentrySdk_ConfigureScope_m365F1871733F0C48E4B585067AC55DD27E654BC4      | /Scripts/AdditionalButtons.cs:43
256        //<source_info:/Scripts/AdditionalButtons.cs:44>                        | /Scripts/AdditionalButtons.cs:43
257        // }                                                                    | /Scripts/AdditionalButtons.cs:43
258        return;                                                                 | /Scripts/AdditionalButtons.cs:44
259    }                                                                           | /Scripts/AdditionalButtons.cs:44
260}                                                                               | /Scripts/AdditionalButtons.cs:44
261        "#.trim();
262        let mut cpp_lines = Vec::new();
263        let mut cs_lines = Vec::new();
264        for (cpp_code, cs_info) in cpp_source.lines().map(|l| l.split_once('|').unwrap()) {
265            cpp_lines.push(cpp_code.trim_end());
266            cs_lines.push(
267                cs_info
268                    .split_once(':')
269                    .map(|(file, line)| (file.trim(), line.parse::<u32>().unwrap())),
270            );
271        }
272        let parsed_mapping = source_to_mapping(cpp_lines.join("\n").as_bytes());
273
274        for (i, expected) in cs_lines.iter().enumerate() {
275            let line_nr = i as u32 + 1;
276            println!("Checking line {: >3}: {}", line_nr, cpp_lines[i]);
277            assert_eq!(parsed_mapping.lookup("main.cpp", line_nr), *expected);
278        }
279    }
280}