use std::path::{Path, PathBuf};
use crate::error::Error;
use crate::util;
#[derive(Debug, Clone)]
pub(crate) struct DualRoot {
pub(crate) workspace_root: PathBuf,
#[allow(dead_code)]
pub(crate) workspace_root_manifest: PathBuf,
pub(crate) member_root: PathBuf,
pub(crate) member_manifest: PathBuf,
pub(crate) workspace_member_context: Option<WorkspaceMemberContext>,
}
#[derive(Debug, Clone)]
pub struct WorkspaceMemberContext {
pub workspace_root_manifest: PathBuf,
pub workspace_root_value: toml::Value,
}
#[derive(Debug)]
pub struct OverlayPlan {
pub upstream_manifest: PathBuf,
pub sibling_manifest: PathBuf,
pub upstream_already_has_dylib: bool,
pub dropped_comments: Vec<String>,
pub upstream_crate_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SyntheticMetadata {
pub dylib_crate: String,
pub extern_crates: Vec<String>,
pub fixture_dirs: Vec<String>,
pub allow_lints: Vec<String>,
}
pub(crate) fn compat_default_synthetic_metadata(
name: &str,
fixture_dirs: Vec<String>,
) -> SyntheticMetadata {
SyntheticMetadata {
dylib_crate: name.to_string(),
extern_crates: vec![name.to_string()],
fixture_dirs,
allow_lints: vec!["unexpected_cfgs".to_string()],
}
}
pub fn materialize_overlay(upstream_manifest_path: &Path) -> Result<OverlayPlan, Error> {
materialize_overlay_with_metadata(upstream_manifest_path, None)
}
pub fn materialize_overlay_with_metadata(
upstream_manifest_path: &Path,
synthetic_metadata: Option<&SyntheticMetadata>,
) -> Result<OverlayPlan, Error> {
materialize_overlay_with_metadata_and_workspace_member_context(
upstream_manifest_path,
synthetic_metadata,
None,
)
}
pub fn materialize_overlay_with_metadata_and_workspace_member_context(
upstream_manifest_path: &Path,
synthetic_metadata: Option<&SyntheticMetadata>,
workspace_member_ctx: Option<&WorkspaceMemberContext>,
) -> Result<OverlayPlan, Error> {
materialize_overlay_inner(
upstream_manifest_path,
|_name| synthetic_metadata.cloned(),
workspace_member_ctx,
)
}
pub fn materialize_overlay_with_workspace_member_context<F>(
upstream_manifest_path: &Path,
builder: F,
workspace_member_ctx: Option<&WorkspaceMemberContext>,
) -> Result<OverlayPlan, Error>
where
F: FnOnce(Option<&str>) -> SyntheticMetadata,
{
materialize_overlay_inner(
upstream_manifest_path,
|name| Some(builder(name)),
workspace_member_ctx,
)
}
fn materialize_overlay_inner<F>(
upstream_manifest_path: &Path,
synthetic_metadata: F,
workspace_member_ctx: Option<&WorkspaceMemberContext>,
) -> Result<OverlayPlan, Error>
where
F: FnOnce(Option<&str>) -> Option<SyntheticMetadata>,
{
let raw_bytes = std::fs::read(upstream_manifest_path).map_err(|e| {
Error::io(
e,
"reading upstream Cargo.toml for overlay",
Some(upstream_manifest_path.to_path_buf()),
)
})?;
let raw_text = String::from_utf8(raw_bytes).map_err(|e| {
Error::io(
std::io::Error::new(std::io::ErrorKind::InvalidData, e),
"decoding upstream Cargo.toml as UTF-8",
Some(upstream_manifest_path.to_path_buf()),
)
})?;
let dropped_comments = scan_dropped_comments(&raw_text);
let mut value: toml::Value =
toml::from_str(&raw_text).map_err(|e: toml::de::Error| Error::TomlParse {
path: upstream_manifest_path.to_path_buf(),
message: e.to_string(),
})?;
if is_workspace_root_manifest(&value) {
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--compat-root` `{}` is a workspace root (declares `[workspace]` \
without `[package]`); pass `--package <pkg>` to target a specific workspace \
member, or set `--compat-root` to a single-crate Cargo.toml.",
upstream_manifest_path.display()
),
});
}
let upstream_already_has_dylib = inspect_existing_crate_type(&value);
let upstream_crate_name = read_upstream_crate_name(&value);
let synthetic = synthetic_metadata(upstream_crate_name.as_deref());
let upstream_dir: PathBuf = upstream_manifest_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let staged_overlay_dir: PathBuf = upstream_dir.join("target").join("lihaaf-overlay");
if let toml::Value::Table(top) = &mut value {
let lib_table = top
.entry("lib".to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
if let toml::Value::Table(lib) = lib_table {
canonicalize_crate_type(lib)?;
} else {
return Err(Error::TomlParse {
path: upstream_manifest_path.to_path_buf(),
message: "`[lib]` must be a table, not an inline value".to_string(),
});
}
absolutize_path_bearing_keys(top, &upstream_dir, upstream_manifest_path)?;
if let Some(ctx) = workspace_member_ctx {
apply_workspace_member_inheritance(top, ctx, upstream_manifest_path)?;
}
apply_self_patch_policy(
top,
upstream_crate_name.as_deref(),
&upstream_dir,
&staged_overlay_dir,
workspace_member_ctx,
)?;
if let Some(meta) = synthetic.as_ref() {
inject_synthetic_metadata(top, meta, upstream_manifest_path)?;
}
override_workspace_inheritance(top, upstream_manifest_path, workspace_member_ctx)?;
}
let serialized = serialize_canonical(&value)?;
let sibling_path = staged_overlay_dir.join("Cargo.toml");
let need_write = match std::fs::read(&sibling_path) {
Ok(existing) => existing != serialized,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
Err(e) => {
return Err(Error::io(
e,
"checking existing staged overlay for idempotent rerun",
Some(sibling_path.clone()),
));
}
};
if need_write {
util::write_file_atomic(&sibling_path, &serialized)?;
}
mirror_upstream_into_overlay(&upstream_dir, &staged_overlay_dir)?;
Ok(OverlayPlan {
upstream_manifest: upstream_manifest_path.to_path_buf(),
sibling_manifest: sibling_path,
upstream_already_has_dylib,
dropped_comments,
upstream_crate_name,
})
}
fn read_upstream_crate_name(value: &toml::Value) -> Option<String> {
value
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
}
const WORKSPACE_MEMBERSHIP_KEYS: &[&str] = &["members", "exclude", "default-members"];
fn override_workspace_inheritance(
top: &mut toml::map::Map<String, toml::Value>,
upstream_manifest_path: &Path,
workspace_member_ctx: Option<&WorkspaceMemberContext>,
) -> Result<(), Error> {
if let Some(toml::Value::Table(pkg)) = top.get("package")
&& pkg.contains_key("workspace")
{
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--compat-root` `{}` is a workspace member: \
`[package].workspace = \"...\"` declares membership in \
an ancestor workspace, which compat mode cannot reach. \
Compat mode currently supports only single-crate \
manifests and workspace-root manifests (where \
`[workspace]` lives in the same Cargo.toml). \
Pass the workspace-ROOT Cargo.toml as `--compat-root` \
instead; it will still resolve `{{ workspace = true }}` \
references in its own manifest because \
`[workspace.dependencies]` / `[workspace.package]` / \
`[workspace.lints]` are preserved in the staged overlay.",
upstream_manifest_path.display()
),
});
}
let has_local_workspace = top.get("workspace").is_some_and(|v| v.is_table());
if workspace_member_ctx.is_none()
&& !has_local_workspace
&& let Some(ancestor_manifest) = detect_implicit_ancestor_workspace(upstream_manifest_path)?
{
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--compat-root` `{}` is an implicit workspace member: \
it has no local `[workspace]` table but an ancestor manifest \
at `{}` carries `[workspace]`. Pass the workspace ROOT \
(`{}` or its containing directory) as `--compat-root` AND \
target this specific member with `--package <pkg-name>`, \
where `<pkg-name>` is the value of the member's \
`[package].name`. Cargo's baseline build walks up the \
filesystem and would apply the ancestor's `[patch]` / \
`[replace]` / `[profile]` / `resolver` / \
`[workspace.dependencies]` tables during dependency \
resolution; without `--package`, the lihaaf overlay \
terminates cargo's walk-up at the staged manifest and \
produces a divergent dependency graph between baseline \
and overlay — and therefore false compat verdicts. \
`--package` enables compat mode to carry the workspace \
root's tables down into the staged overlay so the \
dependency graphs converge.",
upstream_manifest_path.display(),
ancestor_manifest.display(),
ancestor_manifest.display(),
),
});
}
if workspace_member_ctx.is_none()
&& !has_local_workspace
&& manifest_has_inheritance_reference(top, upstream_manifest_path)?
{
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--compat-root` `{}` is an implicit workspace member: \
it has no local `[workspace]` table but uses workspace \
inheritance (one or more `{{ workspace = true }}` \
references in `[package]` / `[dependencies]` / \
`[dev-dependencies]` / `[build-dependencies]` / \
`[target.<cfg>.<deps>]` / `[lints]`). Cargo discovers \
the ancestor workspace by walking up the filesystem, \
but compat mode cannot reach into that ancestor to \
copy down the `[workspace.dependencies]` / \
`[workspace.package]` / `[workspace.lints]` tables \
the inheritance references resolve against. \
Pass the workspace-ROOT Cargo.toml as `--compat-root` \
AND target this member with `--package <pkg-name>`; \
the carry-down then populates the workspace tables.",
upstream_manifest_path.display()
),
});
}
let mut new_workspace = if let Some(toml::Value::Table(existing)) = top.get("workspace") {
let mut cloned = existing.clone();
for key in WORKSPACE_MEMBERSHIP_KEYS {
cloned.remove(*key);
}
cloned
} else {
toml::map::Map::new()
};
for key in WORKSPACE_MEMBERSHIP_KEYS {
new_workspace.remove(*key);
}
top.insert("workspace".to_string(), toml::Value::Table(new_workspace));
Ok(())
}
fn detect_implicit_ancestor_workspace(
upstream_manifest_path: &Path,
) -> Result<Option<PathBuf>, Error> {
let Some(manifest_dir) = upstream_manifest_path.parent() else {
return Ok(None);
};
let mut current = manifest_dir.parent();
while let Some(dir) = current {
let candidate = dir.join("Cargo.toml");
match std::fs::read_to_string(&candidate) {
Ok(text) => {
match toml::from_str::<toml::Value>(&text) {
Ok(value) => {
if value.get("workspace").is_some_and(|v| v.is_table()) {
return Ok(Some(candidate));
}
}
Err(e) => {
eprintln!(
"lihaaf: warning: skipping ancestor Cargo.toml `{}` during \
workspace detection: TOML parse error: {}",
candidate.display(),
e
);
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
}
Err(e) => {
return Err(Error::io(
e,
"reading ancestor Cargo.toml during workspace detection",
Some(candidate),
));
}
}
current = dir.parent();
}
Ok(None)
}
fn manifest_has_inheritance_reference(
top: &toml::map::Map<String, toml::Value>,
manifest_path: &Path,
) -> Result<bool, Error> {
let is_inheritance_table = |v: &toml::Value| -> bool {
v.as_table()
.and_then(|t| t.get("workspace"))
.and_then(|v| v.as_bool())
.unwrap_or(false)
};
let deps_table_has_inheritance = |scope: &toml::map::Map<String, toml::Value>,
key: &str,
scope_label: &str|
-> Result<bool, Error> {
match scope.get(key) {
None => Ok(false),
Some(toml::Value::Table(t)) => Ok(t.values().any(is_inheritance_table)),
Some(_) => Err(Error::TomlParse {
path: manifest_path.to_path_buf(),
message: format!("`{scope_label}{key}` must be a table; found a non-table value"),
}),
}
};
if let Some(toml::Value::Table(pkg)) = top.get("package") {
for (k, v) in pkg.iter() {
if k == "workspace" {
continue;
}
if is_inheritance_table(v) {
return Ok(true);
}
}
}
for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
if deps_table_has_inheritance(top, section, "[")? {
return Ok(true);
}
}
if let Some(toml::Value::Table(targets)) = top.get("target") {
for (cfg_name, cfg_value) in targets.iter() {
let Some(cfg_table) = cfg_value.as_table() else {
return Err(Error::TomlParse {
path: manifest_path.to_path_buf(),
message: format!(
"`[target.{cfg_name}]` must be a table; found a non-table value"
),
});
};
let scope_label = format!("[target.{cfg_name}].");
for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
if deps_table_has_inheritance(cfg_table, section, &scope_label)? {
return Ok(true);
}
}
}
}
if let Some(lints_value) = top.get("lints") {
let toml::Value::Table(lints) = lints_value else {
return Err(Error::TomlParse {
path: manifest_path.to_path_buf(),
message: "`[lints]` must be a table; found a non-table value".to_string(),
});
};
if lints
.get("workspace")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
return Ok(true);
}
if lints.values().any(is_inheritance_table) {
return Ok(true);
}
}
Ok(false)
}
pub fn resolve_workspace_member_manifest(
workspace_root_manifest: &Path,
package_name: &str,
) -> Result<(PathBuf, toml::Value), Error> {
let raw_bytes = std::fs::read(workspace_root_manifest).map_err(|e| {
Error::io(
e,
"reading workspace-root Cargo.toml for `--package` resolver",
Some(workspace_root_manifest.to_path_buf()),
)
})?;
let raw_text = String::from_utf8(raw_bytes).map_err(|e| {
Error::io(
std::io::Error::new(std::io::ErrorKind::InvalidData, e),
"decoding workspace-root Cargo.toml as UTF-8",
Some(workspace_root_manifest.to_path_buf()),
)
})?;
let value: toml::Value =
toml::from_str(&raw_text).map_err(|e: toml::de::Error| Error::TomlParse {
path: workspace_root_manifest.to_path_buf(),
message: e.to_string(),
})?;
if !is_workspace_root_manifest(&value) {
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--package <{package_name}>` requires `--compat-root` to point at a \
workspace root (a `Cargo.toml` declaring `[workspace]` without `[package]`); \
`{}` does not match this shape. Either drop `--package` and point `--compat-root` \
directly at the member's `Cargo.toml`, or fix `--compat-root` to the \
workspace-root directory.",
workspace_root_manifest.display()
),
});
}
let workspace_root_dir = workspace_root_manifest
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let members_array = value
.get("workspace")
.and_then(|w| w.get("members"))
.and_then(|m| m.as_array());
let Some(members_array) = members_array else {
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--package <{package_name}>` resolver: `{}` has `[workspace]` but no \
`[workspace.members]` array; cannot resolve `{package_name}`. Add the package \
to `[workspace.members]` or pass the member's manifest path directly via \
`--compat-manifest`.",
workspace_root_manifest.display()
),
});
};
let exclude_array = value
.get("workspace")
.and_then(|w| w.get("exclude"))
.and_then(|e| e.as_array());
let mut exclude_dirs: std::collections::BTreeSet<PathBuf> = std::collections::BTreeSet::new();
if let Some(arr) = exclude_array {
for entry in arr {
let Some(entry_str) = entry.as_str() else {
continue;
};
let normalized = entry_str.trim_end_matches('/');
if validate_workspace_member_entry(normalized, package_name).is_err() {
continue;
}
let expanded =
expand_workspace_member_entry(normalized, &workspace_root_dir, package_name)?;
for path in expanded {
exclude_dirs.insert(lexical_normalize_pathbuf(&path));
}
}
}
let mut candidate_dirs: std::collections::BTreeMap<PathBuf, ()> =
std::collections::BTreeMap::new();
let mut scanned_member_names: Vec<String> = Vec::new();
for entry in members_array {
let Some(entry_str) = entry.as_str() else {
return Err(Error::TomlParse {
path: workspace_root_manifest.to_path_buf(),
message: format!(
"`[workspace.members]` element is not a string; `--package <{package_name}>` \
resolver requires every member entry to be a path or glob string"
),
});
};
let normalized = entry_str.trim_end_matches('/');
validate_workspace_member_entry(normalized, package_name)?;
scanned_member_names.push(normalized.to_string());
let expanded =
expand_workspace_member_entry(normalized, &workspace_root_dir, package_name)?;
for path in expanded {
let canonical = lexical_normalize_pathbuf(&path);
if exclude_dirs.contains(&canonical) {
continue;
}
candidate_dirs.insert(canonical, ());
}
}
let mut matches: Vec<PathBuf> = Vec::new();
for candidate_dir in candidate_dirs.keys() {
let candidate_manifest = candidate_dir.join("Cargo.toml");
let text = match std::fs::read_to_string(&candidate_manifest) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
continue;
}
Err(e) => {
return Err(Error::io(
e,
"reading workspace-member Cargo.toml for `--package` resolver",
Some(candidate_manifest),
));
}
};
let parsed: toml::Value = match toml::from_str(&text) {
Ok(v) => v,
Err(e) => {
eprintln!(
"warning: skipping unparseable workspace-member `{}`: {}",
candidate_manifest.display(),
e
);
continue;
}
};
if is_workspace_root_manifest(&parsed) {
continue;
}
let candidate_name = parsed
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str());
if candidate_name == Some(package_name) {
matches.push(candidate_manifest);
}
}
match matches.len() {
0 => Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--package <{package_name}>` resolver: no member of workspace `{}` has \
`[package].name = \"{package_name}\"`. Members scanned: [{}]. Confirm \
`{package_name}` exists in `[workspace.members]` and its `Cargo.toml` declares \
the expected package name (if `{package_name}` is also in `[workspace.exclude]`, \
it was subtracted before scanning).",
workspace_root_manifest.display(),
scanned_member_names.join(", "),
),
}),
1 => Ok((matches.remove(0), value)),
_ => Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--package <{package_name}>` resolver: multiple workspace members claim \
`[package].name = \"{package_name}\"`: [{}]. Workspace package names must be \
unique. Inspect each manifest and resolve the duplicate.",
matches
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
),
}),
}
}
fn validate_workspace_member_entry(entry: &str, package_name: &str) -> Result<(), Error> {
let segments: Vec<&str> = entry.split('/').collect();
if Path::new(entry).is_absolute() {
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--package <{package_name}>` resolver: workspace member entry `{entry}` \
is absolute; `[workspace.members]` entries are workspace-relative paths only. \
Use a relative path."
),
});
}
if entry.contains("**") {
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--package <{package_name}>` resolver: workspace member entry `{entry}` \
uses `**` (deep glob); cargo does not support `**` in `[workspace.members]`. \
Use `*` (single-segment glob) or an explicit literal path instead."
),
});
}
if segments.contains(&"..") {
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--package <{package_name}>` resolver: workspace member entry `{entry}` \
uses `..` (parent traversal); members must be descendants of the workspace root. \
Use a relative path within the workspace."
),
});
}
let has_glob_chars = |s: &str| s.bytes().any(|b| matches!(b, b'*' | b'?' | b'['));
let non_empty: Vec<&&str> = segments.iter().filter(|s| !s.is_empty()).collect();
if non_empty.len() > 1 {
for seg in &non_empty[..non_empty.len() - 1] {
if has_glob_chars(seg) {
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--package <{package_name}>` resolver: workspace member entry \
`{entry}` uses a glob in a non-final path segment; only the LAST segment \
may contain glob metachars (`*`, `?`, `[...]`). Use a literal parent \
path or split into multiple entries."
),
});
}
}
}
Ok(())
}
fn expand_workspace_member_entry(
entry: &str,
workspace_root_dir: &Path,
package_name: &str,
) -> Result<Vec<PathBuf>, Error> {
let segments: Vec<&str> = entry
.split('/')
.filter(|s| !s.is_empty() && *s != ".")
.collect();
if segments.is_empty() {
return Ok(Vec::new());
}
let has_glob_chars = |s: &str| s.bytes().any(|b| matches!(b, b'*' | b'?' | b'['));
let last = segments.last().expect("non-empty after filter");
let parent_segments: Vec<&str> = segments[..segments.len() - 1].to_vec();
let mut parent_dir = workspace_root_dir.to_path_buf();
for seg in &parent_segments {
parent_dir.push(seg);
}
if has_glob_chars(last) {
let read_dir_result = match std::fs::read_dir(&parent_dir) {
Ok(it) => it,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => {
return Err(Error::io(
e,
"reading workspace-member parent dir for `--package` glob expansion",
Some(parent_dir),
));
}
};
let pattern_bytes = last.as_bytes();
let mut matches: Vec<PathBuf> = Vec::new();
for child_res in read_dir_result {
let child = child_res.map_err(|e| {
Error::io(
e,
"iterating workspace-member parent dir for `--package` glob expansion",
Some(parent_dir.clone()),
)
})?;
let name_os = child.file_name();
let Some(name_str) = name_os.to_str() else {
continue;
};
let Ok(file_type) = child.file_type() else {
continue;
};
if !file_type.is_dir() {
continue;
}
if super::discovery::glob_segment_matches(pattern_bytes, name_str.as_bytes()) {
matches.push(lexical_normalize_pathbuf(&child.path()));
}
}
matches.sort();
let _ = package_name;
Ok(matches)
} else {
let mut joined = parent_dir;
joined.push(last);
let _ = package_name;
Ok(vec![lexical_normalize_pathbuf(&joined)])
}
}
fn apply_workspace_member_inheritance(
top: &mut toml::map::Map<String, toml::Value>,
ctx: &WorkspaceMemberContext,
member_manifest: &Path,
) -> Result<(), Error> {
if let Some(patch_value) = top.get("patch") {
let toml::Value::Table(patch) = patch_value else {
return Err(Error::TomlParse {
path: member_manifest.to_path_buf(),
message: "`[patch]` must be a table; found a non-table value".to_string(),
});
};
for (registry, registry_value) in patch.iter() {
let toml::Value::Table(registry_table) = registry_value else {
return Err(Error::TomlParse {
path: member_manifest.to_path_buf(),
message: format!(
"`[patch.{registry}]` must be a table; found a non-table value"
),
});
};
if !registry_table.is_empty() {
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--package` resolver: workspace member `{}` declares \
`[patch.{registry}]`; cargo does not permit `[patch]` in workspace \
members (only the workspace root). Move the patch entries to the \
workspace root's `[patch.{registry}]` or remove them.",
member_manifest.display()
),
});
}
}
}
let workspace_root_dir = ctx
.workspace_root_manifest
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let absolutize_against_ws_root = |s: &str| -> String {
let p = Path::new(s);
if p.is_absolute() {
crate::util::to_forward_slash(&p.to_string_lossy())
} else {
crate::util::to_forward_slash(&workspace_root_dir.join(p).to_string_lossy())
}
};
let Some(ws_root_top) = ctx.workspace_root_value.as_table() else {
return Err(Error::TomlParse {
path: ctx.workspace_root_manifest.clone(),
message: "workspace-root TOML value is not a table".to_string(),
});
};
let mut carried_workspace: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let Some(ws_root_workspace) = ws_root_top.get("workspace").and_then(|v| v.as_table()) else {
return Err(Error::TomlParse {
path: ctx.workspace_root_manifest.clone(),
message: "workspace-root has no `[workspace]` table despite passing the predicate"
.to_string(),
});
};
for (key, value) in ws_root_workspace.iter() {
match key.as_str() {
"members" | "exclude" | "default-members" => continue,
"dependencies" => {
let toml::Value::Table(deps) = value else {
return Err(Error::TomlParse {
path: ctx.workspace_root_manifest.clone(),
message:
"`[workspace.dependencies]` must be a table; found a non-table value"
.to_string(),
});
};
let mut carried_deps: toml::map::Map<String, toml::Value> = toml::map::Map::new();
for (dep_name, dep_value) in deps.iter() {
if let Some(dep_table) = dep_value.as_table() {
let mut carried_dep = dep_table.clone();
if let Some(s) = carried_dep.get("path").and_then(|v| v.as_str()) {
let abs = absolutize_against_ws_root(s);
carried_dep.insert("path".to_string(), toml::Value::String(abs));
}
carried_deps.insert(dep_name.clone(), toml::Value::Table(carried_dep));
} else {
carried_deps.insert(dep_name.clone(), dep_value.clone());
}
}
carried_workspace
.insert("dependencies".to_string(), toml::Value::Table(carried_deps));
}
"package" => {
let toml::Value::Table(pkg_table) = value else {
return Err(Error::TomlParse {
path: ctx.workspace_root_manifest.clone(),
message: "`[workspace.package]` must be a table; found a non-table value"
.to_string(),
});
};
let mut carried_pkg = pkg_table.clone();
for path_key in &["readme", "license-file"] {
if let Some(s) = carried_pkg.get(*path_key).and_then(|v| v.as_str()) {
let abs = absolutize_against_ws_root(s);
carried_pkg.insert((*path_key).to_string(), toml::Value::String(abs));
}
}
carried_workspace.insert("package".to_string(), toml::Value::Table(carried_pkg));
}
_ => {
carried_workspace.insert(key.clone(), value.clone());
}
}
}
let workspace_entry = top
.entry("workspace".to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
if let toml::Value::Table(member_workspace) = workspace_entry {
for (key, value) in carried_workspace {
member_workspace.entry(key).or_insert(value);
}
} else {
return Err(Error::TomlParse {
path: member_manifest.to_path_buf(),
message: "member manifest's `[workspace]` is not a table; cannot carry down \
workspace-root inheritance"
.to_string(),
});
}
if let Some(replace_value) = ws_root_top.get("replace") {
let toml::Value::Table(ws_replace) = replace_value else {
return Err(Error::TomlParse {
path: ctx.workspace_root_manifest.clone(),
message: "`[replace]` must be a table; found a non-table value".to_string(),
});
};
let mut carried_replace: toml::map::Map<String, toml::Value> = toml::map::Map::new();
for (source_id, replace_value) in ws_replace.iter() {
if let Some(replace_table) = replace_value.as_table() {
let mut carried = replace_table.clone();
if let Some(s) = carried.get("path").and_then(|v| v.as_str()) {
let abs = absolutize_against_ws_root(s);
carried.insert("path".to_string(), toml::Value::String(abs));
}
carried_replace.insert(source_id.clone(), toml::Value::Table(carried));
} else {
carried_replace.insert(source_id.clone(), replace_value.clone());
}
}
if !carried_replace.is_empty() {
top.insert("replace".to_string(), toml::Value::Table(carried_replace));
}
}
if let Some(profile_value) = ws_root_top.get("profile") {
let toml::Value::Table(ws_profile) = profile_value else {
return Err(Error::TomlParse {
path: ctx.workspace_root_manifest.clone(),
message: "`[profile]` must be a table; found a non-table value".to_string(),
});
};
if !ws_profile.is_empty() {
top.insert(
"profile".to_string(),
toml::Value::Table(ws_profile.clone()),
);
}
}
Ok(())
}
fn inject_synthetic_metadata(
top: &mut toml::map::Map<String, toml::Value>,
meta: &SyntheticMetadata,
manifest_path: &Path,
) -> Result<(), Error> {
let package_entry = top
.entry("package".to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
let toml::Value::Table(package) = package_entry else {
return Err(Error::TomlParse {
path: manifest_path.to_path_buf(),
message: "`[package]` must be a table; found a non-table value".to_string(),
});
};
let metadata_entry = package
.entry("metadata".to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
let toml::Value::Table(metadata) = metadata_entry else {
return Err(Error::TomlParse {
path: manifest_path.to_path_buf(),
message: "`[package.metadata]` must be a table; found a non-table value".to_string(),
});
};
let mut lihaaf_table = toml::map::Map::new();
lihaaf_table.insert(
"dylib_crate".to_string(),
toml::Value::String(meta.dylib_crate.clone()),
);
lihaaf_table.insert(
"extern_crates".to_string(),
toml::Value::Array(
meta.extern_crates
.iter()
.cloned()
.map(toml::Value::String)
.collect(),
),
);
lihaaf_table.insert(
"fixture_dirs".to_string(),
toml::Value::Array(
meta.fixture_dirs
.iter()
.cloned()
.map(toml::Value::String)
.collect(),
),
);
lihaaf_table.insert(
"allow_lints".to_string(),
toml::Value::Array(
meta.allow_lints
.iter()
.cloned()
.map(toml::Value::String)
.collect(),
),
);
metadata.insert("lihaaf".to_string(), toml::Value::Table(lihaaf_table));
Ok(())
}
fn absolutize_path_bearing_keys(
top: &mut toml::map::Map<String, toml::Value>,
upstream_dir: &Path,
manifest_path: &Path,
) -> Result<(), Error> {
let to_abs_string = |relative: &str| -> String {
let joined = upstream_dir.join(relative);
crate::util::to_forward_slash(&joined.to_string_lossy())
};
let absolutize_string_at = |table: &mut toml::map::Map<String, toml::Value>,
key: &str,
upstream_dir: &Path| {
if let Some(toml::Value::String(s)) = table.get(key) {
let p = Path::new(s);
if !p.is_absolute() {
let abs = crate::util::to_forward_slash(&upstream_dir.join(p).to_string_lossy());
table.insert(key.to_string(), toml::Value::String(abs));
}
}
};
let absolutize_array_table_paths =
|top: &mut toml::map::Map<String, toml::Value>, section: &str, upstream_dir: &Path| {
if let Some(toml::Value::Array(entries)) = top.get_mut(section) {
for entry in entries.iter_mut() {
if let toml::Value::Table(t) = entry {
absolutize_string_at(t, "path", upstream_dir);
}
}
}
};
let absolutize_deps_paths = |top: &mut toml::map::Map<String, toml::Value>,
section: &str,
scope_label: &str,
upstream_dir: &Path,
manifest_path: &Path|
-> Result<(), Error> {
match top.get_mut(section) {
None => Ok(()),
Some(toml::Value::Table(deps)) => {
for (_name, dep) in deps.iter_mut() {
if let toml::Value::Table(t) = dep {
absolutize_string_at(t, "path", upstream_dir);
}
}
Ok(())
}
Some(_) => Err(Error::TomlParse {
path: manifest_path.to_path_buf(),
message: format!(
"`{scope_label}{section}` must be a table; found a non-table value"
),
}),
}
};
if let Some(toml::Value::Table(lib)) = top.get_mut("lib") {
let needs_inject = !lib.contains_key("path");
if needs_inject {
lib.insert(
"path".to_string(),
toml::Value::String(to_abs_string("src/lib.rs")),
);
} else {
absolutize_string_at(lib, "path", upstream_dir);
}
}
let upstream_build_rs = upstream_dir.join("build.rs");
if let Some(toml::Value::Table(pkg)) = top.get_mut("package") {
if pkg.contains_key("build") {
absolutize_string_at(pkg, "build", upstream_dir);
} else if upstream_build_rs.is_file() {
pkg.insert(
"build".to_string(),
toml::Value::String(to_abs_string("build.rs")),
);
}
}
absolutize_array_table_paths(top, "bin", upstream_dir);
absolutize_array_table_paths(top, "example", upstream_dir);
absolutize_array_table_paths(top, "test", upstream_dir);
absolutize_array_table_paths(top, "bench", upstream_dir);
if let Some(toml::Value::Table(pkg)) = top.get_mut("package") {
pkg.insert("autobins".to_string(), toml::Value::Boolean(false));
pkg.insert("autoexamples".to_string(), toml::Value::Boolean(false));
pkg.insert("autotests".to_string(), toml::Value::Boolean(false));
pkg.insert("autobenches".to_string(), toml::Value::Boolean(false));
}
absolutize_deps_paths(top, "dependencies", "[", upstream_dir, manifest_path)?;
absolutize_deps_paths(top, "dev-dependencies", "[", upstream_dir, manifest_path)?;
absolutize_deps_paths(top, "build-dependencies", "[", upstream_dir, manifest_path)?;
if let Some(toml::Value::Table(targets)) = top.get_mut("target") {
for (cfg_name, cfg_value) in targets.iter_mut() {
let toml::Value::Table(cfg_table) = cfg_value else {
return Err(Error::TomlParse {
path: manifest_path.to_path_buf(),
message: format!(
"`[target.{cfg_name}]` must be a table; found a non-table value"
),
});
};
let scope_label = format!("[target.{cfg_name}].");
absolutize_deps_paths(
cfg_table,
"dependencies",
&scope_label,
upstream_dir,
manifest_path,
)?;
absolutize_deps_paths(
cfg_table,
"dev-dependencies",
&scope_label,
upstream_dir,
manifest_path,
)?;
absolutize_deps_paths(
cfg_table,
"build-dependencies",
&scope_label,
upstream_dir,
manifest_path,
)?;
}
}
if let Some(toml::Value::Table(ws)) = top.get_mut("workspace") {
for key in ["members", "exclude"] {
if let Some(toml::Value::Array(arr)) = ws.get_mut(key) {
for entry in arr.iter_mut() {
if let toml::Value::String(s) = entry {
let p = Path::new(s.as_str());
if !p.is_absolute() {
let joined = upstream_dir.join(p);
let canonical = lexical_normalize_pathbuf(&joined);
let abs = crate::util::to_forward_slash(&canonical.to_string_lossy());
*entry = toml::Value::String(abs);
}
}
}
}
}
if let Some(toml::Value::Array(arr)) = ws.get_mut("default-members") {
for entry in arr.iter_mut() {
if let toml::Value::String(s) = entry {
let p = Path::new(s.as_str());
if !p.is_absolute() {
let joined = upstream_dir.join(p);
let canonical = lexical_normalize_pathbuf(&joined);
let abs = crate::util::to_forward_slash(&canonical.to_string_lossy());
*entry = toml::Value::String(abs);
}
}
}
}
absolutize_deps_paths(
ws,
"dependencies",
"[workspace.",
upstream_dir,
manifest_path,
)?;
}
if let Some(toml::Value::Table(pkg)) = top.get_mut("package") {
absolutize_string_at(pkg, "workspace", upstream_dir);
}
absolutize_patch_paths(top, upstream_dir, manifest_path)?;
absolutize_replace_paths(top, upstream_dir, manifest_path)?;
Ok(())
}
fn absolutize_patch_paths(
top: &mut toml::map::Map<String, toml::Value>,
upstream_dir: &Path,
manifest_path: &Path,
) -> Result<(), Error> {
let patch = match top.get_mut("patch") {
None => return Ok(()),
Some(toml::Value::Table(t)) => t,
Some(_) => {
return Err(Error::TomlParse {
path: manifest_path.to_path_buf(),
message: "`[patch]` must be a table; found a non-table value".to_string(),
});
}
};
for (_registry, registry_value) in patch.iter_mut() {
if let toml::Value::Table(registry_table) = registry_value {
for (_krate, krate_value) in registry_table.iter_mut() {
if let toml::Value::Table(krate_table) = krate_value {
let needs_rewrite = krate_table
.get("path")
.and_then(|v| v.as_str())
.is_some_and(|s| !Path::new(s).is_absolute());
if needs_rewrite {
let s = krate_table
.get("path")
.and_then(|v| v.as_str())
.expect("needs_rewrite implies path exists");
let abs =
crate::util::to_forward_slash(&upstream_dir.join(s).to_string_lossy());
krate_table.insert("path".to_string(), toml::Value::String(abs));
}
}
}
}
}
Ok(())
}
fn absolutize_replace_paths(
top: &mut toml::map::Map<String, toml::Value>,
upstream_dir: &Path,
manifest_path: &Path,
) -> Result<(), Error> {
let replace = match top.get_mut("replace") {
None => return Ok(()),
Some(toml::Value::Table(t)) => t,
Some(_) => {
return Err(Error::TomlParse {
path: manifest_path.to_path_buf(),
message: "`[replace]` must be a table; found a non-table value".to_string(),
});
}
};
for (_source_id, entry_value) in replace.iter_mut() {
if let toml::Value::Table(entry_table) = entry_value {
let needs_rewrite = entry_table
.get("path")
.and_then(|v| v.as_str())
.is_some_and(|s| !Path::new(s).is_absolute());
if needs_rewrite {
let s = entry_table
.get("path")
.and_then(|v| v.as_str())
.expect("needs_rewrite implies path exists");
let abs = crate::util::to_forward_slash(&upstream_dir.join(s).to_string_lossy());
entry_table.insert("path".to_string(), toml::Value::String(abs));
}
}
}
Ok(())
}
fn lexical_path_normalize_path(p: &Path) -> Vec<std::path::Component<'_>> {
p.components()
.filter(|c| !matches!(c, std::path::Component::CurDir))
.collect()
}
fn lexical_normalize_pathbuf(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for c in lexical_path_normalize_path(p) {
out.push(c.as_os_str());
}
if out.as_os_str().is_empty() {
PathBuf::from(".")
} else {
out
}
}
fn apply_self_patch_policy(
top: &mut toml::map::Map<String, toml::Value>,
upstream_crate_name: Option<&str>,
upstream_dir: &Path,
staged_overlay_dir: &Path,
workspace_member_ctx: Option<&WorkspaceMemberContext>,
) -> Result<(), Error> {
let Some(self_name) = upstream_crate_name else {
return Ok(());
};
if self_name.is_empty() {
return Ok(());
}
let staged_overlay_abs = crate::util::to_forward_slash(&staged_overlay_dir.to_string_lossy());
let patch_entry = top
.entry("patch".to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
let toml::Value::Table(patch) = patch_entry else {
return Err(Error::TomlParse {
path: PathBuf::from("<overlay>"),
message: "`[patch]` must be a table".to_string(),
});
};
if let Some(ctx) = workspace_member_ctx {
let workspace_root_dir = ctx
.workspace_root_manifest
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let ws_root_top = ctx.workspace_root_value.as_table().ok_or_else(|| {
Error::TomlParse {
path: ctx.workspace_root_manifest.clone(),
message: "workspace-root TOML value is not a table".to_string(),
}
})?;
let ws_patch_opt = match ws_root_top.get("patch") {
Some(toml::Value::Table(t)) => Some(t),
None => None,
Some(_) => {
return Err(Error::TomlParse {
path: ctx.workspace_root_manifest.clone(),
message: "workspace-root `[patch]` must be a table; found a non-table value"
.to_string(),
});
}
};
if let Some(ws_patch) = ws_patch_opt {
#[allow(clippy::type_complexity)]
let mut carry: Vec<(String, String, toml::Value)> = Vec::new();
for (registry, registry_value) in ws_patch.iter() {
let toml::Value::Table(ws_registry_table) = registry_value else {
return Err(Error::TomlParse {
path: ctx.workspace_root_manifest.clone(),
message: format!(
"workspace-root `[patch.{registry}]` must be a table; found a \
non-table value"
),
});
};
for (name, value) in ws_registry_table.iter() {
let absolutized = match value {
toml::Value::Table(t) => {
let mut carried = t.clone();
if let Some(s) = carried.get("path").and_then(|v| v.as_str()) {
let p = Path::new(s);
let abs = if p.is_absolute() {
crate::util::to_forward_slash(&p.to_string_lossy())
} else {
crate::util::to_forward_slash(
&workspace_root_dir.join(p).to_string_lossy(),
)
};
carried.insert("path".to_string(), toml::Value::String(abs));
}
toml::Value::Table(carried)
}
other => {
other.clone()
}
};
carry.push((registry.clone(), name.clone(), absolutized));
}
}
for (registry, name, value) in carry {
let registry_entry = patch
.entry(registry)
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
let registry_table = match registry_entry {
toml::Value::Table(t) => t,
other => {
*other = toml::Value::Table(toml::map::Map::new());
match other {
toml::Value::Table(t) => t,
_ => unreachable!("just wrote a Table above"),
}
}
};
registry_table.insert(name, value);
}
}
}
let crates_io_entry = patch
.entry("crates-io".to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
let toml::Value::Table(crates_io) = crates_io_entry else {
return Err(Error::TomlParse {
path: PathBuf::from("<overlay>"),
message: "`[patch.crates-io]` must be a table".to_string(),
});
};
match crates_io.get(self_name).cloned() {
None => {
let mut entry = toml::map::Map::new();
entry.insert("path".to_string(), toml::Value::String(staged_overlay_abs));
crates_io.insert(self_name.to_string(), toml::Value::Table(entry));
Ok(())
}
Some(toml::Value::Table(existing_entry)) => {
let has_git = existing_entry.contains_key("git");
let has_branch = existing_entry.contains_key("branch");
let has_tag = existing_entry.contains_key("tag");
let has_rev = existing_entry.contains_key("rev");
let any_git_keys = has_git || has_branch || has_tag || has_rev;
let path_raw = existing_entry.get("path").and_then(|v| v.as_str());
if let Some(path_raw) = path_raw
&& !any_git_keys
{
let fire_remap = if workspace_member_ctx.is_some() {
true
} else {
let joined = upstream_dir.join(path_raw);
let joined_normalized = lexical_path_normalize_path(&joined);
let upstream_normalized = lexical_path_normalize_path(upstream_dir);
joined_normalized == upstream_normalized
};
if fire_remap {
let mut entry = toml::map::Map::new();
entry.insert("path".to_string(), toml::Value::String(staged_overlay_abs));
crates_io.insert(self_name.to_string(), toml::Value::Table(entry));
return Ok(());
}
}
Err(Error::CompatPatchOverrideConflict {
crate_name: self_name.to_string(),
upstream_entry: format!("{:?}", toml::Value::Table(existing_entry)),
expected_resolution: format!(
"lihaaf would inject [patch.crates-io.{self_name}] = \
{{ path = \"{staged_overlay_abs}\" }} (Rule 1 INJECT) \
or remap an upstream self-patch to that path (Rule 2 \
REMAP), but the upstream's existing entry declares an \
external target (vendored fork, git source, or non-root \
path). This combination is not currently supported; \
open an issue with the manifest shape if you need it."
),
})
}
Some(other) => Err(Error::CompatPatchOverrideConflict {
crate_name: self_name.to_string(),
upstream_entry: format!("{other:?}"),
expected_resolution: format!(
"lihaaf would inject [patch.crates-io.{self_name}] = \
{{ path = \"{staged_overlay_abs}\" }} (Rule 1 INJECT), \
but the upstream's existing entry is not a table — cargo \
requires `[patch.crates-io.<X>] = {{ ... }}`."
),
}),
}
}
const MIRROR_EXCLUDED_TOP_LEVEL: &[&str] = &["target", ".git", "Cargo.toml", "Cargo.lock"];
const MIRROR_MUST_REMOVE_IF_PRESENT: &[&str] = &[".git", "Cargo.lock"];
fn mirror_upstream_into_overlay(
upstream_dir: &Path,
staged_overlay_dir: &Path,
) -> Result<(), Error> {
let upstream_entries = std::fs::read_dir(upstream_dir).map_err(|e| {
Error::overlay_mirror_failed(
upstream_dir.to_path_buf(),
staged_overlay_dir.to_path_buf(),
"read-upstream-dir",
Some(e),
)
})?;
let mut upstream_names: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for entry_res in upstream_entries {
let entry = entry_res.map_err(|e| {
Error::overlay_mirror_failed(
upstream_dir.to_path_buf(),
staged_overlay_dir.to_path_buf(),
"iter-upstream-dir",
Some(e),
)
})?;
let name_os = entry.file_name();
let Some(name) = name_os.to_str() else {
continue;
};
upstream_names.insert(name.to_string());
if MIRROR_EXCLUDED_TOP_LEVEL.contains(&name) {
continue;
}
let upstream_path = upstream_dir.join(name);
let staged_path = staged_overlay_dir.join(name);
reconcile_one_entry(&upstream_path, &staged_path)?;
}
if staged_overlay_dir.is_dir() {
let staged_iter = std::fs::read_dir(staged_overlay_dir).map_err(|e| {
Error::overlay_mirror_failed(
staged_overlay_dir.to_path_buf(),
staged_overlay_dir.to_path_buf(),
"read-staged-dir",
Some(e),
)
})?;
for entry_res in staged_iter {
let entry = entry_res.map_err(|e| {
Error::overlay_mirror_failed(
staged_overlay_dir.to_path_buf(),
staged_overlay_dir.to_path_buf(),
"iter-staged-dir",
Some(e),
)
})?;
let name_os = entry.file_name();
let Some(name) = name_os.to_str() else {
continue;
};
if name == "Cargo.toml" || name == "target" {
continue;
}
if MIRROR_MUST_REMOVE_IF_PRESENT.contains(&name) {
let stale = staged_overlay_dir.join(name);
remove_path_any(&stale).map_err(|e| {
Error::overlay_mirror_failed(
upstream_dir.join(name),
stale.clone(),
"stale-cleanup-must-absent",
Some(e),
)
})?;
continue;
}
if !upstream_names.contains(name) {
let stale = staged_overlay_dir.join(name);
remove_path_any(&stale).map_err(|e| {
Error::overlay_mirror_failed(
upstream_dir.join(name),
stale.clone(),
"stale-cleanup-orphan",
Some(e),
)
})?;
}
}
}
let manifest = staged_overlay_dir.join("Cargo.toml");
let meta = std::fs::symlink_metadata(&manifest).map_err(|e| {
Error::overlay_mirror_failed(
upstream_dir.join("Cargo.toml"),
manifest.clone(),
"post-condition-stat",
Some(e),
)
})?;
if meta.file_type().is_symlink() {
return Err(Error::overlay_mirror_failed(
upstream_dir.join("Cargo.toml"),
manifest.clone(),
"post-condition-cargo-toml-is-symlink",
None,
));
}
if !meta.file_type().is_file() {
return Err(Error::overlay_mirror_failed(
upstream_dir.join("Cargo.toml"),
manifest.clone(),
"post-condition-cargo-toml-not-regular-file",
None,
));
}
Ok(())
}
fn reconcile_one_entry(upstream_path: &Path, staged_path: &Path) -> Result<(), Error> {
let mirror_err = |stage: &str, e: std::io::Error| {
Error::overlay_mirror_failed(
upstream_path.to_path_buf(),
staged_path.to_path_buf(),
stage.to_string(),
Some(e),
)
};
let staged_meta = std::fs::symlink_metadata(staged_path);
match staged_meta {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
create_canonical_mirror(upstream_path, staged_path)
}
Err(e) => Err(mirror_err("stat-staged", e)),
Ok(meta) => {
let ftype = meta.file_type();
if ftype.is_symlink() {
let link_target = std::fs::read_link(staged_path)
.map_err(|e| mirror_err("readlink-staged", e))?;
if link_target == upstream_path {
return Ok(());
}
std::fs::remove_file(staged_path)
.map_err(|e| mirror_err("stale-symlink-unlink", e))?;
create_canonical_mirror(upstream_path, staged_path)
} else if ftype.is_file() {
std::fs::remove_file(staged_path)
.map_err(|e| mirror_err("stale-file-remove", e))?;
create_canonical_mirror(upstream_path, staged_path)
} else if ftype.is_dir() {
std::fs::remove_dir_all(staged_path)
.map_err(|e| mirror_err("stale-dir-remove", e))?;
create_canonical_mirror(upstream_path, staged_path)
} else {
std::fs::remove_file(staged_path)
.map_err(|e| mirror_err("stale-other-remove", e))?;
create_canonical_mirror(upstream_path, staged_path)
}
}
}
}
fn create_canonical_mirror(upstream_path: &Path, staged_path: &Path) -> Result<(), Error> {
let mirror_err = |stage: &str, e: std::io::Error| {
Error::overlay_mirror_failed(
upstream_path.to_path_buf(),
staged_path.to_path_buf(),
stage.to_string(),
Some(e),
)
};
match symlink_platform(upstream_path, staged_path) {
Ok(()) => Ok(()),
Err(e)
if matches!(
e.kind(),
std::io::ErrorKind::PermissionDenied
| std::io::ErrorKind::Unsupported
| std::io::ErrorKind::AlreadyExists
) =>
{
if e.kind() == std::io::ErrorKind::AlreadyExists {
let _ = std::fs::remove_file(staged_path);
}
copy_fallback(upstream_path, staged_path).map_err(|e| mirror_err("copy-fallback", e))
}
Err(e) => Err(mirror_err("symlink", e)),
}
}
fn copy_fallback(src: &Path, dst: &Path) -> std::io::Result<()> {
let meta = std::fs::symlink_metadata(src)?;
let ftype = meta.file_type();
if ftype.is_file() {
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(src, dst)?;
Ok(())
} else if ftype.is_dir() {
if dst.exists() {
std::fs::remove_dir_all(dst)?;
}
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let child_src = entry.path();
let child_dst = dst.join(entry.file_name());
copy_fallback(&child_src, &child_dst)?;
}
Ok(())
} else if ftype.is_symlink() {
let target_meta = std::fs::metadata(src)?;
if target_meta.is_file() {
std::fs::copy(src, dst)?;
} else {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let child_src = entry.path();
let child_dst = dst.join(entry.file_name());
copy_fallback(&child_src, &child_dst)?;
}
}
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"copy-fallback: unsupported file type at upstream path",
))
}
}
#[cfg(unix)]
fn symlink_platform(target: &Path, link: &Path) -> std::io::Result<()> {
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
fn symlink_platform(target: &Path, link: &Path) -> std::io::Result<()> {
let meta = std::fs::metadata(target)?;
if meta.is_dir() {
std::os::windows::fs::symlink_dir(target, link)
} else {
std::os::windows::fs::symlink_file(target, link)
}
}
fn remove_path_any(path: &Path) -> std::io::Result<()> {
let meta = std::fs::symlink_metadata(path)?;
let ftype = meta.file_type();
if ftype.is_dir() {
std::fs::remove_dir_all(path)
} else {
std::fs::remove_file(path)
}
}
fn is_workspace_root_manifest(value: &toml::Value) -> bool {
let Some(top) = value.as_table() else {
return false;
};
let has_workspace = top.get("workspace").is_some_and(|v| v.is_table());
let has_package = top.get("package").is_some_and(|v| v.is_table());
has_workspace && !has_package
}
fn inspect_existing_crate_type(value: &toml::Value) -> bool {
let Some(lib) = value.get("lib") else {
return false;
};
let Some(ct) = lib.get("crate-type") else {
return false;
};
let Some(arr) = ct.as_array() else {
return false;
};
arr.iter().filter_map(|v| v.as_str()).any(|s| s == "dylib")
}
pub(crate) fn canonicalize_crate_type(
table: &mut toml::map::Map<String, toml::Value>,
) -> Result<(), Error> {
let existing: Vec<String> = match table.get("crate-type") {
None => Vec::new(),
Some(toml::Value::Array(arr)) => {
let mut out = Vec::with_capacity(arr.len());
for (idx, v) in arr.iter().enumerate() {
match v.as_str() {
Some(s) => out.push(s.to_string()),
None => {
return Err(Error::TomlParse {
path: PathBuf::from("<overlay>"),
message: format!(
"`[lib] crate-type` element at index {idx} is not a string; \
the overlay accepts only string crate-type entries"
),
});
}
}
}
out
}
Some(other) => {
return Err(Error::TomlParse {
path: PathBuf::from("<overlay>"),
message: format!(
"`[lib] crate-type` must be an array of strings, got `{}`",
type_name_of(other)
),
});
}
};
let mut out: Vec<String> = Vec::with_capacity(existing.len() + 2);
out.push("dylib".to_string());
out.push("rlib".to_string());
for entry in &existing {
if entry == "dylib" || entry == "rlib" {
continue;
}
if !out.contains(entry) {
out.push(entry.clone());
}
}
let array = out.into_iter().map(toml::Value::String).collect::<Vec<_>>();
table.insert("crate-type".to_string(), toml::Value::Array(array));
Ok(())
}
pub(crate) fn canonical_key_order() -> &'static [&'static str] {
&[
"package",
"lib",
"bin",
"example",
"test",
"bench",
"dependencies",
"dev-dependencies",
"build-dependencies",
"target",
"features",
"patch",
"replace",
"profile",
"workspace",
]
}
pub(crate) fn serialize_canonical(value: &toml::Value) -> Result<Vec<u8>, Error> {
let top = match value {
toml::Value::Table(t) => t,
other => {
return Err(Error::TomlParse {
path: PathBuf::from("<overlay>"),
message: format!(
"overlay serializer expected a TOML document (table) at the top level, got `{}`",
type_name_of(other)
),
});
}
};
let mut emitted: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
let mut order: Vec<String> = Vec::with_capacity(top.len());
for canonical in canonical_key_order() {
if top.contains_key(*canonical) {
order.push((*canonical).to_string());
emitted.insert(*canonical);
}
}
let mut leftovers: Vec<&String> = top
.keys()
.filter(|k| !emitted.contains(k.as_str()))
.collect();
leftovers.sort();
for k in leftovers {
order.push(k.clone());
}
let mut segments: Vec<String> = Vec::with_capacity(order.len());
for key in &order {
let v = top.get(key).expect("key came from `top`'s own iteration");
let mut wrapper = toml::map::Map::new();
wrapper.insert(key.clone(), v.clone());
let segment =
toml::ser::to_string(&toml::Value::Table(wrapper)).map_err(|e: toml::ser::Error| {
Error::TomlParse {
path: PathBuf::from("<overlay>"),
message: format!("overlay serializer failed for `{key}`: {e}"),
}
})?;
segments.push(segment);
}
let joined = segments.join("\n");
let normalized = post_process_output(&joined);
Ok(normalized.into_bytes())
}
fn post_process_output(input: &str) -> String {
let mut lines: Vec<&str> = Vec::with_capacity(input.lines().count());
for line in input.lines() {
let trimmed = line.trim_end_matches([' ', '\t', '\r']);
lines.push(trimmed);
}
let mut out = String::with_capacity(input.len());
let mut prev_blank = false;
for line in &lines {
let is_blank = line.is_empty();
if is_blank && prev_blank {
continue;
}
out.push_str(line);
out.push('\n');
prev_blank = is_blank;
}
while out.ends_with("\n\n") {
out.pop();
}
if !out.ends_with('\n') {
out.push('\n');
}
out
}
fn scan_dropped_comments(text: &str) -> Vec<String> {
let bytes = text.as_bytes();
let mut out: Vec<String> = Vec::new();
let mut i = 0usize;
let mut in_basic = false;
let mut in_literal = false;
let mut in_multi_basic = false;
let mut in_multi_literal = false;
while i < bytes.len() {
let b = bytes[i];
if in_multi_basic {
if b == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if b == b'"' && i + 2 < bytes.len() && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' {
in_multi_basic = false;
i += 3;
continue;
}
i += 1;
continue;
}
if in_multi_literal {
if b == b'\'' && i + 2 < bytes.len() && bytes[i + 1] == b'\'' && bytes[i + 2] == b'\'' {
in_multi_literal = false;
i += 3;
continue;
}
i += 1;
continue;
}
if in_basic {
if b == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if b == b'"' {
in_basic = false;
i += 1;
continue;
}
if b == b'\n' {
in_basic = false;
i += 1;
continue;
}
i += 1;
continue;
}
if in_literal {
if b == b'\'' {
in_literal = false;
i += 1;
continue;
}
if b == b'\n' {
in_literal = false;
i += 1;
continue;
}
i += 1;
continue;
}
if b == b'#' {
let start = i + 1;
let mut end = start;
while end < bytes.len() && bytes[end] != b'\n' {
end += 1;
}
let body = &text[start..end];
out.push(body.trim().to_string());
i = end;
continue;
}
if b == b'"' {
if i + 2 < bytes.len() && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' {
in_multi_basic = true;
i += 3;
continue;
}
in_basic = true;
i += 1;
continue;
}
if b == b'\'' {
if i + 2 < bytes.len() && bytes[i + 1] == b'\'' && bytes[i + 2] == b'\'' {
in_multi_literal = true;
i += 3;
continue;
}
in_literal = true;
i += 1;
continue;
}
i += 1;
}
out
}
#[cfg(test)]
fn extract_unquoted_comment(line: &str) -> Option<String> {
let comments = scan_dropped_comments(line);
comments.into_iter().next()
}
fn type_name_of(v: &toml::Value) -> &'static str {
match v {
toml::Value::String(_) => "string",
toml::Value::Integer(_) => "integer",
toml::Value::Float(_) => "float",
toml::Value::Boolean(_) => "boolean",
toml::Value::Datetime(_) => "datetime",
toml::Value::Array(_) => "array",
toml::Value::Table(_) => "table",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonicalize_inserts_dylib_rlib_when_absent() {
let mut t = toml::map::Map::new();
canonicalize_crate_type(&mut t).unwrap();
let ct = t.get("crate-type").unwrap().as_array().unwrap();
let strs: Vec<&str> = ct.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(strs, vec!["dylib", "rlib"]);
}
#[test]
fn canonicalize_prepends_dylib_to_rlib_only() {
let mut t = toml::map::Map::new();
t.insert(
"crate-type".into(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
canonicalize_crate_type(&mut t).unwrap();
let ct = t.get("crate-type").unwrap().as_array().unwrap();
let strs: Vec<&str> = ct.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(strs, vec!["dylib", "rlib"]);
}
#[test]
fn canonicalize_appends_rlib_when_only_dylib() {
let mut t = toml::map::Map::new();
t.insert(
"crate-type".into(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
canonicalize_crate_type(&mut t).unwrap();
let ct = t.get("crate-type").unwrap().as_array().unwrap();
let strs: Vec<&str> = ct.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(strs, vec!["dylib", "rlib"]);
}
#[test]
fn canonicalize_preserves_cdylib_after_pair() {
let mut t = toml::map::Map::new();
t.insert(
"crate-type".into(),
toml::Value::Array(vec![toml::Value::String("cdylib".into())]),
);
canonicalize_crate_type(&mut t).unwrap();
let ct = t.get("crate-type").unwrap().as_array().unwrap();
let strs: Vec<&str> = ct.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(strs, vec!["dylib", "rlib", "cdylib"]);
}
#[test]
fn canonicalize_dedups_duplicates() {
let mut t = toml::map::Map::new();
t.insert(
"crate-type".into(),
toml::Value::Array(vec![
toml::Value::String("rlib".into()),
toml::Value::String("dylib".into()),
toml::Value::String("rlib".into()),
toml::Value::String("cdylib".into()),
]),
);
canonicalize_crate_type(&mut t).unwrap();
let ct = t.get("crate-type").unwrap().as_array().unwrap();
let strs: Vec<&str> = ct.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(strs, vec!["dylib", "rlib", "cdylib"]);
}
#[test]
fn canonicalize_rejects_non_string_element() {
let mut t = toml::map::Map::new();
t.insert(
"crate-type".into(),
toml::Value::Array(vec![toml::Value::Integer(1)]),
);
let err = canonicalize_crate_type(&mut t).unwrap_err();
let s = format!("{err:?}");
assert!(
s.contains("not a string"),
"diagnostic must name the failure: {s}"
);
}
#[test]
fn canonical_key_order_starts_with_package() {
assert_eq!(canonical_key_order()[0], "package");
}
#[test]
fn extract_unquoted_comment_strips_leading_hash() {
assert_eq!(
extract_unquoted_comment("# a leading comment"),
Some("a leading comment".into())
);
}
#[test]
fn extract_unquoted_comment_handles_trailing() {
assert_eq!(
extract_unquoted_comment(r#"name = "demo" # trailing"#),
Some("trailing".into())
);
}
#[test]
fn extract_unquoted_comment_ignores_hash_inside_string() {
assert_eq!(
extract_unquoted_comment(r#"url = "http://example.com/#anchor""#),
None
);
}
#[test]
fn extract_unquoted_comment_ignores_hash_inside_single_quote() {
assert_eq!(extract_unquoted_comment(r#"name = 'foo#bar'"#), None);
}
#[test]
fn scan_ignores_hash_inside_multiline_basic_string() {
let text = "description = \"\"\"\nline with #notacomment\n\"\"\"\n";
let comments = scan_dropped_comments(text);
assert!(
comments.iter().all(|c| !c.contains("notacomment")),
"multi-line basic string body must not be classified as a comment; got {comments:?}",
);
}
#[test]
fn scan_ignores_hash_inside_multiline_literal_string() {
let text = "description = '''\nline with #stillnotacomment\n'''\n";
let comments = scan_dropped_comments(text);
assert!(
comments.iter().all(|c| !c.contains("stillnotacomment")),
"multi-line literal string body must not be classified as a comment; got {comments:?}",
);
}
#[test]
fn scan_recognizes_comment_after_multiline_string_closes() {
let text = "description = \"\"\"\nblock\n\"\"\" # real comment\n";
let comments = scan_dropped_comments(text);
assert!(
comments.iter().any(|c| c == "real comment"),
"comment AFTER the multi-line string close must be captured; got {comments:?}",
);
assert!(
comments.iter().all(|c| !c.contains("block")),
"multi-line body must never appear as a comment; got {comments:?}",
);
}
#[test]
fn scan_basic_string_escape_does_not_strand_state() {
let text = "name = \"foo \\\" #notacomment\"\n# real\n";
let comments = scan_dropped_comments(text);
assert!(
!comments.iter().any(|c| c.contains("notacomment")),
"escaped quote inside basic string must keep scanner in-string; got {comments:?}",
);
assert!(
comments.iter().any(|c| c == "real"),
"comment on the following line must still be captured; got {comments:?}",
);
}
#[test]
fn post_process_strips_trailing_whitespace() {
let raw = "foo = 1 \nbar = 2\t\n";
let out = post_process_output(raw);
assert!(out.lines().all(|l| !l.ends_with(' ') && !l.ends_with('\t')));
}
#[test]
fn post_process_strips_cr() {
let raw = "foo = 1\r\nbar = 2\r\n";
let out = post_process_output(raw);
assert!(!out.contains('\r'));
}
#[test]
fn post_process_collapses_blank_runs() {
let raw = "foo = 1\n\n\n\nbar = 2\n";
let out = post_process_output(raw);
assert_eq!(out, "foo = 1\n\nbar = 2\n");
}
#[test]
fn serialize_canonical_emits_package_first() {
let input = r#"
[features]
default = []
[dependencies]
serde = "1"
[package]
name = "demo"
version = "0.1.0"
"#;
let val: toml::Value = toml::from_str(input).unwrap();
let bytes = serialize_canonical(&val).unwrap();
let out = String::from_utf8(bytes).unwrap();
let first_header = out.lines().find(|l| l.starts_with('[')).unwrap();
assert_eq!(first_header, "[package]", "got:\n{out}");
}
#[test]
fn absolutize_injects_lib_path_when_absent() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let lib = top.get("lib").and_then(|v| v.as_table()).unwrap();
let path = lib.get("path").and_then(|v| v.as_str()).unwrap();
assert_eq!(
path, "/work/demo/src/lib.rs",
"[lib] path must be the absolute upstream src/lib.rs; got `{path}`"
);
}
#[test]
fn absolutize_leaves_absolute_lib_path_unchanged() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
lib.insert(
"path".to_string(),
toml::Value::String("/elsewhere/src/lib.rs".into()),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let lib = top.get("lib").and_then(|v| v.as_table()).unwrap();
let path = lib.get("path").and_then(|v| v.as_str()).unwrap();
assert_eq!(
path, "/elsewhere/src/lib.rs",
"an absolute [lib] path must be preserved; got `{path}`"
);
}
#[test]
fn absolutize_rewrites_relative_lib_path() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
lib.insert(
"path".to_string(),
toml::Value::String("custom/lib.rs".into()),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let lib = top.get("lib").and_then(|v| v.as_table()).unwrap();
let path = lib.get("path").and_then(|v| v.as_str()).unwrap();
assert_eq!(
path, "/work/demo/custom/lib.rs",
"a relative [lib] path must be absolutized against upstream_dir; got `{path}`"
);
}
#[test]
fn absolutize_rewrites_dependencies_path() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut deps = toml::map::Map::new();
let mut inner = toml::map::Map::new();
inner.insert("path".to_string(), toml::Value::String("impl".into()));
deps.insert("inner-impl".to_string(), toml::Value::Table(inner));
top.insert("dependencies".to_string(), toml::Value::Table(deps));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let deps = top.get("dependencies").and_then(|v| v.as_table()).unwrap();
let inner = deps.get("inner-impl").and_then(|v| v.as_table()).unwrap();
let path = inner.get("path").and_then(|v| v.as_str()).unwrap();
assert_eq!(path, "/work/demo/impl");
}
#[test]
fn absolutize_rewrites_target_conditional_dependencies_path() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut targets = toml::map::Map::new();
let mut linux = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut platform_dep = toml::map::Map::new();
platform_dep.insert("path".to_string(), toml::Value::String("linux-impl".into()));
deps.insert(
"platform-bits".to_string(),
toml::Value::Table(platform_dep),
);
linux.insert("dependencies".to_string(), toml::Value::Table(deps));
targets.insert(
r#"cfg(target_os = "linux")"#.to_string(),
toml::Value::Table(linux),
);
top.insert("target".to_string(), toml::Value::Table(targets));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let targets = top.get("target").and_then(|v| v.as_table()).unwrap();
let linux = targets
.get(r#"cfg(target_os = "linux")"#)
.and_then(|v| v.as_table())
.unwrap();
let deps = linux
.get("dependencies")
.and_then(|v| v.as_table())
.unwrap();
let platform_dep = deps
.get("platform-bits")
.and_then(|v| v.as_table())
.unwrap();
let path = platform_dep.get("path").and_then(|v| v.as_str()).unwrap();
assert_eq!(
path, "/work/demo/linux-impl",
"[target.*.dependencies.X].path must be absolutized; got `{path}`"
);
}
#[test]
fn absolutize_rewrites_workspace_members_and_exclude() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut ws = toml::map::Map::new();
ws.insert(
"members".to_string(),
toml::Value::Array(vec![
toml::Value::String("crate-a".into()),
toml::Value::String("crate-b".into()),
toml::Value::String("/elsewhere/crate-c".into()),
]),
);
ws.insert(
"exclude".to_string(),
toml::Value::Array(vec![toml::Value::String("scratch".into())]),
);
top.insert("workspace".to_string(), toml::Value::Table(ws));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let ws = top.get("workspace").and_then(|v| v.as_table()).unwrap();
let members = ws.get("members").and_then(|v| v.as_array()).unwrap();
let member_strs: Vec<&str> = members.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(
member_strs,
vec![
"/work/demo/crate-a",
"/work/demo/crate-b",
"/elsewhere/crate-c"
],
"[workspace] members must be absolutized, leaving already-absolute entries alone"
);
let exclude = ws.get("exclude").and_then(|v| v.as_array()).unwrap();
let exclude_strs: Vec<&str> = exclude.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(exclude_strs, vec!["/work/demo/scratch"]);
}
#[test]
fn absolutize_does_not_inject_build_when_upstream_has_no_build_rs() {
let upstream_dir = Path::new("/work/demo-no-build-rs");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let pkg = top.get("package").and_then(|v| v.as_table()).unwrap();
assert!(
!pkg.contains_key("build"),
"build key must not be injected when no upstream build.rs exists; \
got pkg keys {:?}",
pkg.keys().collect::<Vec<_>>()
);
}
#[test]
fn absolutize_disables_non_lib_auto_discovery() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let pkg = top.get("package").and_then(|v| v.as_table()).unwrap();
for key in ["autobins", "autoexamples", "autotests", "autobenches"] {
let val = pkg.get(key).and_then(|v| v.as_bool());
assert_eq!(
val,
Some(false),
"[package] {key} must be `false` to disable cargo auto-discovery; \
got {val:?}",
);
}
}
#[test]
fn absolutize_rewrites_array_table_paths() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
for (section, value) in [
("bin", "src/bin/foo.rs"),
("example", "examples/eg.rs"),
("test", "tests/it.rs"),
("bench", "benches/bench.rs"),
] {
let mut entry = toml::map::Map::new();
entry.insert("name".to_string(), toml::Value::String("target".into()));
entry.insert("path".to_string(), toml::Value::String(value.into()));
top.insert(
section.to_string(),
toml::Value::Array(vec![toml::Value::Table(entry)]),
);
}
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
for (section, original) in [
("bin", "src/bin/foo.rs"),
("example", "examples/eg.rs"),
("test", "tests/it.rs"),
("bench", "benches/bench.rs"),
] {
let arr = top.get(section).and_then(|v| v.as_array()).unwrap();
let entry = arr[0].as_table().unwrap();
let path = entry.get("path").and_then(|v| v.as_str()).unwrap();
let expected = format!("/work/demo/{original}");
assert_eq!(
path, expected,
"[[{section}]] path must be absolutized to `{expected}`; got `{path}`"
);
}
}
#[test]
fn absolutizes_package_workspace_pointer() {
let upstream_dir = Path::new("/work/cxx");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("cxx".into()));
pkg.insert("workspace".to_string(), toml::Value::String("../".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let pkg = top.get("package").and_then(|v| v.as_table()).unwrap();
let ws_ptr = pkg.get("workspace").and_then(|v| v.as_str()).unwrap();
assert_eq!(
ws_ptr, "/work/cxx/../",
"[package].workspace must be absolutized as Path::join (no normalization); got `{ws_ptr}`"
);
}
#[test]
fn absolutizes_workspace_default_members() {
let upstream_dir = Path::new("/work/repo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("repo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut ws = toml::map::Map::new();
ws.insert(
"default-members".to_string(),
toml::Value::Array(vec![
toml::Value::String("crate-a".into()),
toml::Value::String("crate-b".into()),
]),
);
top.insert("workspace".to_string(), toml::Value::Table(ws));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let ws = top.get("workspace").and_then(|v| v.as_table()).unwrap();
let dm = ws
.get("default-members")
.and_then(|v| v.as_array())
.unwrap();
let dm_strs: Vec<&str> = dm.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(
dm_strs,
vec!["/work/repo/crate-a", "/work/repo/crate-b"],
"[workspace].default-members must be absolutized; got {dm_strs:?}"
);
}
#[test]
fn absolutizes_workspace_dependencies_path() {
let upstream_dir = Path::new("/work/monorepo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("monorepo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut ws = toml::map::Map::new();
let mut ws_deps = toml::map::Map::new();
let mut impl_dep = toml::map::Map::new();
impl_dep.insert("path".to_string(), toml::Value::String("impl".into()));
ws_deps.insert("my-impl".to_string(), toml::Value::Table(impl_dep));
let mut proc_macro_dep = toml::map::Map::new();
proc_macro_dep.insert("path".to_string(), toml::Value::String("proc-macro".into()));
ws_deps.insert(
"my-proc-macro".to_string(),
toml::Value::Table(proc_macro_dep),
);
ws.insert("dependencies".to_string(), toml::Value::Table(ws_deps));
top.insert("workspace".to_string(), toml::Value::Table(ws));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let ws = top.get("workspace").and_then(|v| v.as_table()).unwrap();
let ws_deps = ws.get("dependencies").and_then(|v| v.as_table()).unwrap();
let impl_path = ws_deps
.get("my-impl")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
impl_path, "/work/monorepo/impl",
"[workspace.dependencies.my-impl].path must be absolutized; got `{impl_path}`"
);
let pm_path = ws_deps
.get("my-proc-macro")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
pm_path, "/work/monorepo/proc-macro",
"[workspace.dependencies.my-proc-macro].path must be absolutized; got `{pm_path}`"
);
}
#[test]
fn absolutizes_patch_registry_path() {
let upstream_dir = Path::new("/work/cxx");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("cxx".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut cxx_entry = toml::map::Map::new();
cxx_entry.insert("path".to_string(), toml::Value::String(".".into()));
let mut cxx_build_entry = toml::map::Map::new();
cxx_build_entry.insert("path".to_string(), toml::Value::String("gen/build".into()));
let mut serde_entry = toml::map::Map::new();
serde_entry.insert(
"git".to_string(),
toml::Value::String("https://github.com/serde-rs/serde".into()),
);
serde_entry.insert("branch".to_string(), toml::Value::String("master".into()));
let mut crates_io = toml::map::Map::new();
crates_io.insert("cxx".to_string(), toml::Value::Table(cxx_entry));
crates_io.insert("cxx-build".to_string(), toml::Value::Table(cxx_build_entry));
crates_io.insert("serde".to_string(), toml::Value::Table(serde_entry));
let mut patch = toml::map::Map::new();
patch.insert("crates-io".to_string(), toml::Value::Table(crates_io));
top.insert("patch".to_string(), toml::Value::Table(patch));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let patch = top.get("patch").and_then(|v| v.as_table()).unwrap();
let crates_io = patch.get("crates-io").and_then(|v| v.as_table()).unwrap();
let cxx_path = crates_io
.get("cxx")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
cxx_path, "/work/cxx/.",
"[patch.crates-io.cxx].path absolutized via Path::join preserves the `.`; \
cargo treats `/work/cxx/.` as equivalent to `/work/cxx`; got `{cxx_path}`"
);
let cxx_build_path = crates_io
.get("cxx-build")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
cxx_build_path, "/work/cxx/gen/build",
"[patch.crates-io.cxx-build].path must be absolutized; got `{cxx_build_path}`"
);
let serde = crates_io.get("serde").and_then(|v| v.as_table()).unwrap();
assert!(
!serde.contains_key("path"),
"git-form patch entry must not gain a path key"
);
assert_eq!(
serde.get("git").and_then(|v| v.as_str()),
Some("https://github.com/serde-rs/serde"),
"git URL in git-form patch entry must be unchanged"
);
assert_eq!(
serde.get("branch").and_then(|v| v.as_str()),
Some("master"),
"branch in git-form patch entry must be unchanged"
);
}
#[test]
fn absolutize_leaves_absolute_patch_path_unchanged() {
let upstream_dir = Path::new("/work/cxx");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("cxx".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut abs_entry = toml::map::Map::new();
abs_entry.insert(
"path".to_string(),
toml::Value::String("/absolute/path/to/cxx".into()),
);
let mut crates_io = toml::map::Map::new();
crates_io.insert("cxx".to_string(), toml::Value::Table(abs_entry));
let mut patch = toml::map::Map::new();
patch.insert("crates-io".to_string(), toml::Value::Table(crates_io));
top.insert("patch".to_string(), toml::Value::Table(patch));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let path = top
.get("patch")
.and_then(|v| v.as_table())
.and_then(|t| t.get("crates-io"))
.and_then(|v| v.as_table())
.and_then(|t| t.get("cxx"))
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
path, "/absolute/path/to/cxx",
"an absolute [patch.*.*].path must be left unchanged; got `{path}`"
);
}
#[test]
fn absolutizes_replace_path() {
let upstream_dir = Path::new("/work/project");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("project".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut cxx_entry = toml::map::Map::new();
cxx_entry.insert("path".to_string(), toml::Value::String("vendor/cxx".into()));
let mut serde_entry = toml::map::Map::new();
serde_entry.insert(
"git".to_string(),
toml::Value::String("https://github.com/serde-rs/serde".into()),
);
serde_entry.insert("rev".to_string(), toml::Value::String("abc123".into()));
let mut abs_entry = toml::map::Map::new();
abs_entry.insert(
"path".to_string(),
toml::Value::String("/pre-existing/absolute/path".into()),
);
let mut replace = toml::map::Map::new();
replace.insert("cxx:0.3.0".to_string(), toml::Value::Table(cxx_entry));
replace.insert("serde:1.0.0".to_string(), toml::Value::Table(serde_entry));
replace.insert("abs-dep:0.1.0".to_string(), toml::Value::Table(abs_entry));
top.insert("replace".to_string(), toml::Value::Table(replace));
absolutize_path_bearing_keys(&mut top, upstream_dir, Path::new("/test/Cargo.toml"))
.unwrap();
let replace_out = top.get("replace").and_then(|v| v.as_table()).unwrap();
let cxx_path = replace_out
.get("cxx:0.3.0")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
cxx_path, "/work/project/vendor/cxx",
"[replace.\"cxx:0.3.0\"].path must be absolutized; got `{cxx_path}`"
);
let serde_t = replace_out
.get("serde:1.0.0")
.and_then(|v| v.as_table())
.unwrap();
assert!(
!serde_t.contains_key("path"),
"git-form [replace] entry must not gain a `path` key"
);
assert_eq!(
serde_t.get("git").and_then(|v| v.as_str()),
Some("https://github.com/serde-rs/serde"),
"git URL in git-form [replace] entry must be unchanged"
);
let abs_path = replace_out
.get("abs-dep:0.1.0")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
abs_path, "/pre-existing/absolute/path",
"an already-absolute [replace] path must be left unchanged; got `{abs_path}`"
);
}
fn dummy_upstream_manifest_path() -> std::path::PathBuf {
std::path::PathBuf::from("/tmp/lihaaf-test-upstream/Cargo.toml")
}
#[test]
fn override_workspace_preserves_inheritance_tables() {
let mut top = toml::map::Map::new();
let mut ws = toml::map::Map::new();
ws.insert(
"members".to_string(),
toml::Value::Array(vec![toml::Value::String("crate-a".into())]),
);
ws.insert(
"exclude".to_string(),
toml::Value::Array(vec![toml::Value::String("scratch".into())]),
);
ws.insert(
"default-members".to_string(),
toml::Value::Array(vec![toml::Value::String("crate-a".into())]),
);
ws.insert("resolver".to_string(), toml::Value::String("2".into()));
let mut ws_deps = toml::map::Map::new();
let mut shared = toml::map::Map::new();
shared.insert("path".to_string(), toml::Value::String("/abs/utils".into()));
ws_deps.insert("shared-utils".to_string(), toml::Value::Table(shared));
ws.insert("dependencies".to_string(), toml::Value::Table(ws_deps));
let mut ws_pkg = toml::map::Map::new();
ws_pkg.insert("edition".to_string(), toml::Value::String("2021".into()));
ws_pkg.insert("version".to_string(), toml::Value::String("0.1.0".into()));
ws.insert("package".to_string(), toml::Value::Table(ws_pkg));
let mut ws_lints = toml::map::Map::new();
let mut ws_lints_rust = toml::map::Map::new();
ws_lints_rust.insert(
"unsafe_code".to_string(),
toml::Value::String("forbid".into()),
);
ws_lints.insert("rust".to_string(), toml::Value::Table(ws_lints_rust));
ws.insert("lints".to_string(), toml::Value::Table(ws_lints));
let mut ws_meta = toml::map::Map::new();
let mut ws_meta_tool = toml::map::Map::new();
ws_meta_tool.insert("key".to_string(), toml::Value::String("value".into()));
ws_meta.insert("my-tool".to_string(), toml::Value::Table(ws_meta_tool));
ws.insert("metadata".to_string(), toml::Value::Table(ws_meta));
let mut ws_future = toml::map::Map::new();
ws_future.insert(
"key".to_string(),
toml::Value::String("future-value".into()),
);
ws.insert(
"future-cargo-feature".to_string(),
toml::Value::Table(ws_future),
);
top.insert("workspace".to_string(), toml::Value::Table(ws));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("test".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path(), None)
.expect("workspace-root case must succeed");
let ws_out = top.get("workspace").and_then(|v| v.as_table()).unwrap();
for stripped in ["members", "exclude", "default-members"] {
assert!(
!ws_out.contains_key(stripped),
"membership key `{stripped}` MUST be stripped; got keys: {:?}",
ws_out.keys().collect::<Vec<_>>()
);
}
assert!(
ws_out.contains_key("dependencies"),
"workspace.dependencies must survive"
);
assert!(
ws_out.contains_key("package"),
"workspace.package must survive"
);
assert!(ws_out.contains_key("lints"), "workspace.lints must survive");
assert!(
ws_out.contains_key("metadata"),
"workspace.metadata must survive"
);
assert!(
ws_out.contains_key("resolver"),
"workspace.resolver must survive"
);
assert!(
ws_out.contains_key("future-cargo-feature"),
"unknown `[workspace.X]` table must pass through (forward-compat)"
);
let ws_deps_out = ws_out
.get("dependencies")
.and_then(|v| v.as_table())
.unwrap();
let shared_out = ws_deps_out
.get("shared-utils")
.and_then(|v| v.as_table())
.unwrap();
assert_eq!(
shared_out.get("path").and_then(|v| v.as_str()),
Some("/abs/utils"),
"workspace.dependencies.shared-utils.path must pass through verbatim"
);
let ws_pkg_out = ws_out.get("package").and_then(|v| v.as_table()).unwrap();
assert_eq!(
ws_pkg_out.get("edition").and_then(|v| v.as_str()),
Some("2021"),
"workspace.package.edition must pass through verbatim"
);
}
#[test]
fn override_workspace_injects_empty_when_absent() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("test".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
assert!(!top.contains_key("workspace"));
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path(), None)
.expect("missing `[workspace]` must inject an empty one");
let ws_out = top.get("workspace").and_then(|v| v.as_table()).unwrap();
assert!(
ws_out.is_empty(),
"injected `[workspace]` must be empty when upstream had none; got: {:?}",
ws_out.keys().collect::<Vec<_>>()
);
}
#[test]
fn override_workspace_rejects_workspace_member_manifest() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("member".into()));
pkg.insert("workspace".to_string(), toml::Value::String("../".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let err = override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path(), None)
.expect_err("workspace-member manifest must be rejected");
match err {
Error::Cli {
clap_exit_code,
message,
} => {
assert_eq!(
clap_exit_code, 2,
"exit code must be the clap usage code (2)"
);
assert!(
message.contains("workspace member"),
"rejection diagnostic must name the failure category; got: {message}"
);
assert!(
message.contains("[package].workspace"),
"rejection diagnostic must name the offending key; got: {message}"
);
assert!(
message.contains("/tmp/lihaaf-test-upstream/Cargo.toml"),
"rejection diagnostic must include the offending manifest path; got: {message}"
);
assert!(
!message.contains("implicit"),
"explicit rejection must not use the implicit-case wording; got: {message}"
);
}
other => panic!("expected Error::Cli for workspace-member rejection, got {other:?}"),
}
}
#[test]
fn override_workspace_rejects_implicit_workspace_member_manifest() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("member".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
top.insert("dependencies".to_string(), toml::Value::Table(deps));
let err = override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path(), None)
.expect_err("implicit workspace-member manifest must be rejected");
match err {
Error::Cli {
clap_exit_code,
message,
} => {
assert_eq!(
clap_exit_code, 2,
"exit code must match the explicit-rejection contract (clap usage code 2)"
);
assert!(
message.contains("implicit workspace member"),
"rejection diagnostic must name the implicit-member category; got: {message}"
);
assert!(
message.contains("no local `[workspace]`"),
"diagnostic must name the diagnostic structural signal; got: {message}"
);
assert!(
message.contains("workspace = true"),
"diagnostic must point at the inheritance-reference shape; got: {message}"
);
assert!(
message.contains("/tmp/lihaaf-test-upstream/Cargo.toml"),
"diagnostic must include the offending manifest path; got: {message}"
);
assert!(
!top.contains_key("workspace"),
"rejection must not leave a half-mutated `[workspace]` entry in place"
);
}
other => {
panic!("expected Error::Cli for implicit workspace-member rejection, got {other:?}")
}
}
}
#[test]
fn override_workspace_rejects_manifest_with_ancestor_workspace() {
let tmp = tempfile::tempdir().expect("tempdir for ancestor-workspace rejection test");
let parent_manifest = tmp.path().join("Cargo.toml");
std::fs::write(
&parent_manifest,
r#"[workspace]
members = ["sub"]
[patch.crates-io]
foo = { path = "../my-foo-fork" }
"#,
)
.expect("writing parent Cargo.toml");
let sub_dir = tmp.path().join("sub");
std::fs::create_dir_all(&sub_dir).expect("creating sub/ dir");
let sub_manifest = sub_dir.join("Cargo.toml");
std::fs::write(
&sub_manifest,
r#"[package]
name = "sub"
version = "0.1.0"
"#,
)
.expect("writing sub/Cargo.toml");
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("sub".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let err = override_workspace_inheritance(&mut top, &sub_manifest, None)
.expect_err("manifest with ancestor workspace must be rejected");
match err {
Error::Cli {
clap_exit_code,
message,
} => {
assert_eq!(
clap_exit_code, 2,
"exit code must match the rejection contract (clap usage code 2)"
);
assert!(
message.contains("implicit workspace member"),
"diagnostic must name the implicit-member category; got: {message}"
);
assert!(
message.contains("ancestor manifest"),
"diagnostic must name the ancestor-detection signal; got: {message}"
);
let parent_str = parent_manifest.display().to_string();
assert!(
message.contains(&parent_str),
"diagnostic must include the ancestor manifest path `{parent_str}`; got: {message}"
);
assert!(
!message.contains("workspace = true"),
"ancestor-workspace rejection must not mention inheritance refs (this case has none); got: {message}"
);
assert!(
!top.contains_key("workspace"),
"rejection must not leave a half-mutated `[workspace]` entry in place"
);
}
other => {
panic!("expected Error::Cli for ancestor-workspace rejection, got {other:?}")
}
}
}
#[test]
fn override_workspace_allows_standalone_with_no_ancestor_workspace() {
let tmp = tempfile::tempdir().expect("tempdir for standalone-allows negative-case test");
let manifest = tmp.path().join("Cargo.toml");
std::fs::write(
&manifest,
r#"[package]
name = "standalone"
version = "0.1.0"
"#,
)
.expect("writing standalone Cargo.toml");
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("standalone".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
assert!(!top.contains_key("workspace"));
override_workspace_inheritance(&mut top, &manifest, None).unwrap_or_else(|err| {
panic!(
"standalone manifest with no ancestor workspace must NOT be rejected; \
got: {err:?} (this would indicate an ancestor-workspace rejection regression — the ancestor walk \
spuriously detected a workspace where there is none, OR the test \
environment has an unexpected `Cargo.toml` somewhere above the temp dir)"
)
});
let ws_out = top.get("workspace").and_then(|v| v.as_table()).unwrap();
assert!(
ws_out.is_empty(),
"branch 5 (standalone) must inject an empty `[workspace]`; got keys: {:?}",
ws_out.keys().collect::<Vec<_>>()
);
}
#[test]
fn detect_implicit_ancestor_workspace_returns_none_for_standalone() {
let tmp = tempfile::tempdir().expect("tempdir for ancestor-walk None negative case");
let manifest = tmp.path().join("Cargo.toml");
std::fs::write(&manifest, "[package]\nname = \"standalone\"\n")
.expect("writing standalone Cargo.toml");
let result = detect_implicit_ancestor_workspace(&manifest)
.expect("ancestor walk on a clean tempdir must not return Err");
assert!(
result.is_none(),
"ancestor walk from a standalone tempdir manifest must return None; got: {result:?}"
);
}
#[test]
fn detect_implicit_ancestor_workspace_finds_nearest_ancestor() {
let tmp = tempfile::tempdir().expect("tempdir for ancestor-walk Some positive case");
let parent_manifest = tmp.path().join("Cargo.toml");
std::fs::write(&parent_manifest, "[workspace]\nmembers = [\"sub\"]\n")
.expect("writing parent Cargo.toml");
let sub_dir = tmp.path().join("sub");
std::fs::create_dir_all(&sub_dir).expect("creating sub/");
let sub_manifest = sub_dir.join("Cargo.toml");
std::fs::write(
&sub_manifest,
"[package]\nname = \"sub\"\nversion = \"0.1.0\"\n",
)
.expect("writing sub/Cargo.toml");
let result =
detect_implicit_ancestor_workspace(&sub_manifest).expect("ancestor walk must succeed");
let found = result.expect("ancestor walk must find the parent workspace");
assert_eq!(
found, parent_manifest,
"ancestor walk must return the parent manifest path verbatim"
);
}
#[test]
fn manifest_has_inheritance_reference_returns_false_for_non_inheriting_shapes() {
let top = toml::map::Map::new();
assert!(
!manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"empty manifest has no inheritance references"
);
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
assert!(
!manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"manifest with `[package].name` only has no inheritance references"
);
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("version".to_string(), toml::Value::String("1.0".into()));
deps.insert("foo".to_string(), toml::Value::Table(foo));
top.insert("dependencies".to_string(), toml::Value::Table(deps));
assert!(
!manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"regular dep without `workspace = true` does not count as inheritance"
);
let mut pkg2 = toml::map::Map::new();
pkg2.insert("name".to_string(), toml::Value::String("member".into()));
pkg2.insert("workspace".to_string(), toml::Value::String("../".into()));
let mut top2 = toml::map::Map::new();
top2.insert("package".to_string(), toml::Value::Table(pkg2));
assert!(
!manifest_has_inheritance_reference(&top2, Path::new("/test/Cargo.toml")).unwrap(),
"`[package].workspace = \"...\"` is the explicit-member pointer, not inheritance"
);
}
#[test]
fn manifest_has_inheritance_reference_detects_every_family() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
let mut version = toml::map::Map::new();
version.insert("workspace".to_string(), toml::Value::Boolean(true));
pkg.insert("version".to_string(), toml::Value::Table(version));
top.insert("package".to_string(), toml::Value::Table(pkg));
assert!(
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"`[package].version = {{ workspace = true }}` must be detected"
);
let mut top = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
top.insert("dependencies".to_string(), toml::Value::Table(deps));
assert!(
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"`[dependencies] foo = {{ workspace = true }}` must be detected"
);
let mut top = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
top.insert("dev-dependencies".to_string(), toml::Value::Table(deps));
assert!(
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"`[dev-dependencies] foo = {{ workspace = true }}` must be detected"
);
let mut top = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
top.insert("build-dependencies".to_string(), toml::Value::Table(deps));
assert!(
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"`[build-dependencies] foo = {{ workspace = true }}` must be detected"
);
let mut top = toml::map::Map::new();
let mut targets = toml::map::Map::new();
let mut cfg = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
cfg.insert("dependencies".to_string(), toml::Value::Table(deps));
targets.insert("cfg(unix)".to_string(), toml::Value::Table(cfg));
top.insert("target".to_string(), toml::Value::Table(targets));
assert!(
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"`[target.<cfg>.dependencies]` inheritance must be detected"
);
let mut top = toml::map::Map::new();
let mut targets = toml::map::Map::new();
let mut cfg = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
cfg.insert("dev-dependencies".to_string(), toml::Value::Table(deps));
targets.insert("cfg(windows)".to_string(), toml::Value::Table(cfg));
top.insert("target".to_string(), toml::Value::Table(targets));
assert!(
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"`[target.<cfg>.dev-dependencies]` inheritance must be detected"
);
let mut top = toml::map::Map::new();
let mut targets = toml::map::Map::new();
let mut cfg = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
cfg.insert("build-dependencies".to_string(), toml::Value::Table(deps));
targets.insert(
"cfg(target_arch = \"wasm32\")".to_string(),
toml::Value::Table(cfg),
);
top.insert("target".to_string(), toml::Value::Table(targets));
assert!(
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"`[target.<cfg>.build-dependencies]` inheritance must be detected"
);
let mut top = toml::map::Map::new();
let mut lints = toml::map::Map::new();
lints.insert("workspace".to_string(), toml::Value::Boolean(true));
top.insert("lints".to_string(), toml::Value::Table(lints));
assert!(
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"`[lints] workspace = true` (top-level form) must be detected"
);
let mut top = toml::map::Map::new();
let mut lints = toml::map::Map::new();
let mut rust = toml::map::Map::new();
rust.insert("workspace".to_string(), toml::Value::Boolean(true));
lints.insert("rust".to_string(), toml::Value::Table(rust));
top.insert("lints".to_string(), toml::Value::Table(lints));
assert!(
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"`[lints.rust] workspace = true` (forward-compat nested form) must be detected"
);
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
let mut future = toml::map::Map::new();
future.insert("workspace".to_string(), toml::Value::Boolean(true));
pkg.insert(
"future-inheritable-key".to_string(),
toml::Value::Table(future),
);
top.insert("package".to_string(), toml::Value::Table(pkg));
assert!(
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap(),
"unknown `[package].<future-key>` inheritance must be detected (forward-compat)"
);
}
#[test]
fn override_workspace_allows_root_with_local_workspace_and_inheritance_refs() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("root".into()));
let mut version = toml::map::Map::new();
version.insert("workspace".to_string(), toml::Value::Boolean(true));
pkg.insert("version".to_string(), toml::Value::Table(version));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut ws = toml::map::Map::new();
let mut ws_pkg = toml::map::Map::new();
ws_pkg.insert("version".to_string(), toml::Value::String("0.1.0".into()));
ws.insert("package".to_string(), toml::Value::Table(ws_pkg));
top.insert("workspace".to_string(), toml::Value::Table(ws));
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path(), None)
.expect("root with local [workspace] + inheritance refs must succeed (not implicit)");
let pkg_out = top.get("package").and_then(|v| v.as_table()).unwrap();
let version_out = pkg_out.get("version").and_then(|v| v.as_table()).unwrap();
assert_eq!(
version_out.get("workspace").and_then(|v| v.as_bool()),
Some(true),
"inheritance reference must pass through verbatim for workspace-root case"
);
let ws_out = top.get("workspace").and_then(|v| v.as_table()).unwrap();
assert!(
ws_out.contains_key("package"),
"workspace.package must survive for the workspace-root case"
);
}
#[test]
fn override_workspace_is_idempotent() {
let mut top = toml::map::Map::new();
let mut ws = toml::map::Map::new();
ws.insert(
"members".to_string(),
toml::Value::Array(vec![toml::Value::String("crate-a".into())]),
);
let mut ws_deps = toml::map::Map::new();
let mut shared = toml::map::Map::new();
shared.insert("path".to_string(), toml::Value::String("/abs/utils".into()));
ws_deps.insert("shared".to_string(), toml::Value::Table(shared));
ws.insert("dependencies".to_string(), toml::Value::Table(ws_deps));
top.insert("workspace".to_string(), toml::Value::Table(ws));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("test".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path(), None).unwrap();
let after_first = top.clone();
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path(), None).unwrap();
assert_eq!(
top, after_first,
"second call must be a no-op on already-overridden output"
);
}
fn extract_lihaaf_table(
top: &toml::map::Map<String, toml::Value>,
) -> &toml::map::Map<String, toml::Value> {
top["package"].as_table().unwrap()["metadata"]
.as_table()
.unwrap()["lihaaf"]
.as_table()
.unwrap()
}
#[test]
fn synthetic_metadata_injects_allow_lints() {
let mut top = toml::map::Map::new();
let meta = SyntheticMetadata {
dylib_crate: "demo".into(),
extern_crates: vec!["demo".into()],
fixture_dirs: vec!["/abs/pass".into(), "/abs/fail".into()],
allow_lints: vec!["unexpected_cfgs".to_string()],
};
inject_synthetic_metadata(&mut top, &meta, Path::new("/test/Cargo.toml")).unwrap();
let lihaaf = extract_lihaaf_table(&top);
let lints = lihaaf["allow_lints"]
.as_array()
.expect("allow_lints must be an array");
let lint_strs: Vec<&str> = lints.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(
lint_strs,
vec!["unexpected_cfgs"],
"inject_synthetic_metadata must write the allow_lints array verbatim"
);
}
#[test]
fn synthetic_metadata_default_in_compat_driver() {
let meta = compat_default_synthetic_metadata("demo", vec![]);
assert_eq!(
meta.allow_lints,
vec!["unexpected_cfgs".to_string()],
"compat-driver default allow_lints must be [\"unexpected_cfgs\"]; \
changes also require spec §3.2/C.4 + CHANGELOG updates",
);
}
#[test]
fn synthetic_metadata_replaces_upstream_allow_lints() {
let mut top = toml::map::Map::new();
let mut upstream_lihaaf = toml::map::Map::new();
upstream_lihaaf.insert(
"allow_lints".to_string(),
toml::Value::Array(vec![toml::Value::String("some_other_lint".to_string())]),
);
let mut upstream_metadata = toml::map::Map::new();
upstream_metadata.insert("lihaaf".to_string(), toml::Value::Table(upstream_lihaaf));
let mut upstream_pkg = toml::map::Map::new();
upstream_pkg.insert(
"metadata".to_string(),
toml::Value::Table(upstream_metadata),
);
top.insert("package".to_string(), toml::Value::Table(upstream_pkg));
let meta = SyntheticMetadata {
dylib_crate: "demo".into(),
extern_crates: vec!["demo".into()],
fixture_dirs: vec!["/abs/pass".into()],
allow_lints: vec!["unexpected_cfgs".to_string()],
};
inject_synthetic_metadata(&mut top, &meta, Path::new("/test/Cargo.toml")).unwrap();
let lihaaf = extract_lihaaf_table(&top);
let lints = lihaaf["allow_lints"]
.as_array()
.expect("allow_lints must be an array after injection");
let lint_strs: Vec<&str> = lints.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(
lint_strs,
vec!["unexpected_cfgs"],
"inject_synthetic_metadata must REPLACE upstream allow_lints, not merge or preserve it"
);
}
fn materialize_for_patch_test(input: &str) -> (tempfile::TempDir, PathBuf, String) {
let tmp = tempfile::tempdir().expect("tempdir for self-patch test");
let upstream_dir = tmp.path();
let upstream_manifest = upstream_dir.join("Cargo.toml");
std::fs::write(&upstream_manifest, input).expect("writing upstream Cargo.toml");
std::fs::create_dir_all(upstream_dir.join("src")).expect("creating src/");
std::fs::write(
upstream_dir.join("src").join("lib.rs"),
"pub fn _stub() {}\n",
)
.expect("writing src/lib.rs");
let plan = materialize_overlay(&upstream_manifest).expect("overlay must succeed");
let bytes = std::fs::read(&plan.sibling_manifest).expect("read overlay manifest");
let out = String::from_utf8(bytes).expect("overlay UTF-8");
(tmp, plan.sibling_manifest, out)
}
#[test]
fn apply_self_patch_writes_entry_for_named_package_rule1_inject() {
let input = r#"[package]
name = "demo"
version = "0.1.0"
edition = "2021"
"#;
let (tmp, _staged, out) = materialize_for_patch_test(input);
let parsed: toml::Value = toml::from_str(&out).expect("overlay must parse");
let patch = parsed
.get("patch")
.and_then(|p| p.get("crates-io"))
.and_then(|c| c.get("demo"))
.expect("Rule 1 INJECT must add [patch.crates-io.demo]");
let path = patch
.get("path")
.and_then(|v| v.as_str())
.expect("[patch.crates-io.demo].path must be present");
let expected_tail = "target/lihaaf-overlay";
assert!(
path.ends_with(expected_tail),
"Rule 1 INJECT path must point at the staged-overlay-dir; got `{path}`"
);
assert!(
Path::new(path).is_absolute(),
"Rule 1 INJECT path must be absolute; got `{path}`"
);
drop(tmp);
}
#[test]
fn apply_self_patch_no_entry_when_package_name_absent() {
let input = r#"[package]
version = "0.1.0"
"#;
let (tmp, _staged, out) = materialize_for_patch_test(input);
let parsed: toml::Value = toml::from_str(&out).expect("overlay must parse");
let patch = parsed.get("patch").and_then(|p| p.get("crates-io"));
if let Some(c) = patch {
assert!(
c.as_table().is_none_or(|t| t.is_empty()),
"no [patch.crates-io.<X>] should be injected when [package].name is missing; got: {c}"
);
}
drop(tmp);
}
#[test]
fn apply_self_patch_path_form_is_staged_overlay_dir_not_upstream_rule1() {
let input = r#"[package]
name = "demo"
version = "0.1.0"
"#;
let (tmp, staged_manifest, out) = materialize_for_patch_test(input);
let parsed: toml::Value = toml::from_str(&out).expect("overlay must parse");
let path = parsed
.get("patch")
.and_then(|p| p.get("crates-io"))
.and_then(|c| c.get("demo"))
.and_then(|d| d.get("path"))
.and_then(|v| v.as_str())
.expect("[patch.crates-io.demo].path must exist");
assert!(
path.ends_with("/target/lihaaf-overlay"),
"Rule 1 emission must target the STAGED-OVERLAY-DIR \
(`<upstream>/target/lihaaf-overlay`), NOT the upstream dir. \
Got `{path}`. A regression to the upstream-dir target would \
reintroduce the full clobber self-loop bug (self-patch policy §2.1)."
);
let staged_parent = staged_manifest
.parent()
.expect("staged manifest has a parent dir")
.to_string_lossy()
.replace('\\', "/");
assert_eq!(
path, staged_parent,
"[patch.crates-io.demo].path must equal the staged-overlay parent"
);
drop(tmp);
}
#[cfg(unix)]
#[test]
fn apply_self_patch_idempotent_second_materialize() {
use std::os::unix::fs::MetadataExt;
let tmp = tempfile::tempdir().expect("tempdir for idempotency test");
let upstream_dir = tmp.path();
let upstream_manifest = upstream_dir.join("Cargo.toml");
std::fs::write(
&upstream_manifest,
r#"[package]
name = "demo"
version = "0.1.0"
"#,
)
.expect("writing upstream Cargo.toml");
std::fs::create_dir_all(upstream_dir.join("src")).expect("creating src/");
std::fs::write(
upstream_dir.join("src").join("lib.rs"),
"pub fn _stub() {}\n",
)
.expect("writing src/lib.rs");
std::fs::create_dir_all(upstream_dir.join("include")).expect("creating include/");
std::fs::write(
upstream_dir.join("include").join("stub.h"),
"// stub header\n",
)
.expect("writing include/stub.h");
let plan1 = materialize_overlay(&upstream_manifest).expect("first overlay must succeed");
let bytes1 = std::fs::read(&plan1.sibling_manifest).expect("read overlay 1");
let staged_overlay_dir = plan1
.sibling_manifest
.parent()
.expect("staged manifest has a parent")
.to_path_buf();
let src_ino_before = std::fs::symlink_metadata(staged_overlay_dir.join("src"))
.expect("staged src symlink must exist after first run")
.ino();
let include_ino_before = std::fs::symlink_metadata(staged_overlay_dir.join("include"))
.expect("staged include symlink must exist after first run")
.ino();
let plan2 = materialize_overlay(&upstream_manifest)
.expect("second materialize must return Ok (Option B contract)");
let bytes2 = std::fs::read(&plan2.sibling_manifest).expect("read overlay 2");
assert_eq!(
bytes1, bytes2,
"second materialize must produce byte-identical overlay manifest"
);
let src_ino_after = std::fs::symlink_metadata(staged_overlay_dir.join("src"))
.expect("staged src symlink must still exist after second run")
.ino();
let include_ino_after = std::fs::symlink_metadata(staged_overlay_dir.join("include"))
.expect("staged include symlink must still exist after second run")
.ino();
assert_eq!(
src_ino_before, src_ino_after,
"CASE 2 idempotent skip: src/ symlink must not be re-created (inode identity preserved)"
);
assert_eq!(
include_ino_before, include_ino_after,
"CASE 2 idempotent skip: include/ symlink must not be re-created"
);
let manifest_meta = std::fs::symlink_metadata(&plan2.sibling_manifest)
.expect("staged manifest must exist after second run");
assert!(
manifest_meta.file_type().is_file(),
"CASE 15: staged Cargo.toml must be a regular file after second materialize"
);
assert!(
!manifest_meta.file_type().is_symlink(),
"CASE 15: staged Cargo.toml must not be a symlink"
);
drop(tmp);
}
#[test]
fn apply_self_patch_remap_when_upstream_self_patch_cxx_shape_rule2() {
let input = r#"[package]
name = "demo"
version = "0.1.0"
[patch.crates-io]
demo = { path = "." }
"#;
let (tmp, staged_manifest, out) = materialize_for_patch_test(input);
let parsed: toml::Value = toml::from_str(&out).expect("overlay must parse");
let demo_patch = parsed
.get("patch")
.and_then(|p| p.get("crates-io"))
.and_then(|c| c.get("demo"))
.expect("Rule 2 must keep the [patch.crates-io.demo] entry present");
let path = demo_patch
.get("path")
.and_then(|v| v.as_str())
.expect("Rule 2 REMAP must emit a path string");
let staged_parent = staged_manifest
.parent()
.expect("staged has parent")
.to_string_lossy()
.replace('\\', "/");
assert_eq!(
path, staged_parent,
"Rule 2 REMAP must REWRITE the upstream's `path = \".\"` to point at \
the absolutized staged-overlay-dir; got `{path}` vs expected `{staged_parent}`"
);
assert!(
demo_patch.get("git").is_none(),
"Rule 2 REMAP must not surface any git source in the output"
);
drop(tmp);
}
#[test]
fn apply_self_patch_remap_path_dot_slash_form_rule2() {
let input = r#"[package]
name = "demo"
version = "0.1.0"
[patch.crates-io]
demo = { path = "./" }
"#;
let (tmp, staged_manifest, out) = materialize_for_patch_test(input);
let parsed: toml::Value = toml::from_str(&out).expect("overlay must parse");
let path = parsed
.get("patch")
.and_then(|p| p.get("crates-io"))
.and_then(|c| c.get("demo"))
.and_then(|d| d.get("path"))
.and_then(|v| v.as_str())
.expect("Rule 2 must emit a path under the `./` variant");
let staged_parent = staged_manifest
.parent()
.expect("staged has parent")
.to_string_lossy()
.replace('\\', "/");
assert_eq!(
path, staged_parent,
"Rule 2 REMAP must fire for the trailing-slash variant `path = \"./\"`"
);
drop(tmp);
}
#[test]
fn apply_self_patch_rejects_when_upstream_path_targets_external_source_rule4_path() {
let input = r#"[package]
name = "demo"
version = "0.1.0"
[patch.crates-io]
demo = { path = "../forked-demo" }
"#;
let tmp = tempfile::tempdir().expect("tempdir for Rule 4 path test");
let upstream_dir = tmp.path();
let upstream_manifest = upstream_dir.join("Cargo.toml");
std::fs::write(&upstream_manifest, input).expect("writing upstream Cargo.toml");
std::fs::create_dir_all(upstream_dir.join("src")).expect("creating src/");
std::fs::write(
upstream_dir.join("src").join("lib.rs"),
"pub fn _stub() {}\n",
)
.expect("writing src/lib.rs");
let err = materialize_overlay(&upstream_manifest)
.expect_err("Rule 4 REJECT must surface as Err(_)");
match err {
Error::CompatPatchOverrideConflict {
crate_name,
upstream_entry: _,
expected_resolution,
} => {
assert_eq!(crate_name, "demo");
assert!(
expected_resolution.contains("not currently supported"),
"Rule 4 error must explain the unsupported patch shape; got: {expected_resolution}"
);
}
other => panic!("Rule 4 must return CompatPatchOverrideConflict; got {other:?}"),
}
drop(tmp);
}
#[test]
fn apply_self_patch_rejects_when_upstream_git_form_rule4_git() {
let input = r#"[package]
name = "demo"
version = "0.1.0"
[patch.crates-io]
demo = { git = "https://example.com/demo" }
"#;
let tmp = tempfile::tempdir().expect("tempdir for Rule 4 git test");
let upstream_manifest = tmp.path().join("Cargo.toml");
std::fs::write(&upstream_manifest, input).expect("writing upstream Cargo.toml");
std::fs::create_dir_all(tmp.path().join("src")).expect("creating src/");
std::fs::write(tmp.path().join("src").join("lib.rs"), "pub fn _stub() {}\n")
.expect("writing src/lib.rs");
let err = materialize_overlay(&upstream_manifest)
.expect_err("Rule 4 git-source must surface as Err(_)");
assert!(
matches!(err, Error::CompatPatchOverrideConflict { .. }),
"Rule 4 git-source must return CompatPatchOverrideConflict; got {err:?}"
);
drop(tmp);
}
#[test]
fn apply_self_patch_rejects_when_upstream_mixed_rule4_mixed() {
let input = r#"[package]
name = "demo"
version = "0.1.0"
[patch.crates-io]
demo = { path = ".", git = "https://example.com/demo" }
"#;
let tmp = tempfile::tempdir().expect("tempdir for Rule 4 mixed test");
let upstream_manifest = tmp.path().join("Cargo.toml");
std::fs::write(&upstream_manifest, input).expect("writing upstream Cargo.toml");
std::fs::create_dir_all(tmp.path().join("src")).expect("creating src/");
std::fs::write(tmp.path().join("src").join("lib.rs"), "pub fn _stub() {}\n")
.expect("writing src/lib.rs");
let err = materialize_overlay(&upstream_manifest)
.expect_err("Rule 4 mixed must surface as Err(_)");
assert!(
matches!(err, Error::CompatPatchOverrideConflict { .. }),
"Rule 4 mixed must return CompatPatchOverrideConflict; got {err:?}"
);
drop(tmp);
}
#[test]
fn apply_self_patch_preserves_other_crate_patches_when_remap_or_inject() {
let input = r#"[package]
name = "demo"
version = "0.1.0"
[patch.crates-io]
serde = { git = "https://example.com/serde", branch = "main" }
demo = { path = "." }
"#;
let (tmp, staged_manifest, out) = materialize_for_patch_test(input);
let parsed: toml::Value = toml::from_str(&out).expect("overlay must parse");
let crates_io = parsed
.get("patch")
.and_then(|p| p.get("crates-io"))
.expect("[patch.crates-io] must survive");
let staged_parent = staged_manifest
.parent()
.expect("staged has parent")
.to_string_lossy()
.replace('\\', "/");
let demo_path = crates_io
.get("demo")
.and_then(|d| d.get("path"))
.and_then(|v| v.as_str())
.expect("Rule 2 must emit demo.path");
assert_eq!(
demo_path, staged_parent,
"Rule 2 REMAP must emit staged-overlay-dir"
);
let serde_entry = crates_io
.get("serde")
.expect("serde patch entry must be preserved (Rule 3 no-op)");
assert_eq!(
serde_entry.get("git").and_then(|v| v.as_str()),
Some("https://example.com/serde")
);
assert_eq!(
serde_entry.get("branch").and_then(|v| v.as_str()),
Some("main")
);
assert!(
serde_entry.get("path").is_none(),
"serde patch entry has no path (git-form), must not gain one through the policy"
);
drop(tmp);
}
#[test]
fn lexical_path_normalize_handles_dot_and_trailing_slash() {
let a = lexical_path_normalize_path(Path::new("/work/cxx"));
let b = lexical_path_normalize_path(Path::new("/work/cxx/."));
let c = lexical_path_normalize_path(Path::new("/work/cxx/"));
assert_eq!(a, b, "`/work/cxx` and `/work/cxx/.` must normalize equally");
assert_eq!(a, c, "`/work/cxx` and `/work/cxx/` must normalize equally");
let d = lexical_path_normalize_path(Path::new("/work/cxx/.."));
assert_ne!(
a, d,
"`..` (ParentDir) must be PRESERVED, not collapsed; `/work/cxx/..` must not equal `/work/cxx`"
);
let e = lexical_path_normalize_path(Path::new("/work/cxx/target/lihaaf-overlay"));
assert_ne!(
a, e,
"nested-deeper paths must not equate at the lexical layer"
);
}
#[cfg(unix)]
#[test]
fn lexical_path_normalize_handles_repeated_separators() {
let a = lexical_path_normalize_path(Path::new("/work/cxx"));
let b = lexical_path_normalize_path(Path::new("/work//cxx"));
let c = lexical_path_normalize_path(Path::new("/work///cxx"));
assert_eq!(
a, b,
"`//` must collapse to `/` for lexical-normalize equality (cargo path-source resolution semantics)"
);
assert_eq!(a, c, "multiple separators must also collapse");
}
#[cfg(unix)]
#[test]
fn lexical_path_normalize_does_not_resolve_symlinks() {
let tmp = tempfile::tempdir().expect("tempdir for symlink-normalize test");
let real_dir = tmp.path().join("real");
std::fs::create_dir(&real_dir).expect("creating real/");
let symlink_path = tmp.path().join("alias");
std::os::unix::fs::symlink(&real_dir, &symlink_path).expect("creating symlink");
let real_canon = std::fs::canonicalize(&real_dir).expect("canonicalize real");
let alias_canon = std::fs::canonicalize(&symlink_path).expect("canonicalize alias");
assert_eq!(
real_canon, alias_canon,
"canonicalize SHOULD equate the symlinked paths (sanity check)"
);
let real_norm = lexical_path_normalize_path(&real_dir);
let alias_norm = lexical_path_normalize_path(&symlink_path);
assert_ne!(
real_norm, alias_norm,
"lexical normalize must NOT resolve symlinks; \
this is a documented known limitation (plan §6.11): \
symlinked-equivalent paths fall to Rule 4 REJECT"
);
}
#[cfg(unix)]
#[test]
fn mirror_upstream_rerun_reconciles_stale_entries() {
use std::os::unix::fs::MetadataExt;
let tmp = tempfile::tempdir().expect("tempdir for stale-mirror reconcile test");
let upstream_dir = tmp.path().join("upstream");
std::fs::create_dir(&upstream_dir).expect("creating upstream/");
for sub in ["src", "include", "build"] {
std::fs::create_dir(upstream_dir.join(sub)).expect("creating upstream subdir");
std::fs::write(
upstream_dir.join(sub).join("marker"),
format!("real upstream {sub} marker\n"),
)
.expect("writing marker");
}
std::fs::write(upstream_dir.join("example.txt"), "real upstream bytes\n")
.expect("writing example.txt");
let staged_overlay_dir = tmp.path().join("staged-overlay");
std::fs::create_dir(&staged_overlay_dir).expect("creating staged-overlay/");
std::fs::write(
staged_overlay_dir.join("Cargo.toml"),
"[package]\nname = \"stub\"\n",
)
.expect("seed staged Cargo.toml");
let unrelated_tmp = tempfile::tempdir().expect("tempdir for wrong-target symlink");
std::os::unix::fs::symlink(unrelated_tmp.path(), staged_overlay_dir.join("src"))
.expect("seed wrong-target src symlink");
std::fs::write(
staged_overlay_dir.join("example.txt"),
"stale dummy bytes\n",
)
.expect("seed stale example.txt file");
std::fs::create_dir(staged_overlay_dir.join("build")).expect("seed stale build/ dir");
std::fs::write(
staged_overlay_dir.join("build").join("stale.rs"),
"// stale\n",
)
.expect("seed stale build/stale.rs");
std::fs::write(
staged_overlay_dir.join("include"),
"stale file at include path\n",
)
.expect("seed stale include as file");
mirror_upstream_into_overlay(&upstream_dir, &staged_overlay_dir)
.expect("mirror must reconcile stale state");
let src_meta = std::fs::symlink_metadata(staged_overlay_dir.join("src"))
.expect("staged src must exist");
assert!(
src_meta.file_type().is_symlink(),
"CASE 3: stale wrong-target symlink must be replaced with a fresh canonical symlink"
);
let src_target = std::fs::read_link(staged_overlay_dir.join("src")).expect("readlink src");
assert_eq!(
src_target,
upstream_dir.join("src"),
"CASE 3: new symlink must target upstream/src"
);
let ex_meta = std::fs::symlink_metadata(staged_overlay_dir.join("example.txt"))
.expect("staged example.txt must exist");
assert!(
ex_meta.file_type().is_symlink(),
"CASE 5: stale real file must be replaced with canonical symlink"
);
let ex_content = std::fs::read_to_string(staged_overlay_dir.join("example.txt"))
.expect("read example.txt via symlink");
assert_eq!(
ex_content, "real upstream bytes\n",
"CASE 5: reading through new symlink must yield upstream content, not stale dummy"
);
let build_meta = std::fs::symlink_metadata(staged_overlay_dir.join("build"))
.expect("staged build must exist");
assert!(
build_meta.file_type().is_symlink(),
"CASE 6: stale real directory must be replaced with canonical symlink"
);
let build_target =
std::fs::read_link(staged_overlay_dir.join("build")).expect("readlink build");
assert_eq!(
build_target,
upstream_dir.join("build"),
"CASE 6: new symlink must target upstream/build"
);
let inc_meta = std::fs::symlink_metadata(staged_overlay_dir.join("include"))
.expect("staged include must exist");
assert!(
inc_meta.file_type().is_symlink(),
"CASE 7: stale type-mismatch must be replaced with canonical symlink"
);
let inc_ino_before = std::fs::symlink_metadata(staged_overlay_dir.join("include"))
.unwrap()
.ino();
let build_ino_before = std::fs::symlink_metadata(staged_overlay_dir.join("build"))
.unwrap()
.ino();
std::fs::remove_file(staged_overlay_dir.join("src")).expect("remove canonical src");
let unrelated2 = tempfile::tempdir().expect("tempdir for second wrong-target");
std::os::unix::fs::symlink(unrelated2.path(), staged_overlay_dir.join("src"))
.expect("seed second wrong-target src");
mirror_upstream_into_overlay(&upstream_dir, &staged_overlay_dir)
.expect("second mirror must succeed");
let inc_ino_after = std::fs::symlink_metadata(staged_overlay_dir.join("include"))
.unwrap()
.ino();
let build_ino_after = std::fs::symlink_metadata(staged_overlay_dir.join("build"))
.unwrap()
.ino();
assert_eq!(
inc_ino_before, inc_ino_after,
"CASE 12: canonical include/ symlink must be skipped (inode preserved) under mixed-state rerun"
);
assert_eq!(
build_ino_before, build_ino_after,
"CASE 12: canonical build/ symlink must be skipped under mixed-state rerun"
);
let src_target =
std::fs::read_link(staged_overlay_dir.join("src")).expect("readlink src after rerun");
assert_eq!(
src_target,
upstream_dir.join("src"),
"CASE 12: reintroduced wrong-target src/ must be re-created with correct target (CASE 3 reconcile)"
);
drop(tmp);
}
#[test]
fn mirror_copy_fallback_exact_sync_removes_destination_only_files() {
let tmp = tempfile::tempdir().expect("tempdir for copy-fallback test");
let upstream_src = tmp.path().join("upstream-src");
std::fs::create_dir(&upstream_src).expect("creating upstream-src/");
std::fs::write(upstream_src.join("a.rs"), "// upstream a.rs\n").expect("write a.rs");
std::fs::write(upstream_src.join("b.rs"), "// upstream b.rs\n").expect("write b.rs");
let staged_src = tmp.path().join("staged-src");
copy_fallback(&upstream_src, &staged_src).expect("first copy must succeed");
assert!(staged_src.join("a.rs").exists());
assert!(staged_src.join("b.rs").exists());
std::fs::remove_file(upstream_src.join("b.rs")).expect("remove upstream b.rs");
copy_fallback(&upstream_src, &staged_src).expect("second copy must succeed");
assert!(
staged_src.join("a.rs").exists(),
"CASE 6 copy-fallback exact-sync: surviving upstream file must remain"
);
assert!(
!staged_src.join("b.rs").exists(),
"CASE 6 copy-fallback exact-sync: destination-only b.rs MUST be removed (decision 5)"
);
drop(tmp);
}
fn synthesize_workspace_root(
members_toml: &str,
exclude_toml: Option<&str>,
extra_workspace_toml: &str,
) -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir for workspace-member resolver test");
let ws_root_manifest = tmp.path().join("Cargo.toml");
let exclude_part = exclude_toml
.map(|s| format!("exclude = {s}\n"))
.unwrap_or_default();
let toml_text = format!(
"[workspace]\nmembers = {members_toml}\n{exclude_part}{extra_workspace_toml}\n"
);
std::fs::write(&ws_root_manifest, toml_text).expect("write workspace-root Cargo.toml");
(tmp, ws_root_manifest)
}
fn write_member_manifest(root: &Path, rel: &str, package_name: &str, extra: &str) {
let dir = root.join(rel);
std::fs::create_dir_all(&dir).expect("create member dir");
let toml_text =
format!("[package]\nname = \"{package_name}\"\nversion = \"0.1.0\"\n{extra}");
std::fs::write(dir.join("Cargo.toml"), toml_text).expect("write member Cargo.toml");
}
#[test]
fn resolve_workspace_member_manifest_succeeds_on_literal_member() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["axum"]"#, None, "");
write_member_manifest(tmp.path(), "axum", "axum", "");
let (member_manifest, _ws_value) = resolve_workspace_member_manifest(&ws_manifest, "axum")
.expect("literal member must resolve");
assert_eq!(member_manifest, tmp.path().join("axum").join("Cargo.toml"));
drop(tmp);
}
#[test]
fn resolve_workspace_member_manifest_succeeds_on_glob_match() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["axum-*"]"#, None, "");
write_member_manifest(tmp.path(), "axum-macros", "axum-macros", "");
write_member_manifest(tmp.path(), "axum-core", "axum-core", "");
let (member_manifest, _ws_value) =
resolve_workspace_member_manifest(&ws_manifest, "axum-macros")
.expect("glob match for axum-macros must resolve");
assert_eq!(
member_manifest,
tmp.path().join("axum-macros").join("Cargo.toml")
);
drop(tmp);
}
#[test]
fn resolve_workspace_member_manifest_matches_by_package_name_not_dir_name() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["foo"]"#, None, "");
write_member_manifest(tmp.path(), "foo", "bar", "");
let (member_manifest, _ws_value) = resolve_workspace_member_manifest(&ws_manifest, "bar")
.expect("match-by-package-name must resolve");
assert_eq!(member_manifest, tmp.path().join("foo").join("Cargo.toml"));
assert!(
resolve_workspace_member_manifest(&ws_manifest, "foo").is_err(),
"matching by directory name must NOT resolve when the package name differs"
);
drop(tmp);
}
#[test]
fn resolve_workspace_member_manifest_glob_does_not_match_bare_prefix() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["axum-*"]"#, None, "");
write_member_manifest(tmp.path(), "axum", "axum", "");
let err = resolve_workspace_member_manifest(&ws_manifest, "axum")
.expect_err("glob `axum-*` must NOT match bare `axum`");
match err {
Error::Cli { message, .. } => assert!(
message.contains("no member of workspace"),
"diagnostic must name no-match: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolve_workspace_member_manifest_rejects_when_root_not_workspace_root() {
let tmp = tempfile::tempdir().expect("tempdir for non-workspace-root test");
let single_crate_manifest = tmp.path().join("Cargo.toml");
std::fs::write(
&single_crate_manifest,
"[package]\nname = \"single\"\nversion = \"0.1.0\"\n",
)
.expect("write single-crate Cargo.toml");
let err = resolve_workspace_member_manifest(&single_crate_manifest, "any")
.expect_err("non-workspace-root must be rejected");
match err {
Error::Cli { message, .. } => assert!(
message.contains("workspace root") && message.contains("does not match this shape"),
"diagnostic must name the requirement: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolve_workspace_member_manifest_rejects_when_no_members_array() {
let tmp = tempfile::tempdir().expect("tempdir for no-members test");
let ws_manifest = tmp.path().join("Cargo.toml");
std::fs::write(&ws_manifest, "[workspace]\nresolver = \"2\"\n")
.expect("write empty workspace Cargo.toml");
let err = resolve_workspace_member_manifest(&ws_manifest, "any")
.expect_err("workspace root without `members` must be rejected");
match err {
Error::Cli { message, .. } => assert!(
message.contains("no `[workspace.members]` array"),
"diagnostic must name the missing array: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolve_workspace_member_manifest_no_match_lists_scanned_members() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["a", "b"]"#, None, "");
write_member_manifest(tmp.path(), "a", "a", "");
write_member_manifest(tmp.path(), "b", "b", "");
let err = resolve_workspace_member_manifest(&ws_manifest, "c")
.expect_err("missing package must produce no-match");
match err {
Error::Cli { message, .. } => {
assert!(message.contains("a"), "diagnostic must list `a`: {message}");
assert!(message.contains("b"), "diagnostic must list `b`: {message}");
assert!(
message.contains("Members scanned"),
"diagnostic must label the list: {message}"
);
}
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolve_workspace_member_manifest_skips_unparseable_member_manifest() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["a"]"#, None, "");
let a_dir = tmp.path().join("a");
std::fs::create_dir_all(&a_dir).expect("create a/");
std::fs::write(a_dir.join("Cargo.toml"), "[package\nname = \"a\"\n")
.expect("write malformed a/Cargo.toml");
let err = resolve_workspace_member_manifest(&ws_manifest, "a")
.expect_err("malformed member must be skipped → no-match");
match err {
Error::Cli { message, .. } => assert!(
message.contains("no member"),
"diagnostic must be no-match: {message}"
),
other => panic!("expected Cli no-match, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolve_workspace_member_manifest_skips_missing_member_directory() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["a"]"#, None, "");
let err = resolve_workspace_member_manifest(&ws_manifest, "a")
.expect_err("missing dir must produce no-match");
match err {
Error::Cli { message, .. } => assert!(
message.contains("no member"),
"diagnostic must be no-match: {message}"
),
other => panic!("expected Cli no-match, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolve_workspace_member_manifest_workspace_inheritance_captured() {
let extra = "[workspace.package]\nedition = \"2021\"\n\
[workspace.dependencies]\nserde = \"1.0\"\n";
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["pkg-a"]"#, None, extra);
write_member_manifest(tmp.path(), "pkg-a", "pkg-a", "");
let (_manifest, ws_value) = resolve_workspace_member_manifest(&ws_manifest, "pkg-a")
.expect("inheritance-capturing resolve must succeed");
let ws_table = ws_value.as_table().unwrap();
let workspace = ws_table.get("workspace").unwrap().as_table().unwrap();
assert!(workspace.contains_key("package"));
assert!(workspace.contains_key("dependencies"));
let dr = DualRoot {
workspace_root: tmp.path().to_path_buf(),
workspace_root_manifest: ws_manifest.clone(),
member_root: tmp.path().join("pkg-a"),
member_manifest: tmp.path().join("pkg-a").join("Cargo.toml"),
workspace_member_context: Some(WorkspaceMemberContext {
workspace_root_manifest: ws_manifest.clone(),
workspace_root_value: ws_value,
}),
};
assert_eq!(dr.workspace_root_manifest, ws_manifest);
assert!(dr.workspace_member_context.is_some());
drop(tmp);
}
#[test]
fn override_workspace_skips_branch_2_with_workspace_member_context() {
let tmp = tempfile::tempdir().expect("tempdir for Branch 2 suppression test");
let ws_root_manifest = tmp.path().join("Cargo.toml");
std::fs::write(&ws_root_manifest, "[workspace]\nmembers = [\"member\"]\n")
.expect("write workspace root");
let member_dir = tmp.path().join("member");
std::fs::create_dir_all(&member_dir).expect("create member dir");
let member_manifest = member_dir.join("Cargo.toml");
std::fs::write(
&member_manifest,
"[package]\nname = \"member\"\nversion = \"0.1.0\"\n",
)
.expect("write member manifest");
let mut top_none: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("member".into()));
top_none.insert("package".to_string(), toml::Value::Table(pkg.clone()));
let err = override_workspace_inheritance(&mut top_none, &member_manifest, None)
.expect_err("Branch 2 must fire without ctx");
assert!(matches!(err, Error::Cli { .. }));
let mut top_ctx: toml::map::Map<String, toml::Value> = toml::map::Map::new();
top_ctx.insert("package".to_string(), toml::Value::Table(pkg));
let ws_value: toml::Value =
toml::from_str("[workspace]\nmembers = [\"member\"]\n").unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: ws_root_manifest.clone(),
workspace_root_value: ws_value,
};
override_workspace_inheritance(&mut top_ctx, &member_manifest, Some(&ctx))
.expect("Branch 2 must be suppressed with ctx Some");
drop(tmp);
}
#[test]
fn override_workspace_skips_branch_3_with_workspace_member_context() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
let mut rust_version = toml::map::Map::new();
rust_version.insert("workspace".to_string(), toml::Value::Boolean(true));
pkg.insert("rust-version".to_string(), toml::Value::Table(rust_version));
pkg.insert("name".to_string(), toml::Value::String("m".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let ws_value: toml::Value = toml::from_str(
"[workspace]\nmembers = [\"m\"]\n[workspace.package]\nrust-version = \"1.65\"\n",
)
.unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/tmp/nonexistent/Cargo.toml"),
workspace_root_value: ws_value,
};
let isolated_manifest = PathBuf::from("/this/path/does/not/exist/Cargo.toml");
override_workspace_inheritance(&mut top, &isolated_manifest, Some(&ctx))
.expect("Branch 3 must be suppressed with ctx Some");
}
#[test]
fn override_workspace_still_rejects_branch_1_explicit_member_with_context() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("m".into()));
pkg.insert("workspace".to_string(), toml::Value::String("../".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let ws_value: toml::Value = toml::from_str("[workspace]\nmembers = [\"m\"]\n").unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/tmp/x/Cargo.toml"),
workspace_root_value: ws_value,
};
let err =
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path(), Some(&ctx))
.expect_err("explicit `[package].workspace` MUST still REJECT with ctx Some");
match err {
Error::Cli { message, .. } => assert!(
message.contains("workspace member") && message.contains("[package].workspace"),
"diagnostic must name the explicit-member case: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
}
#[test]
fn apply_workspace_member_inheritance_carries_workspace_dependencies() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let ws_value: toml::Value = toml::from_str(
"[workspace]\nmembers = [\"m\"]\n\
[workspace.dependencies]\nserde = \"1.0\"\n",
)
.unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let member_manifest = PathBuf::from("/ws/m/Cargo.toml");
apply_workspace_member_inheritance(&mut top, &ctx, &member_manifest)
.expect("carry-down must succeed");
let workspace = top.get("workspace").unwrap().as_table().unwrap();
let deps = workspace
.get("dependencies")
.and_then(|v| v.as_table())
.expect("[workspace.dependencies] must be present after carry-down");
assert!(deps.contains_key("serde"), "serde dep must be carried");
}
#[test]
fn apply_workspace_member_inheritance_carries_workspace_package_lints_metadata() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let ws_value: toml::Value = toml::from_str(
"[workspace]\nmembers = [\"m\"]\n\
[workspace.package]\nedition = \"2021\"\n\
[workspace.lints.rust]\nunsafe_code = \"forbid\"\n\
[workspace.metadata.docs]\ncustom = \"value\"\n",
)
.unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
apply_workspace_member_inheritance(&mut top, &ctx, &PathBuf::from("/ws/m/Cargo.toml"))
.expect("carry-down must succeed");
let workspace = top.get("workspace").unwrap().as_table().unwrap();
assert!(workspace.contains_key("package"));
assert!(workspace.contains_key("lints"));
assert!(workspace.contains_key("metadata"));
}
#[test]
fn apply_workspace_member_inheritance_strips_membership_keys() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("m".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let ws_value: toml::Value = toml::from_str(
"[workspace]\nmembers = [\"m\"]\nexclude = [\"old\"]\ndefault-members = [\"m\"]\n",
)
.unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
apply_workspace_member_inheritance(&mut top, &ctx, &PathBuf::from("/ws/m/Cargo.toml"))
.expect("carry-down must succeed");
override_workspace_inheritance(&mut top, &PathBuf::from("/ws/m/Cargo.toml"), Some(&ctx))
.expect("override must succeed");
let workspace = top.get("workspace").unwrap().as_table().unwrap();
assert!(
!workspace.contains_key("members"),
"members must be stripped"
);
assert!(
!workspace.contains_key("exclude"),
"exclude must be stripped"
);
assert!(
!workspace.contains_key("default-members"),
"default-members must be stripped"
);
}
#[test]
fn apply_workspace_member_inheritance_carries_workspace_root_patch_crates_io() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("pkg-macros".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let ws_value: toml::Value = toml::from_str(
"[workspace]\nmembers = [\"pkg-macros\"]\n\
[patch.crates-io.other-dep]\npath = \"vendored/other-dep\"\n",
)
.unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
apply_workspace_member_inheritance(
&mut top,
&ctx,
&PathBuf::from("/ws/pkg-macros/Cargo.toml"),
)
.expect("carry-down must succeed");
apply_self_patch_policy(
&mut top,
Some("pkg-macros"),
&PathBuf::from("/ws/pkg-macros"),
&PathBuf::from("/ws/pkg-macros/target/lihaaf-overlay"),
Some(&ctx),
)
.expect("apply_self_patch_policy must succeed");
let patch = top
.get("patch")
.and_then(|p| p.get("crates-io"))
.and_then(|c| c.as_table())
.expect("`[patch.crates-io]` must be present after carry-down");
let other_dep_path = patch
.get("other-dep")
.and_then(|v| v.get("path"))
.and_then(|v| v.as_str())
.expect("[patch.crates-io.other-dep].path must exist");
assert!(
other_dep_path.contains("vendored/other-dep"),
"non-self entry retains workspace-root-relative segment: {other_dep_path}"
);
assert!(
other_dep_path.starts_with("/ws"),
"non-self entry must be absolutized against workspace_root: {other_dep_path}"
);
let self_path = patch
.get("pkg-macros")
.and_then(|v| v.get("path"))
.and_then(|v| v.as_str())
.expect("[patch.crates-io.pkg-macros].path must exist (Rule 1 INJECT)");
assert!(
self_path.ends_with("lihaaf-overlay"),
"self-entry must point at the staged-overlay dir: {self_path}"
);
}
#[test]
fn dual_root_routing_baseline_cwd_is_workspace_root_member_consumers_use_member_root() {
let collapse = DualRoot {
workspace_root: PathBuf::from("/single/crate"),
workspace_root_manifest: PathBuf::from("/single/crate/Cargo.toml"),
member_root: PathBuf::from("/single/crate"),
member_manifest: PathBuf::from("/single/crate/Cargo.toml"),
workspace_member_context: None,
};
assert_eq!(collapse.workspace_root, collapse.member_root);
assert_eq!(collapse.workspace_root_manifest, collapse.member_manifest);
assert!(collapse.workspace_member_context.is_none());
let ws_value: toml::Value =
toml::from_str("[workspace]\nmembers = [\"axum-macros\"]\n").unwrap();
let dual = DualRoot {
workspace_root: PathBuf::from("/ws"),
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
member_root: PathBuf::from("/ws/axum-macros"),
member_manifest: PathBuf::from("/ws/axum-macros/Cargo.toml"),
workspace_member_context: Some(WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
}),
};
assert_ne!(dual.workspace_root, dual.member_root);
assert_ne!(dual.workspace_root_manifest, dual.member_manifest);
assert!(dual.workspace_member_context.is_some());
assert_eq!(dual.workspace_root, PathBuf::from("/ws"));
assert_eq!(dual.member_root, PathBuf::from("/ws/axum-macros"));
}
#[test]
fn workspace_root_path_absolutization_for_dependencies_path() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let ws_value: toml::Value = toml::from_str(
"[workspace]\nmembers = [\"m\"]\n\
[workspace.dependencies]\n\
foo = { path = \"crates/foo\" }\n\
bar = { git = \"https://example.com/bar.git\" }\n",
)
.unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/abs/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
apply_workspace_member_inheritance(&mut top, &ctx, &PathBuf::from("/abs/ws/m/Cargo.toml"))
.expect("carry-down must succeed");
let deps = top
.get("workspace")
.and_then(|w| w.get("dependencies"))
.and_then(|d| d.as_table())
.expect("[workspace.dependencies] must be present");
let foo_path = deps
.get("foo")
.and_then(|v| v.get("path"))
.and_then(|v| v.as_str())
.expect("foo.path must exist");
assert!(
foo_path.starts_with("/abs/ws/"),
"foo.path must be absolutized against workspace_root: {foo_path}"
);
assert!(
foo_path.ends_with("crates/foo"),
"foo.path must end with the relative segment: {foo_path}"
);
let bar_git = deps
.get("bar")
.and_then(|v| v.get("git"))
.and_then(|v| v.as_str())
.expect("bar.git must exist");
assert_eq!(bar_git, "https://example.com/bar.git");
}
#[test]
fn workspace_root_path_absolutization_for_package_readme_license_file() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let ws_value: toml::Value = toml::from_str(
"[workspace]\nmembers = [\"m\"]\n\
[workspace.package]\n\
readme = \"../../README.md\"\n\
license-file = \"LICENSE-MIT\"\n",
)
.unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/abs/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
apply_workspace_member_inheritance(&mut top, &ctx, &PathBuf::from("/abs/ws/m/Cargo.toml"))
.expect("carry-down must succeed");
let pkg = top
.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.as_table())
.expect("[workspace.package] must be present");
let readme = pkg.get("readme").and_then(|v| v.as_str()).expect("readme");
assert!(
readme.starts_with("/abs/ws/"),
"readme must be absolutized: {readme}"
);
assert!(readme.ends_with("README.md"), "readme suffix: {readme}");
let license = pkg
.get("license-file")
.and_then(|v| v.as_str())
.expect("license-file");
assert!(
license.starts_with("/abs/ws/"),
"license-file must be absolutized: {license}"
);
assert!(
license.ends_with("LICENSE-MIT"),
"license-file suffix: {license}"
);
}
#[test]
fn option_h_root_first_member_second_with_workspace_root_self_patch_entry() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("pkg-name".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let ws_value: toml::Value = toml::from_str(
"[workspace]\nmembers = [\"pkg-name\"]\n\
[patch.crates-io.pkg-name]\npath = \"../local-fork\"\n",
)
.unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/abs/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
apply_workspace_member_inheritance(
&mut top,
&ctx,
&PathBuf::from("/abs/ws/pkg-name/Cargo.toml"),
)
.expect("carry-down must succeed");
let staged_overlay_dir = PathBuf::from("/abs/ws/pkg-name/target/lihaaf-overlay");
apply_self_patch_policy(
&mut top,
Some("pkg-name"),
&PathBuf::from("/abs/ws/pkg-name"),
&staged_overlay_dir,
Some(&ctx),
)
.expect("apply_self_patch_policy must succeed");
let self_path = top
.get("patch")
.and_then(|p| p.get("crates-io"))
.and_then(|c| c.get("pkg-name"))
.and_then(|e| e.get("path"))
.and_then(|v| v.as_str())
.expect("self-entry must have a path");
assert!(
self_path.ends_with("lihaaf-overlay"),
"Rule 2 REMAP must produce overlay-root form: {self_path}"
);
assert!(
!self_path.contains("local-fork"),
"self-entry must NOT contain the upstream's intermediate path: {self_path}"
);
}
#[test]
fn option_h_rejects_member_local_patch_crates_io() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut patch_table = toml::map::Map::new();
let mut crates_io = toml::map::Map::new();
let mut entry = toml::map::Map::new();
entry.insert("path".to_string(), toml::Value::String("./local".into()));
crates_io.insert("some-dep".to_string(), toml::Value::Table(entry));
patch_table.insert("crates-io".to_string(), toml::Value::Table(crates_io));
top.insert("patch".to_string(), toml::Value::Table(patch_table));
let ws_value: toml::Value = toml::from_str("[workspace]\nmembers = [\"m\"]\n").unwrap();
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let err =
apply_workspace_member_inheritance(&mut top, &ctx, &PathBuf::from("/ws/m/Cargo.toml"))
.expect_err("member-local [patch.crates-io] must be rejected");
match err {
Error::Cli { message, .. } => assert!(
message.contains("cargo does not permit `[patch]` in workspace members"),
"diagnostic must name the rejection rationale: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
}
#[test]
fn resolver_glob_crates_star_finds_nested_member() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["crates/*"]"#, None, "");
write_member_manifest(tmp.path(), "crates/foo", "foo", "");
let (member_manifest, _) =
resolve_workspace_member_manifest(&ws_manifest, "foo").expect("crates/* must match");
assert_eq!(
member_manifest,
tmp.path().join("crates").join("foo").join("Cargo.toml")
);
drop(tmp);
}
#[test]
fn resolver_glob_crates_explicit_nested_literal_finds_member() {
let (tmp, ws_manifest) =
synthesize_workspace_root(r#"["crates/foo", "tools/bar"]"#, None, "");
write_member_manifest(tmp.path(), "crates/foo", "foo", "");
write_member_manifest(tmp.path(), "tools/bar", "bar", "");
let (foo_manifest, _) =
resolve_workspace_member_manifest(&ws_manifest, "foo").expect("crates/foo literal");
assert_eq!(
foo_manifest,
tmp.path().join("crates").join("foo").join("Cargo.toml")
);
let (bar_manifest, _) =
resolve_workspace_member_manifest(&ws_manifest, "bar").expect("tools/bar literal");
assert_eq!(
bar_manifest,
tmp.path().join("tools").join("bar").join("Cargo.toml")
);
drop(tmp);
}
#[test]
fn resolver_glob_rejects_deep_glob() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["**/*"]"#, None, "");
let err = resolve_workspace_member_manifest(&ws_manifest, "any")
.expect_err("deep glob must be rejected");
match err {
Error::Cli { message, .. } => assert!(
message.contains("cargo does not support `**` in `[workspace.members]`"),
"diagnostic must name `**`: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolver_glob_rejects_glob_in_non_final_segment() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["*/foo"]"#, None, "");
let err = resolve_workspace_member_manifest(&ws_manifest, "foo")
.expect_err("glob in non-final segment must be rejected");
match err {
Error::Cli { message, .. } => assert!(
message.contains("only the LAST segment may contain glob metachars"),
"diagnostic must name the constraint: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolver_glob_normalizes_trailing_slash() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["axum-macros/"]"#, None, "");
write_member_manifest(tmp.path(), "axum-macros", "axum-macros", "");
let (member_manifest, _) = resolve_workspace_member_manifest(&ws_manifest, "axum-macros")
.expect("trailing slash must normalize");
assert_eq!(
member_manifest,
tmp.path().join("axum-macros").join("Cargo.toml")
);
drop(tmp);
}
#[test]
fn resolver_glob_rejects_absolute_path_member() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["/usr/local/foo"]"#, None, "");
let err = resolve_workspace_member_manifest(&ws_manifest, "foo")
.expect_err("absolute-path member must be rejected");
match err {
Error::Cli { message, .. } => assert!(
message.contains("`[workspace.members]` entries are workspace-relative paths only"),
"diagnostic must name the constraint: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolver_glob_rejects_parent_traversal_member() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["../sibling"]"#, None, "");
let err = resolve_workspace_member_manifest(&ws_manifest, "sibling")
.expect_err("parent traversal must be rejected");
match err {
Error::Cli { message, .. } => assert!(
message.contains("members must be descendants of the workspace root"),
"diagnostic must name the constraint: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolver_subtracts_workspace_exclude_set() {
let (tmp, ws_manifest) =
synthesize_workspace_root(r#"["pkg-*"]"#, Some(r#"["pkg-private"]"#), "");
write_member_manifest(tmp.path(), "pkg-public", "pkg-public", "");
write_member_manifest(tmp.path(), "pkg-private", "pkg-private", "");
let (manifest_public, _) = resolve_workspace_member_manifest(&ws_manifest, "pkg-public")
.expect("pkg-public must resolve (not in exclude)");
assert!(manifest_public.ends_with("pkg-public/Cargo.toml"));
let err = resolve_workspace_member_manifest(&ws_manifest, "pkg-private")
.expect_err("pkg-private must be excluded → no-match");
assert!(matches!(err, Error::Cli { .. }));
drop(tmp);
}
#[test]
fn resolver_default_members_does_not_filter_package_resolution() {
let (tmp, ws_manifest) =
synthesize_workspace_root(r#"["a", "b"]"#, None, "default-members = [\"a\"]\n");
write_member_manifest(tmp.path(), "a", "a", "");
write_member_manifest(tmp.path(), "b", "b", "");
let (manifest_a, _) =
resolve_workspace_member_manifest(&ws_manifest, "a").expect("a must resolve");
assert!(manifest_a.ends_with("a/Cargo.toml"));
let (manifest_b, _) =
resolve_workspace_member_manifest(&ws_manifest, "b").expect("b must resolve");
assert!(manifest_b.ends_with("b/Cargo.toml"));
drop(tmp);
}
#[test]
fn resolver_excluded_package_diagnostic_lists_excluded_name() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["a", "b"]"#, Some(r#"["b"]"#), "");
write_member_manifest(tmp.path(), "a", "a", "");
write_member_manifest(tmp.path(), "b", "b", "");
let err = resolve_workspace_member_manifest(&ws_manifest, "b")
.expect_err("b is excluded → no-match");
match err {
Error::Cli { message, .. } => {
assert!(
message.contains("`b`") || message.contains("\"b\""),
"diagnostic should reference the missing package name: {message}"
);
assert!(
message.contains("excluded") || message.contains("exclude"),
"diagnostic should explain exclude subtraction: {message}"
);
}
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolver_does_not_descend_into_nested_workspace() {
let (tmp, ws_manifest) =
synthesize_workspace_root(r#"["outer-pkg", "nested-ws"]"#, None, "");
write_member_manifest(tmp.path(), "outer-pkg", "outer-pkg", "");
let nested_dir = tmp.path().join("nested-ws");
std::fs::create_dir_all(&nested_dir).expect("create nested-ws dir");
std::fs::write(
nested_dir.join("Cargo.toml"),
"[workspace]\nmembers = [\"inner-pkg\"]\n",
)
.expect("write nested workspace manifest");
write_member_manifest(tmp.path(), "nested-ws/inner-pkg", "inner-pkg", "");
let err = resolve_workspace_member_manifest(&ws_manifest, "inner-pkg")
.expect_err("nested-ws.inner-pkg must NOT be found by outer resolver");
assert!(matches!(err, Error::Cli { .. }));
let err2 = resolve_workspace_member_manifest(&ws_manifest, "nested-ws")
.expect_err("nested-ws (pure-virtual) must NOT match by name");
assert!(matches!(err2, Error::Cli { .. }));
drop(tmp);
}
#[test]
fn resolver_duplicate_package_after_glob_expansion_returns_multiple_match_error() {
let (tmp1, ws_manifest1) = synthesize_workspace_root(r#"["pkg-a", "pkg-*"]"#, None, "");
write_member_manifest(tmp1.path(), "pkg-a", "pkg-a", "");
let (manifest_a, _) = resolve_workspace_member_manifest(&ws_manifest1, "pkg-a")
.expect("overlapping literal + glob must dedup, not multi-match");
assert!(manifest_a.ends_with("pkg-a/Cargo.toml"));
drop(tmp1);
let (tmp2, ws_manifest2) =
synthesize_workspace_root(r#"["pkg-a", "pkg-a-clone"]"#, None, "");
write_member_manifest(tmp2.path(), "pkg-a", "pkg-a", "");
write_member_manifest(tmp2.path(), "pkg-a-clone", "pkg-a", "");
let err = resolve_workspace_member_manifest(&ws_manifest2, "pkg-a")
.expect_err("two dirs claiming pkg-a must produce multiple-match");
match err {
Error::Cli { message, .. } => assert!(
message.contains("multiple workspace members claim"),
"diagnostic must name the multiple-match case: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp2);
}
#[test]
fn resolver_rejects_package_plus_workspace_root_per_v01_scope() {
let tmp = tempfile::tempdir().expect("tempdir for package+workspace test");
let manifest = tmp.path().join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"the-root-pkg\"\nversion = \"0.1.0\"\n\
[workspace]\nmembers = [\"the-member\"]\n",
)
.expect("write package+workspace manifest");
let err = resolve_workspace_member_manifest(&manifest, "the-member")
.expect_err("package+workspace root must be rejected per v0.1.0 scope");
match err {
Error::Cli { message, .. } => assert!(
message.contains("workspace root") && message.contains("without `[package]`"),
"diagnostic must name the virtual-workspace requirement: {message}"
),
other => panic!("expected Cli error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn lexical_normalize_pathbuf_collapses_dot_and_trailing_slash_forms() {
let root = Path::new("/work/ws");
let raw = [
"pkg-a", "./pkg-a", "pkg-a/", "pkg-a/.", ".//pkg-a", "./pkg-a/",
];
let canonical: Vec<PathBuf> = raw
.iter()
.map(|s| lexical_normalize_pathbuf(&root.join(s)))
.collect();
let first = canonical[0].clone();
for (idx, p) in canonical.iter().enumerate() {
assert_eq!(
p,
&first,
"shape {} (`{}`) must lexically-normalize to the same PathBuf as `pkg-a`; \
got `{}`",
idx,
raw[idx],
p.display()
);
}
let with_parent = lexical_normalize_pathbuf(&root.join("pkg-a/.."));
assert_ne!(
with_parent, first,
"`pkg-a/..` must NOT collapse to `pkg-a` (ParentDir preserved per plan §6.11)"
);
}
#[test]
fn lexical_normalize_pathbuf_returns_dot_for_all_curdir_input() {
let a = lexical_normalize_pathbuf(Path::new("."));
let b = lexical_normalize_pathbuf(Path::new("./."));
let c = lexical_normalize_pathbuf(Path::new("././"));
assert_eq!(a, PathBuf::from("."));
assert_eq!(b, PathBuf::from("."));
assert_eq!(c, PathBuf::from("."));
}
#[test]
fn resolver_dot_prefix_member_resolves_same_as_bare() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["./pkg-a"]"#, None, "");
write_member_manifest(tmp.path(), "pkg-a", "pkg-a", "");
let (member_manifest, _ws_value) = resolve_workspace_member_manifest(&ws_manifest, "pkg-a")
.expect("`./pkg-a` member shape must resolve identically to `pkg-a`");
assert_eq!(member_manifest, tmp.path().join("pkg-a").join("Cargo.toml"));
drop(tmp);
}
#[test]
fn resolver_trailing_slash_member_resolves_same_as_bare() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["pkg-a/"]"#, None, "");
write_member_manifest(tmp.path(), "pkg-a", "pkg-a", "");
let (member_manifest, _ws_value) = resolve_workspace_member_manifest(&ws_manifest, "pkg-a")
.expect("`pkg-a/` member shape must resolve identically to `pkg-a`");
assert_eq!(member_manifest, tmp.path().join("pkg-a").join("Cargo.toml"));
drop(tmp);
}
#[test]
fn resolver_curdir_tail_member_resolves_same_as_bare() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["pkg-a/."]"#, None, "");
write_member_manifest(tmp.path(), "pkg-a", "pkg-a", "");
let (member_manifest, _ws_value) = resolve_workspace_member_manifest(&ws_manifest, "pkg-a")
.expect("`pkg-a/.` member shape must resolve identically to `pkg-a`");
assert_eq!(member_manifest, tmp.path().join("pkg-a").join("Cargo.toml"));
drop(tmp);
}
#[test]
fn resolver_dot_prefix_member_excluded_by_bare_exclude() {
let (tmp, ws_manifest) =
synthesize_workspace_root(r#"["./pkg-a"]"#, Some(r#"["pkg-a"]"#), "");
write_member_manifest(tmp.path(), "pkg-a", "pkg-a", "");
let err = resolve_workspace_member_manifest(&ws_manifest, "pkg-a").expect_err(
"`./pkg-a` member must be subtracted by `pkg-a` exclude — no resolver match",
);
match err {
Error::Cli { message, .. } => assert!(
message.contains("no member") || message.contains("subtracted"),
"diagnostic must indicate no member matched: {message}"
),
other => panic!("expected Cli no-match error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolver_bare_member_excluded_by_dot_prefix_exclude() {
let (tmp, ws_manifest) =
synthesize_workspace_root(r#"["pkg-a"]"#, Some(r#"["./pkg-a"]"#), "");
write_member_manifest(tmp.path(), "pkg-a", "pkg-a", "");
let err = resolve_workspace_member_manifest(&ws_manifest, "pkg-a").expect_err(
"`pkg-a` member must be subtracted by `./pkg-a` exclude — no resolver match",
);
match err {
Error::Cli { message, .. } => assert!(
message.contains("no member") || message.contains("subtracted"),
"diagnostic must indicate no member matched: {message}"
),
other => panic!("expected Cli no-match error, got {other:?}"),
}
drop(tmp);
}
#[test]
fn resolver_dot_prefix_glob_member_resolves() {
let (tmp, ws_manifest) = synthesize_workspace_root(r#"["./pkg-*"]"#, None, "");
write_member_manifest(tmp.path(), "pkg-a", "pkg-a", "");
let (member_manifest, _ws_value) = resolve_workspace_member_manifest(&ws_manifest, "pkg-a")
.expect("`./pkg-*` glob shape must resolve identically to `pkg-*`");
assert_eq!(member_manifest, tmp.path().join("pkg-a").join("Cargo.toml"));
drop(tmp);
}
#[test]
fn resolver_overlapping_shapes_dedup_to_single_match() {
let (tmp, ws_manifest) =
synthesize_workspace_root(r#"["./pkg-a", "pkg-a", "pkg-a/"]"#, None, "");
write_member_manifest(tmp.path(), "pkg-a", "pkg-a", "");
let (member_manifest, _ws_value) = resolve_workspace_member_manifest(&ws_manifest, "pkg-a")
.expect("overlapping `./pkg-a`/`pkg-a`/`pkg-a/` must dedup to one match");
assert_eq!(member_manifest, tmp.path().join("pkg-a").join("Cargo.toml"));
drop(tmp);
}
fn assert_inheritance_rejects(workspace_root_toml: &str, expected_phrase: &str) {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let ws_value: toml::Value =
toml::from_str(workspace_root_toml).expect("workspace-root TOML parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let member_manifest = PathBuf::from("/ws/m/Cargo.toml");
let err = apply_workspace_member_inheritance(&mut top, &ctx, &member_manifest)
.expect_err("non-table workspace-root key must reject as TomlParse");
match err {
Error::TomlParse { path, message } => {
assert_eq!(
path,
PathBuf::from("/ws/Cargo.toml"),
"error must name the workspace-root manifest path"
);
assert!(
message.contains(expected_phrase),
"diagnostic must mention `{expected_phrase}`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn workspace_root_workspace_dependencies_non_table_is_rejected() {
assert_inheritance_rejects(
"[workspace]\nmembers = [\"m\"]\ndependencies = \"oops\"\n",
"`[workspace.dependencies]` must be a table",
);
}
#[test]
fn workspace_root_workspace_package_non_table_is_rejected() {
assert_inheritance_rejects(
"[workspace]\nmembers = [\"m\"]\npackage = 42\n",
"`[workspace.package]` must be a table",
);
}
#[test]
fn workspace_root_replace_non_table_is_rejected() {
assert_inheritance_rejects(
"replace = [\"oops\"]\n[workspace]\nmembers = [\"m\"]\n",
"`[replace]` must be a table",
);
}
#[test]
fn workspace_root_profile_non_table_is_rejected() {
assert_inheritance_rejects(
"profile = true\n[workspace]\nmembers = [\"m\"]\n",
"`[profile]` must be a table",
);
}
#[test]
fn workspace_root_patch_non_table_is_rejected() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("m".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let ws_value: toml::Value =
toml::from_str("patch = \"oops\"\n[workspace]\nmembers = [\"m\"]\n")
.expect("workspace-root TOML parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let err = apply_self_patch_policy(
&mut top,
Some("m"),
Path::new("/ws/m"),
Path::new("/ws/m/target/lihaaf-overlay"),
Some(&ctx),
)
.expect_err("workspace-root `[patch]` non-table must reject as TomlParse");
match err {
Error::TomlParse { path, message } => {
assert_eq!(path, PathBuf::from("/ws/Cargo.toml"));
assert!(
message.contains("workspace-root `[patch]` must be a table"),
"diagnostic must name workspace-root `[patch]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn workspace_root_patch_crates_io_non_table_is_rejected() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("m".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let ws_value: toml::Value =
toml::from_str("[workspace]\nmembers = [\"m\"]\n\n[patch]\ncrates-io = \"oops\"\n")
.expect("workspace-root TOML parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let err = apply_self_patch_policy(
&mut top,
Some("m"),
Path::new("/ws/m"),
Path::new("/ws/m/target/lihaaf-overlay"),
Some(&ctx),
)
.expect_err("workspace-root `[patch.crates-io]` non-table must reject as TomlParse");
match err {
Error::TomlParse { path, message } => {
assert_eq!(path, PathBuf::from("/ws/Cargo.toml"));
assert!(
message.contains("workspace-root `[patch.crates-io]` must be a table"),
"diagnostic must name workspace-root `[patch.crates-io]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn inheritance_reference_scan_rejects_non_table_dependencies() {
let mut top = toml::map::Map::new();
top.insert(
"dependencies".to_string(),
toml::Value::String("oops".into()),
);
let err =
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap_err();
match err {
Error::TomlParse { path, message } => {
assert_eq!(path, PathBuf::from("/test/Cargo.toml"));
assert!(
message.contains("`[dependencies` must be a table"),
"diagnostic must name `[dependencies`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn inheritance_reference_scan_rejects_non_table_lints() {
let mut top = toml::map::Map::new();
top.insert("lints".to_string(), toml::Value::Integer(42));
let err =
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap_err();
match err {
Error::TomlParse { message, .. } => {
assert!(
message.contains("`[lints]` must be a table"),
"diagnostic must name `[lints]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn inheritance_reference_scan_rejects_non_table_target_cfg() {
let mut top = toml::map::Map::new();
let mut targets = toml::map::Map::new();
targets.insert(
"cfg(unix)".to_string(),
toml::Value::String("not-a-table".into()),
);
top.insert("target".to_string(), toml::Value::Table(targets));
let err =
manifest_has_inheritance_reference(&top, Path::new("/test/Cargo.toml")).unwrap_err();
match err {
Error::TomlParse { message, .. } => {
assert!(
message.contains("`[target.cfg(unix)]` must be a table"),
"diagnostic must name `[target.cfg(unix)]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn inject_synthetic_metadata_rejects_non_table_package() {
let mut top = toml::map::Map::new();
top.insert("package".to_string(), toml::Value::Boolean(true));
let meta = SyntheticMetadata {
dylib_crate: "stub".to_string(),
extern_crates: vec![],
fixture_dirs: vec![],
allow_lints: vec![],
};
let err =
inject_synthetic_metadata(&mut top, &meta, Path::new("/test/Cargo.toml")).unwrap_err();
match err {
Error::TomlParse { message, path } => {
assert_eq!(path, PathBuf::from("/test/Cargo.toml"));
assert!(
message.contains("`[package]` must be a table"),
"diagnostic must name `[package]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn inject_synthetic_metadata_rejects_non_table_package_metadata() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
pkg.insert("metadata".to_string(), toml::Value::Integer(0));
top.insert("package".to_string(), toml::Value::Table(pkg));
let meta = SyntheticMetadata {
dylib_crate: "stub".to_string(),
extern_crates: vec![],
fixture_dirs: vec![],
allow_lints: vec![],
};
let err =
inject_synthetic_metadata(&mut top, &meta, Path::new("/test/Cargo.toml")).unwrap_err();
match err {
Error::TomlParse { message, .. } => {
assert!(
message.contains("`[package.metadata]` must be a table"),
"diagnostic must name `[package.metadata]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn absolutize_path_bearing_keys_rejects_non_table_dependencies() {
let mut top = toml::map::Map::new();
top.insert("dependencies".to_string(), toml::Value::Array(vec![]));
let err = absolutize_path_bearing_keys(
&mut top,
Path::new("/upstream"),
Path::new("/upstream/Cargo.toml"),
)
.unwrap_err();
match err {
Error::TomlParse { message, .. } => {
assert!(
message.contains("`[dependencies` must be a table"),
"diagnostic must name `[dependencies`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn absolutize_patch_paths_rejects_non_table_patch() {
let mut top = toml::map::Map::new();
top.insert("patch".to_string(), toml::Value::String("oops".into()));
let err = absolutize_patch_paths(
&mut top,
Path::new("/upstream"),
Path::new("/upstream/Cargo.toml"),
)
.unwrap_err();
match err {
Error::TomlParse { message, .. } => {
assert!(
message.contains("`[patch]` must be a table"),
"diagnostic must name `[patch]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn absolutize_replace_paths_rejects_non_table_replace() {
let mut top = toml::map::Map::new();
top.insert("replace".to_string(), toml::Value::Integer(7));
let err = absolutize_replace_paths(
&mut top,
Path::new("/upstream"),
Path::new("/upstream/Cargo.toml"),
)
.unwrap_err();
match err {
Error::TomlParse { message, .. } => {
assert!(
message.contains("`[replace]` must be a table"),
"diagnostic must name `[replace]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn member_local_patch_non_table_is_rejected() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("m".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
top.insert("patch".to_string(), toml::Value::String("oops".into()));
let ws_value: toml::Value =
toml::from_str("[workspace]\nmembers = [\"m\"]\n").expect("ws-root parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let err = apply_workspace_member_inheritance(&mut top, &ctx, Path::new("/ws/m/Cargo.toml"))
.expect_err("member-local non-table `[patch]` must reject as TomlParse");
match err {
Error::TomlParse { path, message } => {
assert_eq!(path, PathBuf::from("/ws/m/Cargo.toml"));
assert!(
message.contains("`[patch]` must be a table"),
"diagnostic must name `[patch]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn member_local_patch_crates_io_non_table_is_rejected() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("m".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut patch = toml::map::Map::new();
patch.insert("crates-io".to_string(), toml::Value::String("oops".into()));
top.insert("patch".to_string(), toml::Value::Table(patch));
let ws_value: toml::Value =
toml::from_str("[workspace]\nmembers = [\"m\"]\n").expect("ws-root parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let err = apply_workspace_member_inheritance(&mut top, &ctx, Path::new("/ws/m/Cargo.toml"))
.expect_err("member-local non-table `[patch.crates-io]` must reject as TomlParse");
match err {
Error::TomlParse { path, message } => {
assert_eq!(path, PathBuf::from("/ws/m/Cargo.toml"));
assert!(
message.contains("`[patch.crates-io]` must be a table"),
"diagnostic must name `[patch.crates-io]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn member_local_patch_my_vendor_non_table_is_rejected() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("m".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut patch = toml::map::Map::new();
patch.insert("my-vendor".to_string(), toml::Value::String("oops".into()));
top.insert("patch".to_string(), toml::Value::Table(patch));
let ws_value: toml::Value =
toml::from_str("[workspace]\nmembers = [\"m\"]\n").expect("ws-root parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let err = apply_workspace_member_inheritance(&mut top, &ctx, Path::new("/ws/m/Cargo.toml"))
.expect_err("member-local non-table `[patch.my-vendor]` must reject as TomlParse");
match err {
Error::TomlParse { path, message } => {
assert_eq!(path, PathBuf::from("/ws/m/Cargo.toml"));
assert!(
message.contains("`[patch.my-vendor]` must be a table"),
"diagnostic must name `[patch.my-vendor]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
#[test]
fn member_local_patch_my_vendor_is_rejected() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("m".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut patch_table = toml::map::Map::new();
let mut my_vendor = toml::map::Map::new();
let mut entry = toml::map::Map::new();
entry.insert("path".to_string(), toml::Value::String("./vendored".into()));
my_vendor.insert("my-crate".to_string(), toml::Value::Table(entry));
patch_table.insert("my-vendor".to_string(), toml::Value::Table(my_vendor));
top.insert("patch".to_string(), toml::Value::Table(patch_table));
let ws_value: toml::Value =
toml::from_str("[workspace]\nmembers = [\"m\"]\n").expect("ws-root parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let err = apply_workspace_member_inheritance(&mut top, &ctx, Path::new("/ws/m/Cargo.toml"))
.expect_err("member-local `[patch.my-vendor]` must be rejected");
match err {
Error::Cli { message, .. } => {
assert!(
message.contains("cargo does not permit `[patch]` in workspace members"),
"diagnostic must name the rejection rationale: {message}"
);
assert!(
message.contains("my-vendor"),
"diagnostic must name the offending registry key: {message}"
);
}
other => panic!("expected Cli error, got {other:?}"),
}
}
#[test]
fn member_local_multi_registry_patch_rejection_includes_all() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("m".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut patch_table = toml::map::Map::new();
let mut vendor_a = toml::map::Map::new();
let mut va_entry = toml::map::Map::new();
va_entry.insert("path".to_string(), toml::Value::String("./vendor-a".into()));
vendor_a.insert("some-crate".to_string(), toml::Value::Table(va_entry));
patch_table.insert("my-vendor-a".to_string(), toml::Value::Table(vendor_a));
let mut vendor_b = toml::map::Map::new();
let mut vb_entry = toml::map::Map::new();
vb_entry.insert("path".to_string(), toml::Value::String("./vendor-b".into()));
vendor_b.insert("other-crate".to_string(), toml::Value::Table(vb_entry));
patch_table.insert("my-vendor-b".to_string(), toml::Value::Table(vendor_b));
top.insert("patch".to_string(), toml::Value::Table(patch_table));
let ws_value: toml::Value =
toml::from_str("[workspace]\nmembers = [\"m\"]\n").expect("ws-root parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let err = apply_workspace_member_inheritance(&mut top, &ctx, Path::new("/ws/m/Cargo.toml"))
.expect_err("member-local multi-registry `[patch]` must be rejected");
match err {
Error::Cli { message, .. } => {
assert!(
message.contains("cargo does not permit `[patch]` in workspace members"),
"diagnostic must name the rejection rationale: {message}"
);
assert!(
message.contains("my-vendor-a"),
"diagnostic must name the first offending registry key: {message}"
);
}
other => panic!("expected Cli error, got {other:?}"),
}
}
#[test]
fn apply_self_patch_policy_carries_workspace_root_non_crates_io_registry() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let ws_value: toml::Value = toml::from_str(
"[workspace]\nmembers = [\"m\"]\n\
[patch.my-vendor]\n\
my-dep = { path = \"vendored/my-dep\" }\n",
)
.expect("ws-root parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
apply_self_patch_policy(
&mut top,
Some("m"),
Path::new("/ws/m"),
Path::new("/ws/m/target/lihaaf-overlay"),
Some(&ctx),
)
.expect("non-crates-io registry must carry down successfully");
let patch = top
.get("patch")
.and_then(|v| v.as_table())
.expect("[patch] must exist after carry-down");
let my_vendor = patch
.get("my-vendor")
.and_then(|v| v.as_table())
.expect("[patch.my-vendor] must be carried into overlay");
let entry = my_vendor
.get("my-dep")
.and_then(|v| v.as_table())
.expect("[patch.my-vendor.my-dep] must be carried");
let path = entry
.get("path")
.and_then(|v| v.as_str())
.expect("path field must be present");
assert!(
path.contains("/ws/vendored/my-dep"),
"path must be absolutized against workspace-root dir; got `{path}`"
);
let crates_io = patch
.get("crates-io")
.and_then(|v| v.as_table())
.expect("[patch.crates-io] must exist after Rule 1 INJECT");
let self_entry = crates_io
.get("m")
.and_then(|v| v.as_table())
.expect("Rule 1 INJECT must produce [patch.crates-io.m]");
let self_path = self_entry
.get("path")
.and_then(|v| v.as_str())
.expect("[patch.crates-io.m].path must be present");
assert!(
self_path.ends_with("target/lihaaf-overlay"),
"Rule 1 INJECT path must tail-match staged-overlay; got `{self_path}`"
);
}
#[test]
fn apply_self_patch_policy_carries_multiple_registries_simultaneously() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let ws_value: toml::Value = toml::from_str(
"[workspace]\nmembers = [\"m\"]\n\
[patch.crates-io]\n\
other-crate = { path = \"vendored/other\" }\n\
[patch.my-vendor]\n\
my-dep = { path = \"vendored/my-dep\" }\n",
)
.expect("ws-root parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
apply_self_patch_policy(
&mut top,
Some("m"),
Path::new("/ws/m"),
Path::new("/ws/m/target/lihaaf-overlay"),
Some(&ctx),
)
.expect("multi-registry carry-down must succeed");
let patch = top
.get("patch")
.and_then(|v| v.as_table())
.expect("[patch] must exist");
let crates_io = patch
.get("crates-io")
.and_then(|v| v.as_table())
.expect("[patch.crates-io] must exist");
assert!(
crates_io.contains_key("other-crate"),
"[patch.crates-io.other-crate] must be carried"
);
assert!(
crates_io.contains_key("m"),
"[patch.crates-io.m] must be INJECTed (Rule 1)"
);
let my_vendor = patch
.get("my-vendor")
.and_then(|v| v.as_table())
.expect("[patch.my-vendor] must exist");
assert!(
my_vendor.contains_key("my-dep"),
"[patch.my-vendor.my-dep] must be carried"
);
}
#[test]
fn apply_self_patch_policy_rejects_non_table_non_crates_io_registry() {
let mut top: toml::map::Map<String, toml::Value> = toml::map::Map::new();
let ws_value: toml::Value =
toml::from_str("[workspace]\nmembers = [\"m\"]\n\n[patch]\nmy-vendor = \"oops\"\n")
.expect("ws-root parses");
let ctx = WorkspaceMemberContext {
workspace_root_manifest: PathBuf::from("/ws/Cargo.toml"),
workspace_root_value: ws_value,
};
let err = apply_self_patch_policy(
&mut top,
Some("m"),
Path::new("/ws/m"),
Path::new("/ws/m/target/lihaaf-overlay"),
Some(&ctx),
)
.expect_err("non-table `[patch.my-vendor]` must reject as TomlParse");
match err {
Error::TomlParse { path, message } => {
assert_eq!(path, PathBuf::from("/ws/Cargo.toml"));
assert!(
message.contains("workspace-root `[patch.my-vendor]` must be a table"),
"diagnostic must name `[patch.my-vendor]`; got: {message}"
);
}
other => panic!("expected TomlParse, got {other:?}"),
}
}
}