sbpf-coverage 0.2.12

Maps SBPF execution traces to source code for test coverage and trace disassembly of Solana programs
Documentation
use std::{
    env::current_dir,
    ffi::OsStr,
    path::{Path, PathBuf},
    process::{Command, Stdio},
};

use crate::Loader;
use crate::addr2line::gimli::{self, DW_AT_language, DW_AT_producer, DwAt, DwTag, LittleEndian};
use crate::{Object, ObjectSection};
use anyhow::anyhow;
use sha2::{Digest, Sha256};

pub trait StripCurrentDir {
    fn strip_current_dir(&self) -> &Self;
}

impl StripCurrentDir for Path {
    fn strip_current_dir(&self) -> &Self {
        let Ok(current_dir) = current_dir() else {
            return self;
        };
        self.strip_prefix(current_dir).unwrap_or(self)
    }
}

pub fn find_files_with_extension(dirs: &[PathBuf], extension: &str) -> Vec<PathBuf> {
    let mut so_files = Vec::new();

    for dir in dirs {
        if dir.is_dir()
            && let Ok(entries) = std::fs::read_dir(dir)
        {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_file() && path.extension().is_some_and(|ext| ext == extension) {
                    so_files.push(path);
                }
            }
        }
    }

    so_files
}

pub fn compute_hash(slice: &[u8]) -> String {
    hex::encode(Sha256::digest(slice).as_slice())
}

pub fn get_section_start_address(loader: &Loader, section: &str) -> anyhow::Result<u64> {
    Ok(loader
        .get_section_range(section.as_bytes())
        .ok_or(anyhow!("Can't get {} section begin address", section))?
        .begin)
}

pub fn get_dwarf_attribute(
    object: &object::File,
    tag: DwTag,
    attribute: DwAt,
) -> anyhow::Result<String> {
    let load_section = |id: gimli::SectionId| -> Result<_, LittleEndian> {
        let data = object
            .section_by_name(id.name())
            .map(|s| s.data().unwrap_or(&[]))
            .unwrap_or(&[]);
        Ok(gimli::EndianSlice::new(data, LittleEndian))
    };

    let dwarf = addr2line::gimli::Dwarf::load(&load_section)
        .map_err(|_| anyhow!("Failed to load DWARF sections"))?;
    let mut iter = dwarf.units();
    while let Ok(Some(header)) = iter.next() {
        let Ok(unit) = dwarf.unit(header) else {
            continue;
        };
        let mut entries = unit.entries();
        while let Ok(Some(entry)) = entries.next_dfs() {
            if let Some(val) = entry.attr_value(attribute)
                && entry.tag() == tag
            {
                match attribute {
                    a if a == DW_AT_producer => {
                        if let Ok(s) = dwarf.attr_string(&unit, val) {
                            return Ok(s.to_string_lossy().to_string());
                        }
                    }
                    a if a == DW_AT_language => {
                        if let gimli::AttributeValue::Language(lang) = val {
                            return Ok(lang.to_string());
                        }
                    }
                    _ => continue,
                }
            }
        }
    }
    Err(anyhow!(
        "No DWARF entry found for {:?} with attribute {:?}",
        tag,
        attribute
    ))
}

pub fn execute_cmd<I, S>(program: &Path, args: I) -> Option<String>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let child = Command::new(program)
        .args(args)
        .stdout(Stdio::piped())
        .spawn()
        .map_err(|e| {
            eprintln!("Failed to execute {}: {}", program.display(), e);
        })
        .ok()?;

    let output = child
        .wait_with_output()
        .map_err(|e| eprintln!("failed to wait on child: {}", e))
        .ok()?;
    Some(
        output
            .stdout
            .as_slice()
            .iter()
            .map(|&c| c as char)
            .collect::<String>()
            .trim()
            .into(),
    )
}

/// Returns the nth line from the given string, or empty if out of bounds.
pub fn read_nth_line(file_content: &str, line_number: usize) -> String {
    file_content
        .lines()
        .nth(line_number)
        .unwrap_or("")
        .to_string()
}