1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
mod from_object;

use indexmap::IndexSet;
use std::collections::HashMap;

pub use from_object::ObjectLineMapping;

/// An internal line mapping.
#[derive(Debug)]
struct LineEntry {
    /// The C++ line that is being mapped.
    cpp_line: u32,
    /// The C# line it corresponds to.
    cs_line: u32,
    /// The index into the `cs_files` [`IndexSet`] below for the corresponding C# file.
    cs_file_idx: usize,
}

/// A parsed Il2Cpp/Unity Line mapping JSON.
#[derive(Debug, Default)]
pub struct LineMapping {
    /// The set of C# files.
    cs_files: IndexSet<String>,
    /// A map of C++ filename to a list of Mappings.
    cpp_file_map: HashMap<String, Vec<LineEntry>>,
}

impl LineMapping {
    /// Parses a JSON buffer into a valid [`LineMapping`].
    ///
    /// Returns [`None`] if the JSON was not a valid mapping.
    pub fn parse(data: &[u8]) -> Option<Self> {
        let json: serde_json::Value = serde_json::from_slice(data).ok()?;
        let mut result = Self::default();

        if let serde_json::Value::Object(object) = json {
            for (cpp_file, file_map) in object {
                // This is a sentinel value for the originating debug file, which
                // `ObjectLineMapping::to_writer` writes to the file to make it unique
                // (and dependent on the originating debug-id).
                if cpp_file == "__debug-id__" {
                    continue;
                }
                let mut lines = Vec::new();
                if let serde_json::Value::Object(file_map) = file_map {
                    for (cs_file, line_map) in file_map {
                        if let serde_json::Value::Object(line_map) = line_map {
                            let cs_file_idx = result.cs_files.insert_full(cs_file).0;
                            for (from, to) in line_map {
                                let cpp_line = from.parse().ok()?;
                                let cs_line = to.as_u64().and_then(|n| n.try_into().ok())?;
                                lines.push(LineEntry {
                                    cpp_line,
                                    cs_line,
                                    cs_file_idx,
                                });
                            }
                        }
                    }
                }
                lines.sort_by_key(|entry| entry.cpp_line);
                result.cpp_file_map.insert(cpp_file, lines);
            }
        }

        Some(result)
    }

    /// Looks up the corresponding C# file/line for a given C++ file/line.
    ///
    /// As these mappings are not exact, this will return an exact match, or a mapping "close-by".
    pub fn lookup(&self, file: &str, line: u32) -> Option<(&str, u32)> {
        let lines = self.cpp_file_map.get(file)?;

        let idx = match lines.binary_search_by_key(&line, |entry| entry.cpp_line) {
            Ok(idx) => idx,
            Err(0) => return None,
            Err(idx) => idx - 1,
        };

        let LineEntry {
            cs_line,
            cs_file_idx,
            cpp_line,
        } = lines.get(idx)?;

        // We will return mappings at most 5 lines away from the source line they refer to.
        if line.saturating_sub(*cpp_line) > 5 {
            return None;
        }

        Some((self.cs_files.get_index(*cs_file_idx)?, *cs_line))
    }
}

#[cfg(test)]
mod tests {
    use super::from_object::SourceInfos;
    use super::*;

    #[test]
    fn test_lookup() {
        // well, we can either use a pre-made json, or create one ourselves:
        let cpp_source = b"Lorem ipsum dolor sit amet
            //<source_info:main.cs:17>
            // some
            // comments
            some expression // 5
            stretching
            over
            multiple lines

            // blank lines

            // and stuff
            // 13
            //<source_info:main.cs:29>
            actual source code // 15
        ";

        let line_mappings: HashMap<_, _> = SourceInfos::new(cpp_source)
            .map(|si| (si.cpp_line, si.cs_line))
            .collect();

        let mapping = HashMap::from([("main.cpp", HashMap::from([("main.cs", line_mappings)]))]);
        let mapping_json = serde_json::to_string(&mapping).unwrap();

        let parsed_mapping = LineMapping::parse(mapping_json.as_bytes()).unwrap();

        assert_eq!(parsed_mapping.lookup("main.cpp", 2), None);
        assert_eq!(parsed_mapping.lookup("main.cpp", 5), Some(("main.cs", 17)));
        assert_eq!(parsed_mapping.lookup("main.cpp", 12), None);
        assert_eq!(parsed_mapping.lookup("main.cpp", 15), Some(("main.cs", 29)));
    }
}