Repository: getsentry/symbolic (rust)

Issue:
Support DWARFv5 source code embedding extension
<!--
Please provide some context on what problem you are trying to solve and how this feature is going to help.

-->

DWARFv5 has an extension for embedding program source code directly into the debug info sections: https://dwarfstd.org/issues/180201.1.html

`symbolic` is not currently aware of this, and can't provide source code for ELF files:
https://github.com/getsentry/symbolic/blob/2b0af545c31b7a24d7c07e8a137272bce5cafa7e/symbolic-debuginfo/src/elf.rs#L536-L537

It'd be nice to have this as a convenient alternative to source bundles. Clang supports it through a [`-gembed-source` flag](
https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-gembed-source
) but other toolchains (Rust) don't have similar flags yet as far as I can tell.

For more information, here's the LLVM implementation of the DWARF bits: https://reviews.llvm.org/D42765

I'll probably take a stab at implementing this but wanted to open an issue first :slightly_smiling_face: 

Pull request description:
Support for DWARFv5 embedded source code extension
Hi,

this implements support for source code embedded directly inside of the DWARF debug info. (Fixes #835)

I'm not sure about the logic in `source_by_path` though. Should it only match full paths or relative paths instead?
My use-case is fine with either way since I'm iterating over the files in the debug session directly.

~~This PR currently depends on support in `gimli` that has not made it into a release yet. I'll update the PR once a new version of `gimli` is available on crates.io.~~

Thanks!

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 869ec8af..7225297e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## Unreleased
+
+**Features**
+
+- Support for DWARFv5 embedded source code extension ([#849](https://github.com/getsentry/symbolic/pull/849))
+
 ## 12.9.2
 
 - Downgrade and pin `zip` to fix SourceBundles with >64k files ([#846](https://github.com/getsentry/symbolic/pull/846))
diff --git a/Cargo.lock b/Cargo.lock
index 8f488300..5db70e3d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -190,9 +190,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
 
 [[package]]
 name = "backtrace"
-version = "0.3.72"
+version = "0.3.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
+checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
 dependencies = [
  "addr2line 0.22.0",
  "cc",
@@ -235,9 +235,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.5.0"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 
 [[package]]
 name = "bitvec"
@@ -315,9 +315,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
 
 [[package]]
 name = "cc"
-version = "1.0.99"
+version = "1.0.101"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695"
+checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d"
 dependencies = [
  "jobserver",
  "libc",
@@ -378,18 +378,18 @@ checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea"
 
 [[package]]
 name = "clap"
-version = "4.5.6"
+version = "4.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9689a29b593160de5bc4aacab7b5d54fb52231de70122626c178e6a368994c7"
+checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
 dependencies = [
  "clap_builder",
 ]
 
 [[package]]
 name = "clap_builder"
-version = "4.5.6"
+version = "4.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e5387378c84f6faa26890ebf9f0a92989f8873d4d380467bcd0d8d8620424df"
+checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
 dependencies = [
  "anstream",
  "anstyle",
@@ -512,7 +512,7 @@ version = "0.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "crossterm_winapi",
  "libc",
  "mio",
@@ -584,9 +584,9 @@ dependencies = [
 
 [[package]]
 name = "displaydoc"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -618,9 +618,9 @@ dependencies = [
 
 [[package]]
 name = "either"
-version = "1.12.0"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
 
 [[package]]
 name = "elementtree"
@@ -770,9 +770,9 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
 
 [[package]]
 name = "gimli"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2e1d97fbe9722ba9bbd0c97051c2956e726562b61f86a25a4360398a40edfc9"
+checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64"
 dependencies = [
  "fallible-iterator 0.3.0",
  "stable_deref_trait",
@@ -1007,9 +1007,9 @@ dependencies = [
 
 [[package]]
 name = "lazy_static"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
 [[package]]
 name = "leb128"
@@ -1053,9 +1053,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
 
 [[package]]
 name = "log"
-version = "0.4.21"
+version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
 
 [[package]]
 name = "matchers"
@@ -1074,9 +1074,9 @@ checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
 
 [[package]]
 name = "memchr"
-version = "2.7.2"
+version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
 name = "memmap2"
@@ -1113,7 +1113,7 @@ version = "0.21.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5c4d14bcca0fd3ed165a03000480aaa364c6860c34e900cb2dafdf3b95340e77"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "debugid",
  "num-derive",
  "num-traits",
@@ -1182,9 +1182,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
 name = "miniz_oxide"
-version = "0.7.3"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
+checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
 dependencies = [
  "adler",
 ]
@@ -1207,7 +1207,7 @@ version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4c25a3bb7d880e8eceab4822f3141ad0700d20f025991c1f03bd3d00219a5fc"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
 ]
 
 [[package]]
@@ -1251,9 +1251,9 @@ dependencies = [
 
 [[package]]
 name = "num-bigint"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
 dependencies = [
  "num-integer",
  "num-traits",
@@ -1297,9 +1297,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.35.0"
+version = "0.36.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
+checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434"
 dependencies = [
  "memchr",
 ]
@@ -1507,9 +1507,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.85"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
 dependencies = [
  "unicode-ident",
 ]
@@ -1520,7 +1520,7 @@ version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "hex",
 ]
 
@@ -1615,11 +1615,11 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
+checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
 ]
 
 [[package]]
@@ -1693,7 +1693,7 @@ version = "0.38.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -1796,9 +1796,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.117"
+version = "1.0.118"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
+checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
 dependencies = [
  "itoa",
  "ryu",
@@ -2057,7 +2057,7 @@ version = "0.113.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "29f7cbdc8d3f31a863224649258942a537be44e37cb8f5413da63b51159779b9"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "is-macro",
  "num-bigint",
  "phf",
@@ -2070,9 +2070,9 @@ dependencies = [
 
 [[package]]
 name = "swc_ecma_parser"
-version = "0.144.2"
+version = "0.144.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31adf4599e8de70f3b754dfc34ec2ab09fa6841d79a9f4a888250a404eae7030"
+checksum = "fc0b4193b9c127db1990a5a08111aafe0122bc8b138646807c63f2a6521b7da4"
 dependencies = [
  "either",
  "new_debug_unreachable",
@@ -2211,7 +2211,7 @@ dependencies = [
  "elsa",
  "fallible-iterator 0.3.0",
  "flate2",
- "gimli 0.30.0",
+ "gimli 0.31.0",
  "goblin",
  "insta",
  "lazy_static",
@@ -2339,9 +2339,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.66"
+version = "2.0.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
+checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2439,9 +2439,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.6.0"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -2538,9 +2538,9 @@ dependencies = [
 
 [[package]]
 name = "triomphe"
-version = "0.1.12"
+version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90"
+checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369"
 dependencies = [
  "serde",
  "stable_deref_trait",
@@ -2601,9 +2601,9 @@ dependencies = [
 
 [[package]]
 name = "url"
-version = "2.5.0"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
 dependencies = [
  "form_urlencoded",
  "idna",
@@ -2618,9 +2618,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "uuid"
-version = "1.8.0"
+version = "1.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
+checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
 dependencies = [
  "sha1_smol",
 ]
@@ -2714,7 +2714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "07035cc9a9b41e62d3bb3a3815a66ab87c993c06fe1cf6b2a3f2a18499d937db"
 dependencies = [
  "ahash",
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "hashbrown",
  "indexmap",
  "semver 1.0.23",
@@ -3025,9 +3025,9 @@ dependencies = [
 
 [[package]]
 name = "zstd-sys"
-version = "2.0.10+zstd.1.5.6"
+version = "2.0.11+zstd.1.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa"
+checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4"
 dependencies = [
  "cc",
  "pkg-config",
diff --git a/Cargo.toml b/Cargo.toml
index e8f9c20a..948b65fd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,7 @@ fallible-iterator = "0.3.0"
 flate2 = { version = "1.0.25", default-features = false, features = [
     "rust_backend",
 ] }
-gimli = { version = "0.30.0", default-features = false, features = [
+gimli = { version = "0.31.0", default-features = false, features = [
     "read",
     "std",
     "fallible-iterator",
diff --git a/symbolic-debuginfo/src/base.rs b/symbolic-debuginfo/src/base.rs
index e6644076..1b20314c 100644
--- a/symbolic-debuginfo/src/base.rs
+++ b/symbolic-debuginfo/src/base.rs
@@ -450,13 +450,25 @@ pub struct FileInfo<'data> {
     name: Cow<'data, [u8]>,
     /// Path to the file.
     dir: Cow<'data, [u8]>,
+    /// Source code
+    source: Option<Cow<'data, [u8]>>,
 }
 
 impl<'data> FileInfo<'data> {
     /// Creates a `FileInfo` with a given directory and the file name.
     #[cfg(feature = "dwarf")]
     pub fn new(dir: Cow<'data, [u8]>, name: Cow<'data, [u8]>) -> Self {
-        FileInfo { name, dir }
+        Self::with_source(dir, name, None)
+    }
+
+    /// Creates a `FileInfo` with a given directory, the file name, and optional source code.
+    #[cfg(feature = "dwarf")]
+    pub fn with_source(
+        dir: Cow<'data, [u8]>,
+        name: Cow<'data, [u8]>,
+        source: Option<Cow<'data, [u8]>>,
+    ) -> Self {
+        FileInfo { name, dir, source }
     }
 
     /// Creates a `FileInfo` from a joined path by trying to split it.
@@ -470,6 +482,7 @@ impl<'data> FileInfo<'data> {
                 Some(dir) => Cow::Borrowed(dir),
                 None => Cow::default(),
             },
+            source: None,
         }
     }
 
@@ -485,6 +498,7 @@ impl<'data> FileInfo<'data> {
                 Some(dir) => Cow::Owned(dir.to_vec()),
                 None => Cow::default(),
             },
+            source: None,
         }
     }
 
@@ -493,6 +507,7 @@ impl<'data> FileInfo<'data> {
         FileInfo {
             name: Cow::Borrowed(name),
             dir: Cow::default(),
+            source: None,
         }
     }
 
@@ -506,6 +521,11 @@ impl<'data> FileInfo<'data> {
         from_utf8_cow_lossy(&self.dir)
     }
 
+    /// The embedded source code as an UTF-8 string.
+    pub fn source_str(&self) -> Option<Cow<'data, str>> {
+        self.source.as_ref().map(from_utf8_cow_lossy)
+    }
+
     /// The full path to the file, relative to the compilation directory.
     pub fn path_str(&self) -> String {
         let joined = join_path(&self.dir_str(), &self.name_str());
diff --git a/symbolic-debuginfo/src/dwarf.rs b/symbolic-debuginfo/src/dwarf.rs
index 2e41bc82..8c35fe0a 100644
--- a/symbolic-debuginfo/src/dwarf.rs
+++ b/symbolic-debuginfo/src/dwarf.rs
@@ -10,7 +10,7 @@
 //! [`PeObject`]: ../pe/struct.PeObject.html
 
 use std::borrow::Cow;
-use std::collections::BTreeSet;
+use std::collections::{BTreeSet, HashMap};
 use std::error::Error;
 use std::fmt;
 use std::marker::PhantomData;
@@ -692,7 +692,7 @@ impl<'d, 'a> DwarfUnit<'d, 'a> {
         line_program: &LineNumberProgramHeader<'d>,
         file: &LineProgramFileEntry<'d>,
     ) -> FileInfo<'d> {
-        FileInfo::new(
+        FileInfo::with_source(
             Cow::Borrowed(resolve_byte_name(
                 self.bcsymbolmap,
                 file.directory(line_program)
@@ -703,6 +703,14 @@ impl<'d, 'a> DwarfUnit<'d, 'a> {
                 self.bcsymbolmap,
                 self.inner.slice_value(file.path_name()).unwrap_or_default(),
             )),
+            file.source().and_then(|source| {
+                let unit_ref = self.inner.unit.unit_ref(self.inner.info);
+                match unit_ref.attr_string(source) {
+                    Ok(source) if source.is_empty() => None,
+                    Err(_) => None,
+                    Ok(source) => Some(Cow::Borrowed(source.slice())),
+                }
+            }),
         )
     }
 
@@ -1295,6 +1303,9 @@ impl std::iter::FusedIterator for DwarfUnitIterator<'_> {}
 pub struct DwarfDebugSession<'data> {
     cell: SelfCell<Box<DwarfSections<'data>>, DwarfInfo<'data>>,
     bcsymbolmap: Option<Arc<BcSymbolMap<'data>>>,
+    // We store the "lookup path" for each entry here to avoid lifetime issues w.r.t
+    // HashMap<_, SourceFileDescriptor<'data>>, as we can't construct this in a method call.
+    sources_path_to_file_idx: OnceCell<HashMap<String, usize>>,
 }
 
 impl<'data> DwarfDebugSession<'data> {
@@ -1316,6 +1327,7 @@ impl<'data> DwarfDebugSession<'data> {
         Ok(DwarfDebugSession {
             cell,
             bcsymbolmap: None,
+            sources_path_to_file_idx: OnceCell::default(),
         })
     }
 
@@ -1348,11 +1360,33 @@ impl<'data> DwarfDebugSession<'data> {
     }
 
     /// See [DebugSession::source_by_path] for more information.
+    /// This lookup returns entries that match a given [FileEntry::abs_path_str].
+    ///
+    /// Note that this does not load additional sources from disk and only works with sources
+    /// embedded directly in the debug information (DW_LNCT_LLVM_source).
     pub fn source_by_path(
         &self,
-        _path: &str,
+        path: &str,
     ) -> Result<Option<SourceFileDescriptor<'_>>, DwarfError> {
-        Ok(None)
+        // Construct / fetch a lookup table to avoid scanning and comparing each file's path in this
+        // operation:
+        let sources = self.sources_path_to_file_idx.get_or_init(|| {
+            let mut res = HashMap::new();
+            for (i, file) in self.files().enumerate() {
+                if let Ok(file) = file {
+                    if file.source_str().is_some() {
+                        res.insert(file.abs_path_str(), i);
+                    }
+                }
+            }
+            res
+        });
+
+        Ok(sources.get(path).map(|&idx| {
+            // These unwraps hold by construction above
+            let file = self.files().nth(idx).unwrap().unwrap();
+            SourceFileDescriptor::new_embedded(file.source_str().unwrap(), None)
+        }))
     }
 }
 
diff --git a/symbolic-debuginfo/src/elf.rs b/symbolic-debuginfo/src/elf.rs
index 6ddb2f29..266c69b8 100644
--- a/symbolic-debuginfo/src/elf.rs
+++ b/symbolic-debuginfo/src/elf.rs
@@ -537,7 +537,14 @@ impl<'data> ElfObject<'data> {
 
     /// Determines whether this object contains embedded source.
     pub fn has_sources(&self) -> bool {
-        false
+        // Note: It'd be great to be able to determine this without iterating over all file entries.
+        //       Unfortunately, though, this seems to be the only correct implementation.
+        match self.debug_session() {
+            Ok(session) => session
+                .files()
+                .any(|f| f.is_ok_and(|f| f.source_str().is_some())),
+            Err(_) => false,
+        }
     }
 
     /// Determines whether this object is malformed and was only partially parsed
diff --git a/symbolic-debuginfo/src/wasm.rs b/symbolic-debuginfo/src/wasm.rs
index 1454f2f4..583bf0a0 100644
--- a/symbolic-debuginfo/src/wasm.rs
+++ b/symbolic-debuginfo/src/wasm.rs
@@ -131,7 +131,12 @@ impl<'data> WasmObject<'data> {
 
     /// Determines whether this object contains embedded source.
     pub fn has_sources(&self) -> bool {
-        false
+        match self.debug_session() {
+            Ok(session) => session
+                .files()
+                .any(|f| f.is_ok_and(|f| f.source_str().is_some())),
+            Err(_) => false,
+        }
     }
 
     /// Determines whether this object is malformed and was only partially parsed
diff --git a/symbolic-symcache/src/lookup.rs b/symbolic-symcache/src/lookup.rs
index e7635d75..9c1a4f4a 100644
--- a/symbolic-symcache/src/lookup.rs
+++ b/symbolic-symcache/src/lookup.rs
@@ -228,9 +228,8 @@ impl<'data> Iterator for Functions<'data> {
             }
         }
 
-        function.map(|f| {
+        function.inspect(|_f| {
             self.function_idx += 1;
-            f
         })
     }
 }
@@ -266,9 +265,8 @@ impl<'data> Iterator for Files<'data> {
     type Item = File<'data>;
 
     fn next(&mut self) -> Option<Self::Item> {
-        self.cache.get_file(self.file_idx).map(|f| {
+        self.cache.get_file(self.file_idx).inspect(|_f| {
             self.file_idx += 1;
-            f
         })
     }
 }


Test patch:
diff --git a/symbolic-debuginfo/tests/test_objects.rs b/symbolic-debuginfo/tests/test_objects.rs
index 6372f0a1..c98ee997 100644
--- a/symbolic-debuginfo/tests/test_objects.rs
+++ b/symbolic-debuginfo/tests/test_objects.rs
@@ -1,4 +1,4 @@
-use std::{env, ffi::CString, fmt, io::BufWriter};
+use std::{collections::HashMap, env, ffi::CString, fmt, io::BufWriter};
 
 use symbolic_common::ByteView;
 use symbolic_debuginfo::{
@@ -871,6 +871,8 @@ fn test_wasm_symbols() -> Result<(), Error> {
         Some("bda18fd85d4a4eb893022d6bfad846b1".into())
     );
 
+    assert!(!object.has_sources());
+
     let symbols = object.symbol_map();
     insta::assert_debug_snapshot!("wasm_symbols", SymbolsDebug(&symbols));
 
@@ -893,3 +895,35 @@ fn test_wasm_line_program() -> Result<(), Error> {
 
     Ok(())
 }
+
+#[test]
+fn test_wasm_has_sources() -> Result<(), Error> {
+    let view = ByteView::open(fixture("wasm/embed-source.wasm"))?;
+    let object = Object::parse(&view)?;
+
+    assert!(object.has_sources());
+
+    let session = object.debug_session()?;
+
+    let files_with_source = session
+        .files()
+        .filter_map(|x| {
+            let file_entry = x.ok()?;
+            let source = file_entry.source_str()?;
+            Some((file_entry.abs_path_str(), source))
+        })
+        .collect::<HashMap<_, _>>();
+
+    insta::assert_debug_snapshot!(files_with_source, @r###"
+    {
+        "/tmp/foo.c": "int main(void) { return 42; }\n",
+    }
+    "###);
+
+    assert!(session.source_by_path("foo/bar.c").unwrap().is_none());
+    assert!(session.source_by_path("foo.c").unwrap().is_none());
+    // source_by_path matches the absolute file path
+    assert!(session.source_by_path("/tmp/foo.c").unwrap().is_some());
+
+    Ok(())
+}