symbolic_common/
sourcelinks.rs

1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3
4/// A pattern for matching source paths.
5///
6/// A pattern either matches a string exactly (`Exact`)
7/// or it matches any string starting with a certain prefix (`Prefix`).
8///
9/// Patterns are ordered as follows:
10/// 1. Exact patterns come before prefixes
11/// 2. Exact patterns are ordered lexicographically
12/// 3. Prefix patterns are ordered inversely by length, i.e.,
13///    longer before shorter, and lexicographically among equally long strings.
14#[derive(Debug, Clone, PartialEq, Eq)]
15enum Pattern {
16    Exact(String),
17    Prefix(String),
18}
19
20impl Pattern {
21    fn parse(input: &str) -> Self {
22        if let Some(prefix) = input.strip_suffix('*') {
23            Pattern::Prefix(prefix.to_lowercase())
24        } else {
25            Pattern::Exact(input.to_lowercase())
26        }
27    }
28}
29
30impl Ord for Pattern {
31    fn cmp(&self, other: &Self) -> Ordering {
32        match (self, other) {
33            (Pattern::Exact(s), Pattern::Exact(t)) => s.cmp(t),
34            (Pattern::Exact(_), Pattern::Prefix(_)) => Ordering::Less,
35            (Pattern::Prefix(_), Pattern::Exact(_)) => Ordering::Greater,
36            (Pattern::Prefix(s), Pattern::Prefix(t)) => match s.len().cmp(&t.len()) {
37                Ordering::Greater => Ordering::Less,
38                Ordering::Equal => s.cmp(t),
39                Ordering::Less => Ordering::Greater,
40            },
41        }
42    }
43}
44
45impl PartialOrd for Pattern {
46    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
47        Some(self.cmp(other))
48    }
49}
50
51/// A structure mapping source file paths to remote locations.
52///
53/// # Example
54/// ```
55/// use symbolic_common::SourceLinkMappings;
56/// let mappings = vec![
57///     ("C:\\src\\*", "http://MyDefaultDomain.com/src/*"),
58///     ("C:\\src\\fOO\\*", "http://MyFooDomain.com/src/*"),
59///     ("C:\\src\\foo\\specific.txt", "http://MySpecificFoodDomain.com/src/specific.txt"),
60///     ("C:\\src\\bar\\*", "http://MyBarDomain.com/src/*"),
61/// ];
62/// let mappings = SourceLinkMappings::new(mappings.into_iter());
63/// let resolved = mappings.resolve("c:\\src\\bAr\\foo\\FiLe.txt").unwrap();
64/// assert_eq!(resolved, "http://MyBarDomain.com/src/foo/FiLe.txt");
65/// ````
66#[derive(Debug, Default, Clone, PartialEq, Eq)]
67pub struct SourceLinkMappings {
68    mappings: BTreeMap<Pattern, String>,
69}
70
71impl<'a> Extend<(&'a str, &'a str)> for SourceLinkMappings {
72    fn extend<T: IntoIterator<Item = (&'a str, &'a str)>>(&mut self, iter: T) {
73        self.mappings.extend(
74            iter.into_iter()
75                .map(|(k, v)| (Pattern::parse(k), v.to_string())),
76        )
77    }
78}
79
80impl SourceLinkMappings {
81    /// Creates a `SourceLinkMappings` struct from an iterator of pattern/target pairs.
82    pub fn new<'a, I: IntoIterator<Item = (&'a str, &'a str)>>(iter: I) -> Self {
83        let mut res = Self::default();
84        res.extend(iter);
85        res
86    }
87    /// Returns true if this structure contains no mappings.
88    pub fn is_empty(&self) -> bool {
89        self.mappings.is_empty()
90    }
91
92    /// Resolve the path to a URL.
93    pub fn resolve(&self, path: &str) -> Option<String> {
94        // Note: this is currently quite simple, just pick the first match. If we needed to improve
95        // performance in the future because we encounter PDBs with too many items, we can do a
96        // prefix binary search, for example.
97        let path_lower = path.to_lowercase();
98        for (pattern, target) in &self.mappings {
99            match &pattern {
100                Pattern::Exact(value) => {
101                    if value == &path_lower {
102                        return Some(target.clone());
103                    }
104                }
105                Pattern::Prefix(value) => {
106                    if path_lower.starts_with(value) {
107                        let replacement = path
108                            .get(value.len()..)
109                            .unwrap_or_default()
110                            .replace('\\', "/");
111                        return Some(target.replace('*', &replacement));
112                    }
113                }
114            }
115        }
116        None
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_mapping() {
126        let mappings = vec![
127            ("C:\\src\\*", "http://MyDefaultDomain.com/src/*"),
128            ("C:\\src\\fOO\\*", "http://MyFooDomain.com/src/*"),
129            (
130                "C:\\src\\foo\\specific.txt",
131                "http://MySpecificFoodDomain.com/src/specific.txt",
132            ),
133            ("C:\\src\\bar\\*", "http://MyBarDomain.com/src/*"),
134            ("C:\\src\\file.txt", "https://example.com/file.txt"),
135            ("/home/user/src/*", "https://linux.com/*"),
136        ];
137
138        let mappings = SourceLinkMappings::new(mappings);
139
140        assert_eq!(mappings.mappings.len(), 6);
141
142        // In this example:
143        //   All files under directory bar will map to a relative URL beginning with http://MyBarDomain.com/src/.
144        //   All files under directory foo will map to a relative URL beginning with http://MyFooDomain.com/src/ EXCEPT foo/specific.txt which will map to http://MySpecificFoodDomain.com/src/specific.txt.
145        //   All other files anywhere under the src directory will map to a relative url beginning with http://MyDefaultDomain.com/src/.
146        assert!(mappings.resolve("c:\\other\\path").is_none());
147        assert!(mappings.resolve("/home/path").is_none());
148        assert_eq!(
149            mappings.resolve("c:\\src\\bAr\\foo\\FiLe.txt").unwrap(),
150            "http://MyBarDomain.com/src/foo/FiLe.txt"
151        );
152        assert_eq!(
153            mappings.resolve("c:\\src\\foo\\FiLe.txt").unwrap(),
154            "http://MyFooDomain.com/src/FiLe.txt"
155        );
156        assert_eq!(
157            mappings.resolve("c:\\src\\foo\\SpEcIfIc.txt").unwrap(),
158            "http://MySpecificFoodDomain.com/src/specific.txt"
159        );
160        assert_eq!(
161            mappings.resolve("c:\\src\\other\\path").unwrap(),
162            "http://MyDefaultDomain.com/src/other/path"
163        );
164        assert_eq!(
165            mappings.resolve("c:\\src\\other\\path").unwrap(),
166            "http://MyDefaultDomain.com/src/other/path"
167        );
168        assert_eq!(
169            mappings.resolve("/home/user/src/Path/TO/file.txt").unwrap(),
170            "https://linux.com/Path/TO/file.txt"
171        );
172    }
173}