use std::path::{Path, PathBuf};
use crate::error::Error;
use crate::util;
#[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 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_inner(upstream_manifest_path, |_name| synthetic_metadata.cloned())
}
pub fn materialize_overlay_with_synthetic_metadata_builder<F>(
upstream_manifest_path: &Path,
builder: F,
) -> Result<OverlayPlan, Error>
where
F: FnOnce(Option<&str>) -> SyntheticMetadata,
{
materialize_overlay_inner(upstream_manifest_path, |name| Some(builder(name)))
}
fn materialize_overlay_inner<F>(
upstream_manifest_path: &Path,
synthetic_metadata: F,
) -> 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` must point to a single-crate Cargo.toml; \
`{}` is a workspace root (declares `[workspace]` without `[package]`). \
Pass a member crate's Cargo.toml as `--compat-root` instead.",
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("."));
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);
if let Some(meta) = synthetic.as_ref() {
inject_synthetic_metadata(top, meta);
}
}
let serialized = serialize_canonical(&value)?;
let crate_dir = upstream_manifest_path
.parent()
.unwrap_or(upstream_manifest_path);
let sibling_path = crate_dir
.join("target")
.join("lihaaf-overlay")
.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)?;
}
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)
}
fn inject_synthetic_metadata(
top: &mut toml::map::Map<String, toml::Value>,
meta: &SyntheticMetadata,
) {
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;
};
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;
};
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(),
),
);
metadata.insert("lihaaf".to_string(), toml::Value::Table(lihaaf_table));
}
fn absolutize_path_bearing_keys(
top: &mut toml::map::Map<String, toml::Value>,
upstream_dir: &Path,
) {
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, upstream_dir: &Path| {
if let Some(toml::Value::Table(deps)) = top.get_mut(section) {
for (_name, dep) in deps.iter_mut() {
if let toml::Value::Table(t) = dep {
absolutize_string_at(t, "path", upstream_dir);
}
}
}
};
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);
absolutize_deps_paths(top, "dev-dependencies", upstream_dir);
absolutize_deps_paths(top, "build-dependencies", upstream_dir);
if let Some(toml::Value::Table(targets)) = top.get_mut("target") {
for (_cfg, cfg_value) in targets.iter_mut() {
if let toml::Value::Table(cfg_table) = cfg_value {
absolutize_deps_paths(cfg_table, "dependencies", upstream_dir);
absolutize_deps_paths(cfg_table, "dev-dependencies", upstream_dir);
absolutize_deps_paths(cfg_table, "build-dependencies", upstream_dir);
}
}
}
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 abs = crate::util::to_forward_slash(
&upstream_dir.join(p).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 abs =
crate::util::to_forward_slash(&upstream_dir.join(p).to_string_lossy());
*entry = toml::Value::String(abs);
}
}
}
}
absolutize_deps_paths(ws, "dependencies", upstream_dir);
}
if let Some(toml::Value::Table(pkg)) = top.get_mut("package") {
absolutize_string_at(pkg, "workspace", upstream_dir);
}
absolutize_patch_paths(top, upstream_dir);
absolutize_replace_paths(top, upstream_dir);
}
fn absolutize_patch_paths(top: &mut toml::map::Map<String, toml::Value>, upstream_dir: &Path) {
let Some(toml::Value::Table(patch)) = top.get_mut("patch") else {
return;
};
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));
}
}
}
}
}
}
fn absolutize_replace_paths(top: &mut toml::map::Map<String, toml::Value>, upstream_dir: &Path) {
let Some(toml::Value::Table(replace)) = top.get_mut("replace") else {
return;
};
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));
}
}
}
}
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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}`"
);
}
}