use crate::LockedPackage;
use std::collections::BTreeMap;
pub(super) fn version_to_dep_path(name: &str, version: &str) -> String {
format!("{name}@{version}")
}
pub(super) fn dep_path_tail<'a>(dep_path: &'a str, name: &str) -> &'a str {
dep_path
.strip_prefix(&format!("{name}@"))
.unwrap_or(dep_path)
}
pub(super) fn peerless_dep_path(name: &str, value: &str) -> String {
version_to_dep_path(name, value.split('(').next().unwrap_or(value))
}
fn outer_paren_segments(s: &str) -> Vec<&str> {
let bytes = s.as_bytes();
let mut segments = Vec::new();
let mut i = 0;
while i < bytes.len() && bytes[i] != b'(' {
i += 1;
}
while i < bytes.len() {
if bytes[i] != b'(' {
i += 1;
continue;
}
let start = i;
let mut depth: i32 = 0;
while i < bytes.len() {
match bytes[i] {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
i += 1;
segments.push(&s[start..i]);
break;
}
}
_ => {}
}
i += 1;
}
if depth != 0 {
break;
}
}
segments
}
pub(super) fn rewrite_peer_suffix(s: &str, translate: &impl Fn(&str) -> Option<String>) -> String {
let Some(head_end) = s.find('(') else {
return s.to_string();
};
let segments = outer_paren_segments(&s[head_end..]);
if segments.is_empty() {
warn_unbalanced_peer_suffix(s);
return s.to_string();
}
let mut out = String::with_capacity(s.len());
out.push_str(&s[..head_end]);
for seg in segments {
out.push('(');
out.push_str(&rewrite_peer_reference(&seg[1..seg.len() - 1], translate));
out.push(')');
}
out
}
fn warn_unbalanced_peer_suffix(s: &str) {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_LOCKFILE_MALFORMED_PEER_SUFFIX,
dep_path = s,
"unbalanced parentheses in pnpm dep_path peer suffix; preserving the key verbatim"
);
}
fn rewrite_peer_reference(inner: &str, translate: &impl Fn(&str) -> Option<String>) -> String {
let Some(nested_start) = inner.find('(') else {
return translate(inner).unwrap_or_else(|| inner.to_string());
};
let segments = outer_paren_segments(&inner[nested_start..]);
if segments.is_empty() {
warn_unbalanced_peer_suffix(inner);
return inner.to_string();
}
let mut out = String::with_capacity(inner.len());
out.push_str(&inner[..nested_start]);
for seg in segments {
out.push('(');
out.push_str(&rewrite_peer_reference(&seg[1..seg.len() - 1], translate));
out.push(')');
}
out
}
pub(super) fn peerless_alias_target<'a>(
packages: &'a BTreeMap<String, LockedPackage>,
real_dep_path: &str,
) -> Option<&'a LockedPackage> {
let (real_name, real_version) = parse_dep_path(real_dep_path)?;
packages.get(&version_to_dep_path(&real_name, &real_version))
}
pub(super) fn parse_dep_path(dep_path: &str) -> Option<(String, String)> {
let s = dep_path.strip_prefix('/').unwrap_or(dep_path);
let at_idx = if s.starts_with('@') {
let after_scope = s.find('/')? + 1;
after_scope + s[after_scope..].find('@')?
} else {
s.find('@')?
};
let name = s[..at_idx].to_string();
let version_str = &s[at_idx + 1..];
let version = version_str
.split('(')
.next()
.unwrap_or(version_str)
.to_string();
Some((name, version))
}
pub(super) fn rewrite_snapshot_alias_deps(
deps: &mut BTreeMap<String, String>,
alias_remaps: &mut Vec<(String, String, String, String)>,
) {
for (dep_name, dep_value) in deps.iter_mut() {
let bare = dep_value.split('(').next().unwrap_or(dep_value);
let Some((real_name, resolved)) = parse_dep_path(bare) else {
continue;
};
if real_name == *dep_name {
continue;
}
let peer_suffix = dep_value.find('(').map(|i| &dep_value[i..]).unwrap_or("");
let alias_dep_path = format!("{dep_name}@{resolved}{peer_suffix}");
let real_dep_path = dep_value.clone();
alias_remaps.push((alias_dep_path, real_dep_path, dep_name.clone(), real_name));
*dep_value = format!("{resolved}{peer_suffix}");
}
}
#[cfg(test)]
mod rewrite_peer_suffix_tests {
use super::rewrite_peer_suffix;
fn translate(head: &str) -> Option<String> {
(head == "request@url+1ff5271859b51655")
.then(|| "request@https://codeload.github.com/owner/request/tar.gz/abc".to_string())
}
#[test]
fn no_suffix_is_unchanged() {
assert_eq!(
rewrite_peer_suffix("lodash@4.18.1", &translate),
"lodash@4.18.1"
);
assert_eq!(rewrite_peer_suffix("4.18.1", &translate), "4.18.1");
}
#[test]
fn flat_local_peer_renders_as_spec() {
assert_eq!(
rewrite_peer_suffix(
"request-promise-core@1.1.4(request@url+1ff5271859b51655)",
&translate
),
"request-promise-core@1.1.4(request@https://codeload.github.com/owner/request/tar.gz/abc)"
);
assert_eq!(
rewrite_peer_suffix("1.1.4(request@url+1ff5271859b51655)", &translate),
"1.1.4(request@https://codeload.github.com/owner/request/tar.gz/abc)"
);
}
#[test]
fn registry_peer_is_left_untouched() {
let s = "react-dom@18.2.0(react@18.2.0)";
assert_eq!(rewrite_peer_suffix(s, &translate), s);
}
#[test]
fn nested_suffix_translates_only_the_inner_local_peer() {
assert_eq!(
rewrite_peer_suffix(
"consumer@1.0.0(request-promise@4.2.6(request@url+1ff5271859b51655))",
&translate
),
"consumer@1.0.0(request-promise@4.2.6(request@https://codeload.github.com/owner/request/tar.gz/abc))"
);
}
#[test]
fn multiple_segments_each_handled_independently() {
assert_eq!(
rewrite_peer_suffix(
"pkg@1.0.0(react@18.2.0)(request@url+1ff5271859b51655)",
&translate
),
"pkg@1.0.0(react@18.2.0)(request@https://codeload.github.com/owner/request/tar.gz/abc)"
);
}
#[test]
fn unbalanced_parens_preserved_verbatim_not_dropped() {
let flat = "request-promise-core@1.1.4(request@bad";
assert_eq!(rewrite_peer_suffix(flat, &translate), flat);
let nested = "consumer@1.0.0(request-promise@4.2.6(request@bad)";
assert_eq!(rewrite_peer_suffix(nested, &translate), nested);
}
}