use crate::winpath::is_windows_absolute;
use std::sync::OnceLock;
pub(crate) fn file_path_to_sarif_uri(path: &str) -> String {
if let Some(rel) = relative_to_scan_root(path) {
return render_relative_uri(&rel);
}
if path.starts_with('/') {
format!("file://{}", percent_encode_path(path))
} else if is_windows_absolute(path) {
let normalised = path.replace('\\', "/");
format!("file:///{}", percent_encode_path(&normalised))
} else {
render_relative_uri(path)
}
}
fn render_relative_uri(rel: &str) -> String {
let encoded = percent_encode_path(rel);
if first_segment_has_colon(&encoded) {
format!("./{encoded}")
} else {
encoded
}
}
fn first_segment_has_colon(uri: &str) -> bool {
match uri.split_once('/') {
Some((first, _)) => first.contains(':'),
None => uri.contains(':'),
}
}
fn scan_root() -> Option<&'static std::path::Path> {
static ROOT: OnceLock<Option<std::path::PathBuf>> = OnceLock::new();
ROOT.get_or_init(|| std::env::current_dir().ok()).as_deref() }
fn relative_to_scan_root(path: &str) -> Option<String> {
if !path.starts_with('/') && !is_windows_absolute(path) {
return None; }
relative_to(path, scan_root()?)
}
pub(crate) fn relative_to(path: &str, root: &std::path::Path) -> Option<String> {
let normalised = path.replace('\\', "/");
std::path::Path::new(&normalised)
.strip_prefix(root)
.ok() .map(|r| r.to_string_lossy().replace('\\', "/"))
}
pub(crate) const CODE_SCANNING_SECURITY_TAG: &str = "security";
pub(crate) fn code_scanning_security_severity(severity: crate::Severity) -> &'static str {
use crate::Severity as S;
match severity {
S::Critical => "9.5",
S::High => "8.0",
S::Medium => "5.0",
S::Low => "2.0",
S::ClientSafe => "1.0",
S::Info => "0.0",
}
}
pub(crate) fn credential_fingerprints(
credential_hash: crate::CredentialHash,
) -> Option<std::collections::BTreeMap<String, String>> {
if credential_hash.is_zero() {
return None;
}
let mut fp = std::collections::BTreeMap::new();
fp.insert(
"keyhog/credentialHash/v1".to_string(),
crate::finding::hex_encode(credential_hash),
);
Some(fp)
}
fn percent_encode_path(path: &str) -> String {
let mut out = String::with_capacity(path.len());
for byte in path.bytes() {
let safe =
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~' | b'/' | b':');
if safe {
out.push(byte as char);
} else {
out.push('%');
const HEX: &[u8; 16] = b"0123456789ABCDEF";
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0F) as usize] as char);
}
}
out
}