use bytes::Bytes;
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use parlov_core::{ProbeDefinition, Technique};
use crate::context::ScanContext;
use crate::types::{ProbePair, StrategyMetadata};
#[must_use]
pub fn substitute_url(template: &str, id: &str) -> String {
template.replacen("{id}", id, 1)
}
#[must_use]
pub fn substitute_body(template: Option<&str>, id: &str) -> Option<Bytes> {
template.map(|t| Bytes::from(t.replace("{id}", id)))
}
#[must_use]
pub fn build_pair(
ctx: &ScanContext,
method: Method,
baseline_headers: HeaderMap,
probe_headers: HeaderMap,
body: Option<Bytes>,
metadata: StrategyMetadata,
technique: Technique,
) -> ProbePair {
let baseline_url = substitute_url(&ctx.target, &ctx.baseline_id);
let probe_url = substitute_url(&ctx.target, &ctx.probe_id);
let baseline_body = body
.clone()
.or_else(|| substitute_body(ctx.body_template.as_deref(), &ctx.baseline_id));
let probe_body = body.or_else(|| substitute_body(ctx.body_template.as_deref(), &ctx.probe_id));
ProbePair {
baseline: ProbeDefinition {
url: baseline_url,
method: method.clone(),
headers: baseline_headers,
body: baseline_body,
},
probe: ProbeDefinition {
url: probe_url,
method,
headers: probe_headers,
body: probe_body,
},
canonical_baseline: None,
metadata,
technique,
chain_provenance: None,
}
}
#[must_use]
pub(crate) fn url_pair_specs_with_canonical(
baseline_url: &str,
probe_url: &str,
canonical_url: &str,
headers: &http::HeaderMap,
metadata: &crate::types::StrategyMetadata,
technique: &parlov_core::Technique,
) -> Vec<crate::types::ProbeSpec> {
let mut specs = Vec::with_capacity(2);
for method in [http::Method::GET, http::Method::HEAD] {
let canonical = parlov_core::ProbeDefinition {
url: canonical_url.to_owned(),
method: method.clone(),
headers: headers.clone(),
body: None,
};
let pair = crate::types::ProbePair {
baseline: parlov_core::ProbeDefinition {
url: baseline_url.to_owned(),
method: method.clone(),
headers: headers.clone(),
body: None,
},
probe: parlov_core::ProbeDefinition {
url: probe_url.to_owned(),
method,
headers: headers.clone(),
body: None,
},
canonical_baseline: Some(canonical),
metadata: metadata.clone(),
technique: *technique,
chain_provenance: None,
};
specs.push(crate::types::ProbeSpec::Pair(pair));
}
specs
}
#[must_use]
pub(crate) fn clone_headers_static(
base: &HeaderMap,
key: &'static str,
value: &'static str,
) -> HeaderMap {
let mut map = base.clone();
map.insert(
HeaderName::from_static(key),
HeaderValue::from_static(value),
);
map
}
#[must_use]
pub(crate) fn try_clone_headers_with(
base: &HeaderMap,
key: &'static str,
value: &str,
) -> Option<HeaderMap> {
let val = HeaderValue::from_str(value).ok()?;
let mut map = base.clone();
map.insert(HeaderName::from_static(key), val);
Some(map)
}
#[must_use]
pub(crate) fn url_pair_specs(
baseline_url: &str,
probe_url: &str,
headers: &http::HeaderMap,
metadata: &crate::types::StrategyMetadata,
technique: &parlov_core::Technique,
) -> Vec<crate::types::ProbeSpec> {
let mut specs = Vec::with_capacity(2);
for method in [http::Method::GET, http::Method::HEAD] {
let pair = crate::types::ProbePair {
baseline: parlov_core::ProbeDefinition {
url: baseline_url.to_owned(),
method: method.clone(),
headers: headers.clone(),
body: None,
},
probe: parlov_core::ProbeDefinition {
url: probe_url.to_owned(),
method,
headers: headers.clone(),
body: None,
},
canonical_baseline: None,
metadata: metadata.clone(),
technique: *technique,
chain_provenance: None,
};
specs.push(crate::types::ProbeSpec::Pair(pair));
}
specs
}
#[must_use]
pub(crate) fn garble_path_segment(target: &str) -> String {
let (url_part, query) = match target.find('?') {
Some(q) => (&target[..q], &target[q..]),
None => (target, ""),
};
let path_start = url_part.find("://").map_or(0, |i| {
i + 3
+ url_part[i + 3..]
.find('/')
.unwrap_or(url_part.len() - i - 3)
});
let garbled = garble_path(url_part, path_start);
format!("{garbled}{query}")
}
fn garble_path(url: &str, path_start: usize) -> String {
let path = &url[path_start..];
if let Some(rel) = path.find("/{id}") {
let id_seg_start = path_start + rel; let prefix = &url[..id_seg_start];
if let Some(parent_rel) = prefix[path_start..].rfind('/') {
let parent_pos = path_start + parent_rel;
return format!("{}/_parlov_no_route/0", &url[..parent_pos]);
}
return format!("{prefix}/_parlov_no_route");
}
if let Some(rel) = path.rfind('/') {
let slash_pos = path_start + rel;
return format!("{}/_parlov_no_route", &url[..slash_pos]);
}
url.replacen("{id}", "_parlov_no_route", 1)
}
#[must_use]
pub(crate) fn derive_probe_location(
target: &str,
baseline_id: &str,
probe_id: &str,
location: &str,
) -> Option<String> {
let id_pos = target.find("{id}")?;
let id_end = id_pos + baseline_id.len();
if location.get(..id_pos) != target.get(..id_pos) {
return None;
}
if location.get(id_pos..id_end) != Some(baseline_id) {
return None;
}
Some(format!(
"{}{}{}",
&location[..id_pos],
probe_id,
&location[id_end..]
))
}
#[must_use]
pub fn json_body(fields: &[(&str, &str)]) -> Bytes {
let map: serde_json::Map<String, serde_json::Value> = fields
.iter()
.map(|(k, v)| ((*k).to_owned(), serde_json::Value::String((*v).to_owned())))
.collect();
let vec = serde_json::to_vec(&map).expect("serializing string-keyed map is infallible");
Bytes::from(vec)
}
#[cfg(test)]
#[path = "util_tests.rs"]
mod tests;