use miette::{Context, IntoDiagnostic, Result, miette};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct ResolvedPatch {
pub key: String,
#[allow(dead_code)]
pub name: String,
#[allow(dead_code)]
pub version: String,
#[allow(dead_code)]
pub path: PathBuf,
pub content: String,
}
impl ResolvedPatch {
pub fn content_hash(&self) -> String {
let mut h = Sha256::new();
h.update(self.content.as_bytes());
hex::encode(h.finalize())
}
}
fn is_safe_patch_rel(rel: &str) -> bool {
if rel.is_empty() || rel.contains('\0') {
return false;
}
let p = Path::new(rel);
if p.is_absolute() || p.has_root() {
return false;
}
if rel.len() >= 2 && rel.as_bytes()[1] == b':' {
return false;
}
p.components().all(|c| {
matches!(
c,
std::path::Component::Normal(_) | std::path::Component::CurDir
)
})
}
pub fn split_patch_key(key: &str) -> Result<(String, String)> {
let (name, ver) = if let Some(rest) = key.strip_prefix('@') {
let slash = rest
.find('/')
.ok_or_else(|| miette!("invalid patch key {key:?}: scoped name missing slash"))?;
let after = &rest[slash + 1..];
let at = after
.find('@')
.ok_or_else(|| miette!("invalid patch key {key:?}: missing version"))?;
let split = 1 + slash + 1 + at;
(&key[..split], &key[split + 1..])
} else {
let at = key
.find('@')
.ok_or_else(|| miette!("invalid patch key {key:?}: missing version"))?;
(&key[..at], &key[at + 1..])
};
if name.is_empty() || ver.is_empty() {
return Err(miette!("invalid patch key {key:?}"));
}
Ok((name.to_string(), ver.to_string()))
}
pub fn load_patches_for_linker(
cwd: &Path,
) -> Result<(aube_linker::Patches, BTreeMap<String, String>)> {
use std::sync::{Mutex, OnceLock};
type CachedShape = (aube_linker::Patches, BTreeMap<String, String>);
static CACHE: OnceLock<Mutex<std::collections::HashMap<PathBuf, CachedShape>>> =
OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
let key = cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf());
if let Ok(guard) = cache.lock()
&& let Some(hit) = guard.get(&key)
{
return Ok(hit.clone());
}
let resolved = load_patches(cwd)?;
let patches: aube_linker::Patches = resolved
.values()
.map(|p| (p.key.clone(), p.content.clone()))
.collect();
let hashes: BTreeMap<String, String> = resolved
.values()
.map(|p| (p.key.clone(), p.content_hash()))
.collect();
if let Ok(mut guard) = cache.lock() {
guard.insert(key, (patches.clone(), hashes.clone()));
}
Ok((patches, hashes))
}
pub fn load_patches(cwd: &Path) -> Result<BTreeMap<String, ResolvedPatch>> {
let mut entries: BTreeMap<String, String> = BTreeMap::new();
let manifest_path = cwd.join("package.json");
if manifest_path.exists() {
let manifest = aube_manifest::PackageJson::from_path(&manifest_path)
.map_err(miette::Report::new)
.wrap_err("failed to read package.json")?;
entries.extend(manifest.pnpm_patched_dependencies());
}
let ws_config = aube_manifest::workspace::WorkspaceConfig::load(cwd)
.map_err(miette::Report::new)
.wrap_err("failed to read pnpm-workspace.yaml")?;
entries.extend(ws_config.patched_dependencies);
let mut out = BTreeMap::new();
for (key, rel) in entries {
let (name, version) = split_patch_key(&key)?;
if !is_safe_patch_rel(&rel) {
return Err(miette!(
"refusing unsafe patch path for {key}: {rel:?} (absolute, UNC, or contains `..`)"
));
}
let path = cwd.join(&rel);
let content = std::fs::read_to_string(&path)
.into_diagnostic()
.map_err(|e| {
miette!(
"failed to read patch file {} for {key}: {e}",
path.display()
)
})?;
out.insert(
key.clone(),
ResolvedPatch {
key,
name,
version,
path,
content,
},
);
}
Ok(out)
}
pub fn upsert_patched_dependency(cwd: &Path, key: &str, rel_patch_path: &str) -> Result<PathBuf> {
use aube_manifest::workspace::ConfigWriteTarget;
match aube_manifest::workspace::config_write_target(cwd) {
ConfigWriteTarget::PackageJson => {
aube_manifest::workspace::edit_setting_map(cwd, "patchedDependencies", |map| {
map.insert(
key.to_string(),
serde_json::Value::String(rel_patch_path.to_string()),
);
})
.map_err(miette::Report::new)
.wrap_err("failed to write package.json")?;
Ok(cwd.join("package.json"))
}
ConfigWriteTarget::WorkspaceYaml(path) => {
aube_manifest::workspace::upsert_workspace_patched_dependency(
&path,
key,
rel_patch_path,
)
.map_err(miette::Report::new)
.wrap_err_with(|| format!("failed to write {}", path.display()))?;
Ok(path)
}
}
}
pub fn remove_patched_dependency(cwd: &Path, key: &str) -> Result<Vec<PathBuf>> {
let mut rewritten = Vec::new();
if let Some(ws_path) = aube_manifest::workspace::workspace_yaml_existing(cwd)
&& aube_manifest::workspace::remove_workspace_patched_dependency(&ws_path, key)
.map_err(miette::Report::new)
.wrap_err_with(|| format!("failed to write {}", ws_path.display()))?
{
rewritten.push(ws_path);
}
if aube_manifest::workspace::remove_setting_entry(cwd, "patchedDependencies", key)
.map_err(miette::Report::new)
.wrap_err("failed to write package.json")?
{
rewritten.push(cwd.join("package.json"));
}
Ok(rewritten)
}
pub fn read_patched_dependencies(cwd: &Path) -> Result<BTreeMap<String, String>> {
let mut out = read_package_json_patched_dependencies(cwd)?;
let ws_config = aube_manifest::workspace::WorkspaceConfig::load(cwd)
.map_err(miette::Report::new)
.wrap_err("failed to read workspace yaml")?;
out.extend(ws_config.patched_dependencies);
Ok(out)
}
fn read_package_json_patched_dependencies(cwd: &Path) -> Result<BTreeMap<String, String>> {
let manifest_path = cwd.join("package.json");
if !manifest_path.exists() {
return Ok(BTreeMap::new());
}
let manifest = aube_manifest::PackageJson::from_path(&manifest_path)
.map_err(miette::Report::new)
.wrap_err("failed to read package.json")?;
Ok(manifest.pnpm_patched_dependencies())
}
pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
std::fs::create_dir_all(dst)
.into_diagnostic()
.map_err(|e| miette!("failed to create {}: {e}", dst.display()))?;
for entry in std::fs::read_dir(src)
.into_diagnostic()
.map_err(|e| miette!("failed to read {}: {e}", src.display()))?
{
let entry = entry
.into_diagnostic()
.map_err(|e| miette!("failed to read entry under {}: {e}", src.display()))?;
let ty = entry
.file_type()
.into_diagnostic()
.map_err(|e| miette!("failed to stat {}: {e}", entry.path().display()))?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&from, &to)?;
} else if ty.is_symlink() {
continue;
} else {
std::fs::copy(&from, &to).into_diagnostic().map_err(|e| {
miette!("failed to copy {} -> {}: {e}", from.display(), to.display())
})?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_simple() {
let (n, v) = split_patch_key("is-positive@3.1.0").unwrap();
assert_eq!(n, "is-positive");
assert_eq!(v, "3.1.0");
}
#[test]
fn split_scoped() {
let (n, v) = split_patch_key("@babel/core@7.0.0").unwrap();
assert_eq!(n, "@babel/core");
assert_eq!(v, "7.0.0");
}
#[test]
fn split_missing_version_errors() {
assert!(split_patch_key("is-positive").is_err());
assert!(split_patch_key("@babel/core").is_err());
}
#[test]
fn upsert_writes_to_yaml_when_one_exists() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("package.json"), "{}\n").unwrap();
std::fs::write(
dir.path().join("pnpm-workspace.yaml"),
"packages:\n - 'pkgs/*'\n",
)
.unwrap();
let written =
upsert_patched_dependency(dir.path(), "a@1.0.0", "patches/a@1.0.0.patch").unwrap();
assert_eq!(written, dir.path().join("pnpm-workspace.yaml"));
}
#[test]
fn upsert_writes_to_package_json_when_no_yaml() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("package.json"), "{}\n").unwrap();
let written =
upsert_patched_dependency(dir.path(), "a@1.0.0", "patches/a@1.0.0.patch").unwrap();
assert_eq!(written, dir.path().join("package.json"));
}
#[test]
fn upsert_writes_to_aube_namespace_when_no_pnpm_in_manifest() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("package.json"), "{}\n").unwrap();
upsert_patched_dependency(dir.path(), "a@1.0.0", "patches/a@1.0.0.patch").unwrap();
let manifest = std::fs::read_to_string(dir.path().join("package.json")).unwrap();
assert!(
manifest.contains("\"aube\""),
"expected aube namespace, got:\n{manifest}"
);
assert!(
!manifest.contains("\"pnpm\""),
"should not introduce pnpm namespace, got:\n{manifest}"
);
assert!(manifest.contains("\"patchedDependencies\""));
}
#[test]
fn upsert_collapses_shadow_when_other_namespace_holds_stale_entry() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("package.json"),
"{\"aube\":{\"patchedDependencies\":{\"a@1.0.0\":\"patches/old.patch\"}},\"pnpm\":{\"someKey\":1}}\n",
)
.unwrap();
upsert_patched_dependency(dir.path(), "a@1.0.0", "patches/new.patch").unwrap();
let raw = std::fs::read_to_string(dir.path().join("package.json")).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
assert_eq!(
parsed["pnpm"]["patchedDependencies"]["a@1.0.0"],
"patches/new.patch"
);
assert!(parsed["aube"]["patchedDependencies"].is_null());
assert_eq!(parsed["pnpm"]["someKey"], 1);
}
#[test]
fn upsert_writes_to_pnpm_namespace_when_pnpm_already_present() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("package.json"),
"{\"pnpm\":{\"allowBuilds\":{\"esbuild\":true}}}\n",
)
.unwrap();
upsert_patched_dependency(dir.path(), "a@1.0.0", "patches/a@1.0.0.patch").unwrap();
let manifest = std::fs::read_to_string(dir.path().join("package.json")).unwrap();
assert!(manifest.contains("\"pnpm\""));
assert!(
!manifest.contains("\"aube\""),
"should not introduce aube namespace alongside pnpm: {manifest}"
);
let parsed: serde_json::Value = serde_json::from_str(&manifest).unwrap();
assert!(parsed["pnpm"]["patchedDependencies"]["a@1.0.0"].is_string());
}
#[test]
fn remove_returns_each_rewritten_file() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("package.json"),
"{\"pnpm\":{\"patchedDependencies\":{\"a@1.0.0\":\"patches/a@1.0.0.patch\"}}}\n",
)
.unwrap();
std::fs::write(
dir.path().join("pnpm-workspace.yaml"),
"patchedDependencies:\n \"a@1.0.0\": patches/a@1.0.0.patch\n",
)
.unwrap();
let rewritten = remove_patched_dependency(dir.path(), "a@1.0.0").unwrap();
let names: Vec<String> = rewritten
.iter()
.filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.collect();
assert_eq!(names, vec!["pnpm-workspace.yaml", "package.json"]);
}
#[test]
fn remove_returns_empty_when_neither_file_holds_key() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("package.json"), "{}\n").unwrap();
std::fs::write(
dir.path().join("pnpm-workspace.yaml"),
"packages:\n - 'pkgs/*'\n",
)
.unwrap();
let rewritten = remove_patched_dependency(dir.path(), "missing@9.9.9").unwrap();
assert!(rewritten.is_empty());
}
}