samply_symbols/
mapped_path.rs

1use nom::branch::alt;
2use nom::bytes::complete::{tag, take_until1};
3use nom::combinator::{eof, map};
4use nom::error::ErrorKind;
5use nom::sequence::terminated;
6use nom::{Err, IResult};
7
8/// A special source file path for source files which are hosted online.
9///
10/// About "special path" strings: Special paths strings are a string serialization
11/// of a mapped path. The format of this string was adopted from the format used
12/// in [Firefox Breakpad .sym files](https://searchfox.org/mozilla-central/rev/4ebfb48f7e82251145afa4a822f970931dd06c68/toolkit/crashreporter/tools/symbolstore.py#199).
13/// This format is also used in the [Tecken symbolication API](https://tecken.readthedocs.io/en/latest/symbolication.html#id9),
14/// and [internally in the Firefox Profiler](https://github.com/firefox-devtools/profiler/blob/fb9ff03ab8b98d9e7c29e36314080fae555bbe78/src/utils/special-paths.js).
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum MappedPath {
17    /// A path to a file in a git repository.
18    Git {
19        /// The web host + root path where the repository is hosted, e.g. `"github.com/rust-lang/rust"`.
20        repo: String,
21        /// The path to this file inside the repository, e.g. `"library/std/src/sys/unix/thread.rs"`.
22        path: String,
23        /// The revision, e.g. `"53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b"`.
24        rev: String,
25    },
26    /// A path to a file in a mercurial repository (hg).
27    Hg {
28        /// The web host + root path where the repository is hosted, e.g. `"hg.mozilla.org/mozilla-central"`.
29        repo: String,
30        /// The path to this file inside the repository, e.g. `"widget/cocoa/nsAppShell.mm"`.
31        path: String,
32        /// The revision, e.g. `"997f00815e6bc28806b75448c8829f0259d2cb28"`.
33        rev: String,
34    },
35    /// A path to a file hosted in an S3 bucket.
36    S3 {
37        /// The name of the S3 bucket, e.g. `"gecko-generated-sources"` (which is hosted at `https://gecko-generated-sources.s3.amazonaws.com/`).
38        bucket: String,
39        /// The "digest" of the file, i.e. a long hash string of hex characters.
40        digest: String,
41        /// The path to this file inside the bucket, e.g. `"ipc/ipdl/PBackgroundChild.cpp"`.
42        path: String,
43    },
44    /// A path to a file in a Rust package which is hosted in a cargo registry (usually on crates.io).
45    Cargo {
46        /// The name of the cargo registry, usually `"github.com-1ecc6299db9ec823"`.
47        registry: String,
48        /// The name of the package, e.g. `"tokio"`.
49        crate_name: String,
50        /// The version of the package, e.g. `"1.6.1"`.
51        version: String,
52        /// The path to this file inside the package, e.g. `"src/runtime/task/mod.rs"`.
53        path: String,
54    },
55}
56
57impl MappedPath {
58    /// Parse a "special path" string. These types of strings are found in Breakpad
59    /// .sym files on the Mozilla symbol server.
60    ///
61    /// So this parsing code basically exists here because this crate supports obtaining
62    /// symbols from Breakpad symbol files, so that consumers don't have parse this
63    /// syntax when looking up symbols from a `SymbolMap` from such a .sym file.
64    pub fn from_special_path_str(special_path: &str) -> Option<Self> {
65        match parse_special_path(special_path) {
66            Ok((_, mapped_path)) => Some(mapped_path),
67            Err(_) => None,
68        }
69    }
70
71    /// Detect some URLs of plain text files and convert them to a `MappedPath`.
72    pub fn from_url(url: &str) -> Option<Self> {
73        match parse_url(url) {
74            Ok((_, mapped_path)) => Some(mapped_path),
75            Err(_) => None,
76        }
77    }
78
79    /// Serialize this mapped path to a string, using the "special path" syntax.
80    pub fn to_special_path_str(&self) -> String {
81        match self {
82            MappedPath::Git { repo, path, rev } => format!("git:{repo}:{path}:{rev}"),
83            MappedPath::Hg { repo, path, rev } => format!("hg:{repo}:{path}:{rev}"),
84            MappedPath::S3 {
85                bucket,
86                digest,
87                path,
88            } => format!("s3:{bucket}:{digest}/{path}:"),
89            MappedPath::Cargo {
90                registry,
91                crate_name,
92                version,
93                path,
94            } => format!("cargo:{registry}:{crate_name}-{version}:{path}"),
95        }
96    }
97
98    /// Create a short, display-friendly form of this path.
99    pub fn display_path(&self) -> String {
100        match self {
101            MappedPath::Git { path, .. } => path.clone(),
102            MappedPath::Hg { path, .. } => path.clone(),
103            MappedPath::S3 { path, .. } => path.clone(),
104            MappedPath::Cargo {
105                crate_name,
106                version,
107                path,
108                ..
109            } => format!("{crate_name}-{version}/{path}"),
110        }
111    }
112}
113
114fn git_path(input: &str) -> IResult<&str, (String, String, String)> {
115    let (input, _) = tag("git:")(input)?;
116    let (input, repo) = terminated(take_until1(":"), tag(":"))(input)?;
117    let (rev, path) = terminated(take_until1(":"), tag(":"))(input)?;
118    Ok(("", (repo.to_owned(), path.to_owned(), rev.to_owned())))
119}
120
121fn hg_path(input: &str) -> IResult<&str, (String, String, String)> {
122    let (input, _) = tag("hg:")(input)?;
123    let (input, repo) = terminated(take_until1(":"), tag(":"))(input)?;
124    let (rev, path) = terminated(take_until1(":"), tag(":"))(input)?;
125    Ok(("", (repo.to_owned(), path.to_owned(), rev.to_owned())))
126}
127
128fn s3_path(input: &str) -> IResult<&str, (String, String, String)> {
129    let (input, _) = tag("s3:")(input)?;
130    let (input, bucket) = terminated(take_until1(":"), tag(":"))(input)?;
131    let (input, digest) = terminated(take_until1("/"), tag("/"))(input)?;
132    let (_, path) = terminated(take_until1(":"), terminated(tag(":"), eof))(input)?;
133    Ok(("", (bucket.to_owned(), digest.to_owned(), path.to_owned())))
134}
135
136fn cargo_path(input: &str) -> IResult<&str, (String, String, String, String)> {
137    let (input, _) = tag("cargo:")(input)?;
138    let (input, registry) = terminated(take_until1(":"), tag(":"))(input)?;
139    let (path, crate_name_and_version) = terminated(take_until1(":"), tag(":"))(input)?;
140    let (crate_name, version) = match crate_name_and_version.rfind('-') {
141        Some(pos) => (
142            &crate_name_and_version[..pos],
143            &crate_name_and_version[(pos + 1)..],
144        ),
145        None => {
146            return Err(Err::Error(nom::error::Error::new(
147                crate_name_and_version,
148                ErrorKind::Digit,
149            )))
150        }
151    };
152    Ok((
153        "",
154        (
155            registry.to_owned(),
156            crate_name.to_owned(),
157            version.to_owned(),
158            path.to_owned(),
159        ),
160    ))
161}
162
163fn parse_special_path(input: &str) -> IResult<&str, MappedPath> {
164    alt((
165        map(git_path, |(repo, path, rev)| MappedPath::Git {
166            repo,
167            path,
168            rev,
169        }),
170        map(hg_path, |(repo, path, rev)| MappedPath::Hg {
171            repo,
172            path,
173            rev,
174        }),
175        map(s3_path, |(bucket, digest, path)| MappedPath::S3 {
176            bucket,
177            digest,
178            path,
179        }),
180        map(cargo_path, |(registry, crate_name, version, path)| {
181            MappedPath::Cargo {
182                registry,
183                crate_name,
184                version,
185                path,
186            }
187        }),
188    ))(input)
189}
190
191fn github_url(input: &str) -> IResult<&str, (String, String, String)> {
192    // Example: "https://raw.githubusercontent.com/baldurk/renderdoc/v1.15/renderdoc/data/glsl/gl_texsample.h"
193    let (input, _) = tag("https://raw.githubusercontent.com/")(input)?;
194    let (input, org) = terminated(take_until1("/"), tag("/"))(input)?;
195    let (input, repo_name) = terminated(take_until1("/"), tag("/"))(input)?;
196    let (input, rev) = terminated(take_until1("/"), tag("/"))(input)?;
197    let path = input;
198    Ok((
199        "",
200        (
201            format!("github.com/{org}/{repo_name}"),
202            path.to_owned(),
203            rev.to_owned(),
204        ),
205    ))
206}
207
208fn hg_url(input: &str) -> IResult<&str, (String, String, String)> {
209    // Example: "https://hg.mozilla.org/mozilla-central/raw-file/1706d4d54ec68fae1280305b70a02cb24c16ff68/mozglue/baseprofiler/core/ProfilerBacktrace.cpp"
210    let (input, _) = tag("https://hg.")(input)?;
211    let (input, host_rest) = terminated(take_until1("/"), tag("/"))(input)?;
212    let (input, repo) = terminated(take_until1("/raw-file/"), tag("/raw-file/"))(input)?;
213    let (input, rev) = terminated(take_until1("/"), tag("/"))(input)?;
214    let path = input;
215    Ok((
216        "",
217        (
218            format!("hg.{host_rest}/{repo}"),
219            path.to_owned(),
220            rev.to_owned(),
221        ),
222    ))
223}
224
225fn s3_url(input: &str) -> IResult<&str, (String, String, String)> {
226    // Example: "https://gecko-generated-sources.s3.amazonaws.com/7a1db5dfd0061d0e0bcca227effb419a20439aef4f6c4e9cd391a9f136c6283e89043d62e63e7edbd63ad81c339c401092bcfeff80f74f9cae8217e072f0c6f3/x86_64-pc-windows-msvc/release/build/swgl-59e3a0e09f56f4ea/out/brush_solid_DEBUG_OVERDRAW.h"
227    let (input, _) = tag("https://")(input)?;
228    let (input, bucket) =
229        terminated(take_until1(".s3.amazonaws.com/"), tag(".s3.amazonaws.com/"))(input)?;
230    let (input, digest) = terminated(take_until1("/"), tag("/"))(input)?;
231    let path = input;
232    Ok(("", (bucket.to_owned(), digest.to_owned(), path.to_owned())))
233}
234
235fn parse_url(input: &str) -> IResult<&str, MappedPath> {
236    alt((
237        map(github_url, |(repo, path, rev)| MappedPath::Git {
238            repo,
239            path,
240            rev,
241        }),
242        map(hg_url, |(repo, path, rev)| MappedPath::Hg {
243            repo,
244            path,
245            rev,
246        }),
247        map(s3_url, |(bucket, digest, path)| MappedPath::S3 {
248            bucket,
249            digest,
250            path,
251        }),
252    ))(input)
253}
254
255#[cfg(test)]
256mod test {
257    use super::*;
258
259    #[test]
260    fn parse_hg_paths() {
261        assert_eq!(
262            MappedPath::from_special_path_str(
263                "hg:hg.mozilla.org/mozilla-central:widget/cocoa/nsAppShell.mm:997f00815e6bc28806b75448c8829f0259d2cb28"
264            ),
265            Some(MappedPath::Hg {
266                repo: "hg.mozilla.org/mozilla-central".to_string(),
267                path: "widget/cocoa/nsAppShell.mm".to_string(),
268                rev: "997f00815e6bc28806b75448c8829f0259d2cb28".to_string(),
269            })
270        );
271        assert_eq!(
272            MappedPath::from_url(
273                "https://hg.mozilla.org/mozilla-central/raw-file/1706d4d54ec68fae1280305b70a02cb24c16ff68/mozglue/baseprofiler/core/ProfilerBacktrace.cpp"
274            ),
275            Some(MappedPath::Hg {
276                repo: "hg.mozilla.org/mozilla-central".to_string(),
277                path: "mozglue/baseprofiler/core/ProfilerBacktrace.cpp".to_string(),
278                rev: "1706d4d54ec68fae1280305b70a02cb24c16ff68".to_string(),
279            })
280        );
281    }
282
283    #[test]
284    fn parse_git_paths() {
285        assert_eq!(
286            MappedPath::from_special_path_str(
287                "git:github.com/rust-lang/rust:library/std/src/sys/unix/thread.rs:53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b"
288            ),
289            Some(MappedPath::Git {
290                repo: "github.com/rust-lang/rust".to_string(),
291                path: "library/std/src/sys/unix/thread.rs".to_string(),
292                rev: "53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b".to_string(),
293            })
294        );
295        assert_eq!(
296            MappedPath::from_special_path_str(
297                "git:chromium.googlesource.com/chromium/src:content/gpu/gpu_main.cc:4dac2548d4812df2aa4a90ac1fc8912363f4d59c"
298            ),
299            Some(MappedPath::Git {
300                repo: "chromium.googlesource.com/chromium/src".to_string(),
301                path: "content/gpu/gpu_main.cc".to_string(),
302                rev: "4dac2548d4812df2aa4a90ac1fc8912363f4d59c".to_string(),
303            })
304        );
305        assert_eq!(
306            MappedPath::from_special_path_str(
307                "git:pdfium.googlesource.com/pdfium:core/fdrm/fx_crypt.cpp:dab1161c861cc239e48a17e1a5d729aa12785a53"
308            ),
309            Some(MappedPath::Git {
310                repo: "pdfium.googlesource.com/pdfium".to_string(),
311                path: "core/fdrm/fx_crypt.cpp".to_string(),
312                rev: "dab1161c861cc239e48a17e1a5d729aa12785a53".to_string(),
313            })
314        );
315        assert_eq!(
316            MappedPath::from_url(
317                "https://raw.githubusercontent.com/baldurk/renderdoc/v1.15/renderdoc/data/glsl/gl_texsample.h"
318            ),
319            Some(MappedPath::Git {
320                repo: "github.com/baldurk/renderdoc".to_string(),
321                path: "renderdoc/data/glsl/gl_texsample.h".to_string(),
322                rev: "v1.15".to_string(),
323            })
324        );
325    }
326
327    #[test]
328    fn parse_s3_paths() {
329        assert_eq!(
330            MappedPath::from_special_path_str(
331                "s3:gecko-generated-sources:a5d3747707d6877b0e5cb0a364e3cb9fea8aa4feb6ead138952c2ba46d41045297286385f0e0470146f49403e46bd266e654dfca986de48c230f3a71c2aafed4/ipc/ipdl/PBackgroundChild.cpp:"
332            ),
333            Some(MappedPath::S3 {
334                bucket: "gecko-generated-sources".to_string(),
335                path: "ipc/ipdl/PBackgroundChild.cpp".to_string(),
336                digest:
337                "a5d3747707d6877b0e5cb0a364e3cb9fea8aa4feb6ead138952c2ba46d41045297286385f0e0470146f49403e46bd266e654dfca986de48c230f3a71c2aafed4".to_string(),
338            })
339        );
340        assert_eq!(
341            MappedPath::from_special_path_str(
342                "s3:gecko-generated-sources:4fd754dd7ca7565035aaa3357b8cd99959a2dddceba0fc2f7018ef99fd78ea63d03f9bf928afdc29873089ee15431956791130b97f66ab8fcb88ec75f4ba6b04/aarch64-apple-darwin/release/build/swgl-580c7d646d09cf59/out/ps_text_run_ALPHA_PASS_TEXTURE_2D.h:"
343            ),
344            Some(MappedPath::S3 {
345                bucket: "gecko-generated-sources".to_string(),
346                path: "aarch64-apple-darwin/release/build/swgl-580c7d646d09cf59/out/ps_text_run_ALPHA_PASS_TEXTURE_2D.h".to_string(),
347                digest: "4fd754dd7ca7565035aaa3357b8cd99959a2dddceba0fc2f7018ef99fd78ea63d03f9bf928afdc29873089ee15431956791130b97f66ab8fcb88ec75f4ba6b04".to_string(),
348            })
349        );
350        assert_eq!(
351            MappedPath::from_url(
352                "https://gecko-generated-sources.s3.amazonaws.com/7a1db5dfd0061d0e0bcca227effb419a20439aef4f6c4e9cd391a9f136c6283e89043d62e63e7edbd63ad81c339c401092bcfeff80f74f9cae8217e072f0c6f3/x86_64-pc-windows-msvc/release/build/swgl-59e3a0e09f56f4ea/out/brush_solid_DEBUG_OVERDRAW.h"
353            ),
354            Some(MappedPath::S3 {
355                bucket: "gecko-generated-sources".to_string(),
356                path: "x86_64-pc-windows-msvc/release/build/swgl-59e3a0e09f56f4ea/out/brush_solid_DEBUG_OVERDRAW.h".to_string(),
357                digest: "7a1db5dfd0061d0e0bcca227effb419a20439aef4f6c4e9cd391a9f136c6283e89043d62e63e7edbd63ad81c339c401092bcfeff80f74f9cae8217e072f0c6f3".to_string(),
358            })
359        );
360    }
361
362    #[test]
363    fn parse_cargo_paths() {
364        assert_eq!(
365            MappedPath::from_special_path_str(
366                "cargo:github.com-1ecc6299db9ec823:addr2line-0.16.0:src/function.rs"
367            ),
368            Some(MappedPath::Cargo {
369                registry: "github.com-1ecc6299db9ec823".to_string(),
370                crate_name: "addr2line".to_string(),
371                version: "0.16.0".to_string(),
372                path: "src/function.rs".to_string(),
373            })
374        );
375        assert_eq!(
376            MappedPath::from_special_path_str(
377                "cargo:github.com-1ecc6299db9ec823:tokio-1.6.1:src/runtime/task/mod.rs"
378            ),
379            Some(MappedPath::Cargo {
380                registry: "github.com-1ecc6299db9ec823".to_string(),
381                crate_name: "tokio".to_string(),
382                version: "1.6.1".to_string(),
383                path: "src/runtime/task/mod.rs".to_string(),
384            })
385        );
386        assert_eq!(
387            MappedPath::from_special_path_str(
388                "cargo:github.com-1ecc6299db9ec823:fxprof-processed-profile-0.3.0:src/lib.rs"
389            ),
390            Some(MappedPath::Cargo {
391                registry: "github.com-1ecc6299db9ec823".to_string(),
392                crate_name: "fxprof-processed-profile".to_string(),
393                version: "0.3.0".to_string(),
394                path: "src/lib.rs".to_string(),
395            })
396        );
397    }
398
399    fn test_roundtrip(s: &str) {
400        let mapped_path = MappedPath::from_special_path_str(s).unwrap();
401        let roundtripped = mapped_path.to_special_path_str();
402        assert_eq!(&roundtripped, s);
403    }
404
405    #[test]
406    fn roundtrips() {
407        test_roundtrip("hg:hg.mozilla.org/mozilla-central:widget/cocoa/nsAppShell.mm:997f00815e6bc28806b75448c8829f0259d2cb28");
408        test_roundtrip("git:github.com/rust-lang/rust:library/std/src/sys/unix/thread.rs:53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b");
409        test_roundtrip("git:chromium.googlesource.com/chromium/src:content/gpu/gpu_main.cc:4dac2548d4812df2aa4a90ac1fc8912363f4d59c");
410        test_roundtrip("git:pdfium.googlesource.com/pdfium:core/fdrm/fx_crypt.cpp:dab1161c861cc239e48a17e1a5d729aa12785a53");
411        test_roundtrip("s3:gecko-generated-sources:a5d3747707d6877b0e5cb0a364e3cb9fea8aa4feb6ead138952c2ba46d41045297286385f0e0470146f49403e46bd266e654dfca986de48c230f3a71c2aafed4/ipc/ipdl/PBackgroundChild.cpp:");
412        test_roundtrip("s3:gecko-generated-sources:4fd754dd7ca7565035aaa3357b8cd99959a2dddceba0fc2f7018ef99fd78ea63d03f9bf928afdc29873089ee15431956791130b97f66ab8fcb88ec75f4ba6b04/aarch64-apple-darwin/release/build/swgl-580c7d646d09cf59/out/ps_text_run_ALPHA_PASS_TEXTURE_2D.h:");
413        test_roundtrip("cargo:github.com-1ecc6299db9ec823:addr2line-0.16.0:src/function.rs");
414        test_roundtrip("cargo:github.com-1ecc6299db9ec823:tokio-1.6.1:src/runtime/task/mod.rs");
415        test_roundtrip(
416            "cargo:github.com-1ecc6299db9ec823:fxprof-processed-profile-0.3.0:src/lib.rs",
417        );
418    }
419}