use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use assert_json_diff::assert_json_eq;
pub use samply_api::debugid::DebugId;
use samply_api::{samply_symbols, Api};
use samply_symbols::{
CandidatePathInfo, FileAndPathHelper, FileAndPathHelperResult, FileLocation, LibraryInfo,
OptionallySendFuture, SymbolManager,
};
pub async fn query_api(request_url: &str, request_json: &str, symbol_directory: PathBuf) -> String {
let helper = Helper { symbol_directory };
let symbol_manager = SymbolManager::with_helper(helper);
let api = Api::new(&symbol_manager);
api.query_api(request_url, request_json).await
}
struct Helper {
symbol_directory: PathBuf,
}
impl FileAndPathHelper for Helper {
type F = memmap2::Mmap;
type FL = FileLocationType;
fn get_candidate_paths_for_debug_file(
&self,
library_info: &LibraryInfo,
) -> FileAndPathHelperResult<Vec<CandidatePathInfo<FileLocationType>>> {
let debug_name = match library_info.debug_name.as_deref() {
Some(debug_name) => debug_name,
None => return Ok(Vec::new()),
};
let mut paths = vec![];
if debug_name.ends_with(".so") {
let debug_debug_name = format!("{debug_name}.dbg");
paths.push(CandidatePathInfo::SingleFile(FileLocationType(
self.symbol_directory.join(debug_debug_name),
)));
}
if !debug_name.ends_with(".pdb") {
paths.push(CandidatePathInfo::SingleFile(FileLocationType(
self.symbol_directory
.join(format!("{debug_name}.dSYM"))
.join("Contents")
.join("Resources")
.join("DWARF")
.join(debug_name),
)));
}
paths.push(CandidatePathInfo::SingleFile(FileLocationType(
self.symbol_directory.join(debug_name),
)));
if self.symbol_directory.starts_with("/usr/")
|| self.symbol_directory.starts_with("/System/")
{
if let Some(dylib_path) = self.symbol_directory.join(debug_name).to_str() {
paths.extend(
self.get_dyld_shared_cache_paths(None)
.unwrap()
.into_iter()
.map(|dyld_cache_path| CandidatePathInfo::InDyldCache {
dyld_cache_path,
dylib_path: dylib_path.to_string(),
}),
);
}
}
Ok(paths)
}
fn get_dyld_shared_cache_paths(
&self,
_arch: Option<&str>,
) -> FileAndPathHelperResult<Vec<FileLocationType>> {
Ok(vec![
FileLocationType::new("/System/Library/dyld/dyld_shared_cache_arm64e"),
FileLocationType::new("/System/Library/dyld/dyld_shared_cache_x86_64h"),
FileLocationType::new("/System/Library/dyld/dyld_shared_cache_x86_64"),
])
}
fn load_file(
&self,
location: FileLocationType,
) -> std::pin::Pin<Box<dyn OptionallySendFuture<Output = FileAndPathHelperResult<Self::F>> + '_>>
{
Box::pin(async {
let mut path = location.0;
if !path.starts_with(&self.symbol_directory) {
if let Some(filename) = path.file_name() {
let redirected_path = self.symbol_directory.join(filename);
if std::fs::metadata(&redirected_path).is_ok() {
eprintln!("Redirecting {:?} to {:?}", &path, &redirected_path);
path = redirected_path;
}
}
}
eprintln!("Reading file {:?}", &path);
let file = File::open(&path)?;
Ok(unsafe { memmap2::MmapOptions::new().map(&file)? })
})
}
fn get_candidate_paths_for_binary(
&self,
library_info: &LibraryInfo,
) -> FileAndPathHelperResult<Vec<CandidatePathInfo<FileLocationType>>> {
let name = match library_info.name.as_deref() {
Some(name) => name,
None => return Ok(Vec::new()),
};
let mut paths = vec![];
paths.push(CandidatePathInfo::SingleFile(FileLocationType(
self.symbol_directory.join(name),
)));
if self.symbol_directory.starts_with("/usr/")
|| self.symbol_directory.starts_with("/System/")
{
if let Some(dylib_path) = self.symbol_directory.join(name).to_str() {
paths.extend(
self.get_dyld_shared_cache_paths(None)
.unwrap()
.into_iter()
.map(|dyld_cache_path| CandidatePathInfo::InDyldCache {
dyld_cache_path,
dylib_path: dylib_path.to_string(),
}),
);
}
}
Ok(paths)
}
}
#[derive(Clone)]
struct FileLocationType(PathBuf);
impl FileLocationType {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self(path.into())
}
}
impl std::fmt::Display for FileLocationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.to_string_lossy().fmt(f)
}
}
impl FileLocation for FileLocationType {
fn location_for_dyld_subcache(&self, suffix: &str) -> Option<Self> {
let mut filename = self.0.file_name().unwrap().to_owned();
filename.push(suffix);
Some(Self(self.0.with_file_name(filename)))
}
fn location_for_external_object_file(&self, object_file: &str) -> Option<Self> {
Some(Self(object_file.into()))
}
fn location_for_pdb_from_binary(&self, _pdb_path_in_binary: &str) -> Option<Self> {
None
}
fn location_for_source_file(&self, source_file_path: &str) -> Option<Self> {
Some(Self(source_file_path.into()))
}
fn location_for_breakpad_symindex(&self) -> Option<Self> {
Some(Self(self.0.with_extension("symindex")))
}
fn location_for_dwo(&self, _comp_dir: &str, _path: &str) -> Option<Self> {
None }
fn location_for_dwp(&self) -> Option<Self> {
let mut s = self.0.as_os_str().to_os_string();
s.push(".dwp");
Some(Self(s.into()))
}
}
fn fixtures_dir() -> PathBuf {
let this_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
this_dir.join("..").join("fixtures")
}
fn compare_snapshot(
request_url: &str,
request_json: &str,
symbol_directory: PathBuf,
snapshot_filename: &str,
output_filename: &str,
) {
let output = futures::executor::block_on(crate::query_api(
request_url,
request_json,
symbol_directory,
));
let output_json: serde_json::Value = serde_json::from_str(&output).unwrap();
let mut expected_json: Option<serde_json::Value> = None;
if let Ok(mut snapshot_file) =
File::open(fixtures_dir().join("snapshots").join(snapshot_filename))
{
let mut expected: String = String::new();
snapshot_file.read_to_string(&mut expected).unwrap();
expected_json = Some(serde_json::from_str(&expected).unwrap());
}
if expected_json.as_ref() != Some(&output_json) {
let mut output_file =
File::create(fixtures_dir().join("snapshots").join(output_filename)).unwrap();
output_file.write_all(output.as_bytes()).unwrap();
}
match expected_json {
Some(expected_json) => assert_json_eq!(output_json, expected_json),
None => panic!("No snapshot found"),
}
}
#[test]
fn win64_local_v5_snapshot_1() {
compare_snapshot(
"/symbolicate/v5",
r#"{
"memoryMap": [
[
"mozglue.dll",
"B3CC644ECC086E044C4C44205044422E1"
]
],
"stacks": [
[
[0, 214644]
]
]
}"#,
fixtures_dir().join("win64-local"),
"api-v5-win64-local-1.txt",
"output-api-v5-win64-local-1.txt",
);
}
#[test]
fn win64_ci_v5_snapshot() {
compare_snapshot(
"/symbolicate/v5",
r#"{
"memoryMap": [
[
"firefox.pdb",
"AA152DEB2D9B76084C4C44205044422E1"
],
[
"mozglue.pdb",
"63C609072D3499F64C4C44205044422E1"
]
],
"stacks": [
[
[0, 204776],
[0, 129423],
[0, 244290],
[0, 244219],
[1, 244290],
[1, 244219],
[1, 237799]
]
]
}"#,
fixtures_dir().join("win64-ci"),
"api-v5-win64-ci.txt",
"output-api-v5-win64-ci.txt",
);
}
#[test]
fn android32_v5_local() {
compare_snapshot(
"/symbolicate/v5",
r#"{
"memoryMap": [
[
"libmozglue.so",
"0CE47B7C29F27CED55C41233B93EBA450"
]
],
"stacks": [
[
[0, 247618],
[0, 685896],
[0, 686768]
]
]
}"#,
fixtures_dir().join("android32-local"),
"api-v5-android32-local.txt",
"output-api-v5-android32-local.txt",
);
}
#[test]
fn stripped_macos() {
compare_snapshot(
"/symbolicate/v5",
r#"{
"memoryMap": [
[
"libsoftokn3.dylib",
"F7DE6E25737B3B1885A5079DC41D77B40"
]
],
"stacks": [
[
[0, 230071],
[0, 232505]
]
]
}"#,
fixtures_dir().join("macos-ci"),
"api-v5-stripped-macos.txt",
"output-api-v5-stripped-macos.txt",
);
}
#[test]
fn win_exe() {
compare_snapshot(
"/symbolicate/v5",
r#"{
"memoryMap": [
[
"updater.exe",
"5C08299576CB004F4C4C44205044422E1"
]
],
"stacks": [
[
[0, 27799],
[0, 158574]
]
]
}"#,
fixtures_dir().join("win64-local"),
"api-v5-win-exe.txt",
"output-api-v5-win-exe.txt",
);
}
#[test]
fn asm_with_continue() {
compare_snapshot(
"/asm/v1",
r#"{
"name": "libmozglue.so",
"codeId": "7c7be40cf229ed7c55c41233b93eba456dcbc082",
"debugName": "libmozglue.so",
"debugId": "0CE47B7C29F27CED55C41233B93EBA450",
"startAddress": "0x51fd1",
"size": "0x8",
"continueUntilFunctionEnd": true
}"#,
fixtures_dir().join("android32-local"),
"asm_with_continue.txt",
"output-asm_with_continue.txt",
)
}
#[test]
fn asm_x86_64() {
compare_snapshot(
"/asm/v1",
r#"{
"name": "firefox.exe",
"debugName": "firefox.pdb",
"debugId": "8A913DE821D9DE764C4C44205044422E1",
"startAddress": "0x17a20",
"size": "0x3a"
}"#,
fixtures_dir().join("win64-local"),
"asm_x86_64.txt",
"output-asm_x86_64.txt",
)
}