use hashbrown::{HashMap, HashSet};
use log::warn;
use regex::Regex;
use serde::Deserialize;
use sha2::{Digest, Sha512};
use std::path::Path;
use std::sync::Mutex;
use crate::common;
use crate::utils;
#[derive(Debug, Default, Deserialize)]
struct Mapping {
source: String,
destination: String,
files: HashSet<String>,
}
#[derive(Debug, Default, Deserialize)]
struct Mappings {
variables: HashMap<String, String>,
mappings: Vec<Mapping>,
}
impl Mappings {
fn new(path: &str) -> common::Result<Self> {
let data = utils::read(path)?;
match serde_json::from_slice::<Self>(&data) {
Err(e) => Err(e.into()),
Ok(m) => Ok(m),
}
}
}
fn get_digest(file: &Path, uppercase: bool) -> common::Result<String> {
let data = utils::read(file)?;
let sha = Sha512::digest(data);
Ok(if uppercase {
format!("{sha:X}")
} else {
format!("{sha:x}")
})
}
#[derive(Debug)]
pub enum ActionKind {
Group(usize),
MappingVar(String),
Digest(bool),
None,
}
#[derive(Debug)]
struct Action {
kind: ActionKind,
start: usize,
end: usize,
}
#[derive(Debug)]
struct PathMappingGenerator {
re: Regex,
actions: Vec<Action>,
mapping: Vec<u8>,
files: HashSet<String>,
}
impl PathMappingGenerator {
pub(crate) fn new(
re: &str,
mapping: &str,
mapping_vars: &HashMap<String, String>,
files: HashSet<String>,
) -> common::Result<Self> {
let re = Regex::new(re)?;
let mapping = mapping.as_bytes().to_vec();
let mut chunk_start = 0;
let mut chunk_end = 0;
let mut open = false;
let mut actions = Vec::new();
for (i, c) in mapping.iter().enumerate() {
if *c == b'{' {
chunk_end = i;
open = true;
} else if *c == b'}' {
if !open {
continue;
}
let action = &mapping[chunk_end + 1..i];
let action = std::str::from_utf8(action).unwrap();
let action = if action == "DIGEST" {
ActionKind::Digest(true)
} else if action == "digest" {
ActionKind::Digest(false)
} else if let Some(val) = mapping_vars.get(action) {
ActionKind::MappingVar(val.clone())
} else if let Ok(group) = action.parse::<usize>() {
ActionKind::Group(group)
} else {
anyhow::bail!("Invalid action {} in mapping string", action);
};
actions.push(Action {
kind: action,
start: chunk_start,
end: chunk_end,
});
open = false;
chunk_start = i + 1;
}
}
if chunk_start < mapping.len() {
actions.push(Action {
kind: ActionKind::None,
start: chunk_start,
end: mapping.len(),
});
}
Ok(PathMappingGenerator {
re,
actions,
mapping,
files,
})
}
pub(crate) fn apply(&self, file: &Path, file_str: &str) -> common::Result<Option<String>> {
if self.files.is_empty() || self.files.contains(file_str) {
if let Some(caps) = self.re.captures(file_str) {
let mut buf = Vec::with_capacity(self.mapping.len() * 3);
for action in self.actions.iter() {
buf.extend_from_slice(&self.mapping[action.start..action.end]);
match action.kind {
ActionKind::Group(group) => {
if let Some(group) = caps.get(group) {
let group = group.as_str();
if cfg!(windows) {
let group = group.replace('\\', "/");
buf.extend_from_slice(group.as_bytes());
} else {
buf.extend_from_slice(group.as_bytes());
}
} else {
return Ok(None);
}
}
ActionKind::MappingVar(ref val) => {
buf.extend_from_slice(val.as_bytes());
}
ActionKind::Digest(uppercase) => {
buf.extend_from_slice(get_digest(file, uppercase)?.as_bytes());
}
ActionKind::None => {}
}
}
return Ok(Some(unsafe { String::from_utf8_unchecked(buf) }));
}
}
Ok(None)
}
}
#[derive(Debug, Default)]
pub struct PathMappings {
mappings: Vec<PathMappingGenerator>,
cache: Mutex<HashMap<String, String>>,
}
impl PathMappings {
pub(crate) fn new(
variables: &Option<Vec<&str>>,
sources: &Option<Vec<&str>>,
destinations: &Option<Vec<&str>>,
file: &Option<&str>,
) -> common::Result<Option<Self>> {
let vars = Self::get_variables(variables)?;
let mut mappings = Vec::new();
Self::get_mappings_from_file(&vars, file, &mut mappings)?;
Self::get_mappings(&vars, sources, destinations, &mut mappings)?;
Ok(if mappings.is_empty() {
None
} else {
Some(PathMappings {
mappings,
cache: Mutex::new(HashMap::default()),
})
})
}
fn get_variables(vars: &Option<Vec<&str>>) -> common::Result<HashMap<String, String>> {
let mut variables = HashMap::default();
if let Some(vars) = vars {
for var in vars {
let pair = var.splitn(2, '=').collect::<Vec<_>>();
anyhow::ensure!(pair.len() == 2, "Invalid pair {}: must be var=value", var);
variables.insert(pair[0].to_string(), pair[1].to_string());
}
}
Ok(variables)
}
fn get_mappings(
vars: &HashMap<String, String>,
sources: &Option<Vec<&str>>,
destinations: &Option<Vec<&str>>,
out: &mut Vec<PathMappingGenerator>,
) -> common::Result<()> {
if sources.is_none() && destinations.is_none() {
return Ok(());
}
anyhow::ensure!(
sources.as_ref().map_or(0, |v| v.len()) == destinations.as_ref().map_or(0, |v| v.len()),
"mapping-src and mapping-dest must have the same number of elements"
);
let sources = sources.as_ref().unwrap();
let destinations = destinations.as_ref().unwrap();
for (src, dest) in sources.iter().zip(destinations.iter()) {
out.push(PathMappingGenerator::new(
src,
dest,
vars,
HashSet::default(),
)?);
}
Ok(())
}
fn get_mappings_from_file(
vars: &HashMap<String, String>,
file: &Option<&str>,
out: &mut Vec<PathMappingGenerator>,
) -> common::Result<()> {
if let Some(file) = file {
let mut mappings = Mappings::new(file)?;
for (k, v) in vars {
mappings.variables.insert(k.to_string(), v.to_string());
}
let mut no_files = Vec::new();
for Mapping {
source,
destination,
files,
} in mappings.mappings.drain(..)
{
if files.is_empty() {
no_files.push(PathMappingGenerator::new(
&source,
&destination,
&mappings.variables,
files,
)?);
} else {
out.push(PathMappingGenerator::new(
&source,
&destination,
&mappings.variables,
files,
)?);
}
}
for m in no_files.drain(..) {
out.push(m);
}
}
Ok(())
}
pub(crate) fn map<P: AsRef<Path>>(&self, file: P) -> common::Result<Option<String>> {
let file = file.as_ref();
let file_str = match file.to_str() {
Some(f) => f,
None => {
warn!(
"The path {} is not a valid UTF-8 string so cannot find a mapping for it.",
file.to_string_lossy()
);
return Ok(None);
}
};
#[cfg(windows)]
let file_str = file_str.strip_prefix(r"\\?\").unwrap_or(file_str);
{
let cache = self.cache.lock().unwrap();
if let Some(cached) = cache.get(file_str) {
return Ok(Some(cached.to_string()));
}
}
let mut res = None;
for mapping in self.mappings.iter() {
let mapping = mapping.apply(file, file_str)?;
if let Some(mapping) = mapping {
res = Some(mapping);
break;
}
}
if let Some(res) = res {
let mut cache = self.cache.lock().unwrap();
cache.insert(file_str.to_string(), res.clone());
return Ok(Some(res));
}
warn!("Cannot find a mapping for file {file_str}");
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_mapping_generator() {
let mut vars_map = HashMap::default();
vars_map.insert("rev".to_string(), "abcdef".to_string());
let p = PathMappingGenerator::new(
r"test_data/linux/(.*)",
"https://source/{rev}/{digest}/{1}/last",
&vars_map,
HashSet::default(),
)
.unwrap();
let file = "test_data/linux/basic.cpp";
let s = p.apply(Path::new(file), file).unwrap().unwrap();
assert_eq!(s, "https://source/abcdef/dec67d788155e1895ba4fd1a178ca595798964529aab6a17ea1ecff133499137fc67ebdcf0c768ffb4fb7ec4f1f0fcf558073ec8a3b23c1063d23d62cc76b37a/basic.cpp/last");
}
#[test]
fn test_path_mapping_file() {
let mappings = PathMappings::new(&None, &None, &None, &Some("./test_data/mapping.json"))
.unwrap()
.unwrap();
let files = vec![
("/home/worker/a/c/ddd.cpp", "hg:hg.mozilla.org/mozilla-central:a/c/ddd.cpp:6639deb894172375b05d6791f5f8c7d53ca79723"),
("./test_data/mapping/bbb.cpp", "s3:gecko-generated-sources:cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e/bbb.cpp"),
("/rustc/f3e1a954d2ead4e2fc197c7da7d71e6c61bad196/src/libcore/str/pattern.rs", "git:github.com/rust-lang/rust:src/libcore/str/pattern.rs:f3e1a954d2ead4e2fc197c7da7d71e6c61bad196"),
("./test_data/mapping/aaa.cpp", "s3:gecko-generated-sources:cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e/aaa.cpp"),
("/cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/print.rs", "https://doc.rs/0.3.40/backtrace/src/print.rs.html"),
("./test_data/mapping/ccc.cpp", "s3:gecko-generated-sources:cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e/ccc.cpp"),
];
for (path, expected) in files {
assert_eq!(mappings.map(path).unwrap().unwrap(), expected.to_string())
}
}
}