From 4cf99089d3f466b847e408fcf54edba0612fefcf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 05:58:37 +0000 Subject: [PATCH 1/3] Start refactor for #693 Assisted-by: Claude Code:claude-opus-5-5 From bfb282e4cfcb513c6c836776cf89892c10d42e47 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 06:02:55 +0000 Subject: [PATCH 2/3] Read Cargo.toml identity through one reader The cargo crawler, VEX product detection and cargo_tag each read a Cargo.toml [package] table their own way, and they disagreed: the crawler's line scanner missed a manifest with a BOM, the legacy [project] table or dotted keys, so such a vendored crate could not be found at all, while it accepted text cargo rejects as invalid TOML. formats::cargo::manifest now parses with toml_edit for all three, and the crawler's hand-rolled scanner is deleted. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/crawlers/cargo_crawler.rs | 213 +----------------- .../src/crawlers/cargo_crawler/oracle.rs | 4 +- .../src/formats/cargo/manifest.rs | 114 ++++++++++ .../src/formats/cargo/mod.rs | 3 + .../socket-patch-core/src/vendor/cargo_tag.rs | 6 +- crates/socket-patch-core/src/vex/product.rs | 51 ++++- .../tests/crawler_cargo_e2e.rs | 50 +++- 7 files changed, 214 insertions(+), 227 deletions(-) create mode 100644 crates/socket-patch-core/src/formats/cargo/manifest.rs diff --git a/crates/socket-patch-core/src/crawlers/cargo_crawler.rs b/crates/socket-patch-core/src/crawlers/cargo_crawler.rs index af787b772..34fc0ee43 100644 --- a/crates/socket-patch-core/src/crawlers/cargo_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/cargo_crawler.rs @@ -3,116 +3,13 @@ use std::path::{Path, PathBuf}; use super::listing::list_dir_sync; use super::types::{CrawledPackage, CrawlerOptions}; +use crate::formats::cargo::manifest::package_name_version; use crate::patch::path_safety; use crate::utils::fs::{is_dir, run_blocking}; #[cfg(test)] mod oracle; -// --------------------------------------------------------------------------- -// Cargo.toml minimal parser -// --------------------------------------------------------------------------- - -/// Parse `name` and `version` from a `Cargo.toml` `[package]` section. -/// -/// Uses a simple line-based parser — no TOML crate dependency. -/// Handles `name = "..."` and `version = "..."` within the `[package]` table. -/// Returns `None` if `version.workspace = true` or fields are missing. -pub fn parse_cargo_toml_name_version(content: &str) -> Option<(String, String)> { - let mut in_package = false; - let mut name: Option = None; - let mut version: Option = None; - - for line in content.lines() { - let trimmed = line.trim(); - - // Skip comments and empty lines - if trimmed.starts_with('#') || trimmed.is_empty() { - continue; - } - - // Track table headers. Use `parse_table_header` rather than an - // exact `== "[package]"` comparison so a header carrying a - // trailing inline comment (`[package] # ...`) or whitespace - // inside the brackets (`[ package ]`) is still recognized — - // both are valid TOML and a too-strict match would silently - // drop the package's name/version. - if trimmed.starts_with('[') { - if let Some(table) = parse_table_header(trimmed) { - if table == "package" { - in_package = true; - } else if in_package { - // We left the [package] section (a sibling table or - // a `[package.*]` subtable — bare keys can no longer - // follow per TOML, so stop scanning). - break; - } - } - continue; - } - - if !in_package { - continue; - } - - if let Some(val) = extract_string_value(trimmed, "name") { - name = Some(val); - } else if let Some(val) = extract_string_value(trimmed, "version") { - version = Some(val); - } else if trimmed.starts_with("version") && trimmed.contains("workspace") { - // version.workspace = true — cannot determine version from this file - return None; - } - - if name.is_some() && version.is_some() { - break; - } - } - - match (name, version) { - (Some(n), Some(v)) if !n.is_empty() && !v.is_empty() => Some((n, v)), - _ => None, - } -} - -/// Extract the table name from a TOML header line. -/// -/// `[package]` -> `Some("package")`, `[package] # comment` -> -/// `Some("package")`, `[ package ]` -> `Some("package")`. Returns -/// `None` for a line that is not a `[...]` header. Anything after the -/// closing `]` (typically an inline comment) is ignored. -fn parse_table_header(line: &str) -> Option<&str> { - let rest = line.strip_prefix('[')?; - let end = rest.find(']')?; - Some(rest[..end].trim()) -} - -/// Extract a quoted string value from a `key = "value"` line. -/// -/// Handles both TOML string flavors that Cargo accepts for `name` / -/// `version`: basic strings (`"..."`) and literal strings (`'...'`). -/// A too-strict double-quote-only match would silently drop a crate -/// whose manifest uses single quotes — and in the vendor layout, where -/// the directory name carries no version, that crate would become -/// undiscoverable (and thus unpatchable). -fn extract_string_value(line: &str, key: &str) -> Option { - let rest = line.strip_prefix(key)?; - let rest = rest.trim_start(); - let rest = rest.strip_prefix('=')?; - let rest = rest.trim_start(); - // The value must open with a quote of one kind; the matching close - // is the next quote of the *same* kind (literal strings have no - // escapes, and basic strings used for name/version never contain an - // escaped quote in practice). - let quote = match rest.chars().next()? { - c @ ('"' | '\'') => c, - _ => return None, - }; - let rest = &rest[1..]; - let end = rest.find(quote)?; - Some(rest[..end].to_string()) -} - // --------------------------------------------------------------------------- // CargoCrawler // --------------------------------------------------------------------------- @@ -293,7 +190,7 @@ impl CargoCrawler { Err(_) => return false, }; - match parse_cargo_toml_name_version(&content) { + match package_name_version(&content) { Some((n, v)) => n == name && v == version, // Fallback: check directory name None => path @@ -403,7 +300,7 @@ fn read_crate_cargo_toml(crate_path: &Path, dir_name: &str) -> Option<(String, S let content = crate::utils::fs::read_regular_to_string_sync(&cargo_toml_path).ok()?; // Fallback: parse directory name as - - parse_cargo_toml_name_version(&content) + package_name_version(&content) .or_else(|| CargoCrawler::parse_dir_name_version(dir_name)) } @@ -431,96 +328,6 @@ impl Default for CargoCrawler { mod tests { use super::*; - #[test] - fn test_parse_cargo_toml_basic() { - let content = r#" -[package] -name = "serde" -version = "1.0.200" -edition = "2021" -"#; - let (name, version) = parse_cargo_toml_name_version(content).unwrap(); - assert_eq!(name, "serde"); - assert_eq!(version, "1.0.200"); - } - - #[test] - fn test_parse_cargo_toml_with_comments() { - let content = r#" -# This is a comment -[package] -name = "tokio" # inline comment ignored since we stop at first " -version = "1.38.0" -"#; - let (name, version) = parse_cargo_toml_name_version(content).unwrap(); - assert_eq!(name, "tokio"); - assert_eq!(version, "1.38.0"); - } - - #[test] - fn test_parse_cargo_toml_workspace_version() { - let content = r#" -[package] -name = "my-crate" -version.workspace = true -"#; - assert!(parse_cargo_toml_name_version(content).is_none()); - } - - /// The inline-table spelling `version = { workspace = true }` is the - /// equally-valid sibling of the tested `version.workspace = true`. - /// `extract_string_value` must bail on the `{` (not a quote), after - /// which the `version`+`workspace` short-circuit returns `None` for - /// the whole manifest — the version cannot be determined here. - #[test] - fn test_parse_cargo_toml_inline_table_workspace_version() { - let content = "[package]\nname = \"my-crate\"\nversion = { workspace = true }\n"; - assert!(parse_cargo_toml_name_version(content).is_none()); - } - - /// A malformed bracket line (`[oops` — no closing `]`) inside - /// `[package]` is not a table header: it must be skipped without - /// terminating the section, so keys after it are still read. - #[test] - fn test_parse_cargo_toml_malformed_header_line_skipped() { - let content = "[package]\nname = \"a\"\n[oops\nversion = \"1.0\"\n"; - assert_eq!( - parse_cargo_toml_name_version(content), - Some(("a".to_string(), "1.0".to_string())) - ); - } - - #[test] - fn test_parse_cargo_toml_missing_fields() { - let content = r#" -[package] -name = "incomplete" -"#; - assert!(parse_cargo_toml_name_version(content).is_none()); - } - - #[test] - fn test_parse_cargo_toml_no_package_section() { - let content = r#" -[dependencies] -serde = "1.0" -"#; - assert!(parse_cargo_toml_name_version(content).is_none()); - } - - #[test] - fn test_parse_cargo_toml_stops_at_next_section() { - let content = r#" -[package] -name = "foo" - -[dependencies] -version = "fake" -"#; - // Should not find version since it's under [dependencies] - assert!(parse_cargo_toml_name_version(content).is_none()); - } - #[test] fn test_parse_dir_name_version() { assert_eq!( @@ -826,7 +633,7 @@ version = "fake" name = "serde" version = "1.0.200" "#; - let (name, version) = parse_cargo_toml_name_version(content).unwrap(); + let (name, version) = package_name_version(content).unwrap(); assert_eq!(name, "serde"); assert_eq!(version, "1.0.200"); } @@ -834,7 +641,7 @@ version = "1.0.200" #[test] fn test_parse_cargo_toml_header_with_inner_spaces() { let content = "[ package ]\nname = \"tokio\"\nversion = \"1.38.0\"\n"; - let (name, version) = parse_cargo_toml_name_version(content).unwrap(); + let (name, version) = package_name_version(content).unwrap(); assert_eq!(name, "tokio"); assert_eq!(version, "1.38.0"); } @@ -850,7 +657,7 @@ name = "foo" version = "fake" "#; // `version` lives under the metadata subtable, not [package]. - assert!(parse_cargo_toml_name_version(content).is_none()); + assert!(package_name_version(content).is_none()); } // --- regression: single-quoted (literal) string values ------------- @@ -861,7 +668,7 @@ version = "fake" #[test] fn test_parse_cargo_toml_single_quoted_values() { let content = "[package]\nname = 'serde'\nversion = '1.0.200'\n"; - let (name, version) = parse_cargo_toml_name_version(content).unwrap(); + let (name, version) = package_name_version(content).unwrap(); assert_eq!(name, "serde"); assert_eq!(version, "1.0.200"); } @@ -870,7 +677,7 @@ version = "fake" #[test] fn test_parse_cargo_toml_mixed_quote_values() { let content = "[package]\nname = 'tokio'\nversion = \"1.38.0\"\n"; - let (name, version) = parse_cargo_toml_name_version(content).unwrap(); + let (name, version) = package_name_version(content).unwrap(); assert_eq!(name, "tokio"); assert_eq!(version, "1.38.0"); } @@ -881,7 +688,7 @@ version = "fake" #[test] fn test_parse_cargo_toml_single_quoted_with_comment() { let content = "[package]\nname = 'serde' # the lib\nversion = '1.0.200'\n"; - let (name, version) = parse_cargo_toml_name_version(content).unwrap(); + let (name, version) = package_name_version(content).unwrap(); assert_eq!(name, "serde"); assert_eq!(version, "1.0.200"); } @@ -891,7 +698,7 @@ version = "fake" #[test] fn test_parse_cargo_toml_workspace_still_none_after_quote_fix() { let content = "[package]\nname = 'my-crate'\nversion.workspace = true\n"; - assert!(parse_cargo_toml_name_version(content).is_none()); + assert!(package_name_version(content).is_none()); } /// End-to-end: a vendored crate whose `Cargo.toml` uses single-quoted diff --git a/crates/socket-patch-core/src/crawlers/cargo_crawler/oracle.rs b/crates/socket-patch-core/src/crawlers/cargo_crawler/oracle.rs index 6513c4d86..c3a6fc65a 100644 --- a/crates/socket-patch-core/src/crawlers/cargo_crawler/oracle.rs +++ b/crates/socket-patch-core/src/crawlers/cargo_crawler/oracle.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use std::path::Path; -use super::{parse_cargo_toml_name_version, CargoCrawler}; +use super::{package_name_version, CargoCrawler}; use crate::crawlers::types::{CrawledPackage, CrawlerOptions}; pub(super) struct LegacyCargoCrawler; @@ -60,7 +60,7 @@ impl LegacyCargoCrawler { let cargo_toml_path = crate_path.join("Cargo.toml"); let content = tokio::fs::read_to_string(&cargo_toml_path).await.ok()?; - let (name, version) = parse_cargo_toml_name_version(&content) + let (name, version) = package_name_version(&content) .or_else(|| CargoCrawler::parse_dir_name_version(dir_name))?; let purl = crate::utils::purl::build_cargo_purl(&name, &version); diff --git a/crates/socket-patch-core/src/formats/cargo/manifest.rs b/crates/socket-patch-core/src/formats/cargo/manifest.rs new file mode 100644 index 000000000..fa20cb173 --- /dev/null +++ b/crates/socket-patch-core/src/formats/cargo/manifest.rs @@ -0,0 +1,114 @@ +//! `Cargo.toml`'s `[package]` identity: the ONE reader of a manifest's +//! `name` and `version`. +//! +//! Every caller parses with `toml_edit`, so they agree with cargo on what a +//! manifest says: a UTF-8 BOM, the legacy `[project]` table, dotted keys +//! (`package.name = "…"`) and an inline `package = { … }` all read the +//! same, and a manifest cargo rejects (invalid TOML) reads as nothing. +//! +//! Callers: the cargo crawler (crate identity), VEX product detection (the +//! project purl) and `vendor::cargo_tag` (the version literal it rewrites). + +use toml_edit::{Document, Item, Table, TableLike}; + +/// The `[package]` table of a parsed manifest, else its legacy `[project]` +/// spelling (crates published before manifest normalization ship it +/// verbatim, and cargo still accepts it). +pub(crate) fn package_table(root: &Table) -> Option<&dyn TableLike> { + ["package", "project"] + .iter() + .find_map(|key| root.get(key).and_then(Item::as_table_like)) +} + +/// The literal `name` and `version` of a `Cargo.toml`'s package table. +/// +/// `None` when the text is not valid TOML, has no package table, or either +/// field is missing, empty or not a literal string — notably +/// `version.workspace = true`, whose version this file alone cannot tell. +pub fn package_name_version(text: &str) -> Option<(String, String)> { + let doc = Document::parse(text).ok()?; + let package = package_table(doc.as_table())?; + let field = |key| { + package + .get(key) + .and_then(Item::as_str) + .filter(|s| !s.is_empty()) + .map(str::to_string) + }; + Some((field("name")?, field("version")?)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn nv(name: &str, version: &str) -> Option<(String, String)> { + Some((name.to_string(), version.to_string())) + } + + #[test] + fn reads_every_spelling_cargo_accepts() { + let cases = [ + ( + "[package]\nname = \"a\"\nversion = \"1.0.0\"\n", + nv("a", "1.0.0"), + ), + ( + "\u{feff}[package]\nname = \"a\"\nversion = \"1.0.0\"\n", + nv("a", "1.0.0"), + ), + ( + "[project]\nname = \"a\"\nversion = \"1.0.0\"\n", + nv("a", "1.0.0"), + ), + ( + "package.name = \"a\"\npackage.version = \"1.0.0\"\n", + nv("a", "1.0.0"), + ), + ( + "package = { name = \"a\", version = \"1.0.0\" }\n", + nv("a", "1.0.0"), + ), + ( + "[package]\nname = 'a'\nversion = '1.0.0'\n", + nv("a", "1.0.0"), + ), + ( + "[ package ] # c\nname = \"a\" # c\nversion = \"1.0.0\"\n", + nv("a", "1.0.0"), + ), + ( + "[package]\r\nname = \"a\"\r\nversion = \"1.0.0\"\r\n", + nv("a", "1.0.0"), + ), + ]; + for (text, want) in cases { + assert_eq!(package_name_version(text), want, "{text:?}"); + } + } + + #[test] + fn package_wins_over_project() { + let text = "[package]\nname = \"a\"\nversion = \"1.0.0\"\n\ + [project]\nname = \"b\"\nversion = \"2.0.0\"\n"; + assert_eq!(package_name_version(text), nv("a", "1.0.0")); + } + + #[test] + fn reads_nothing_it_cannot_tell() { + for text in [ + "[package]\nname = \"a\"\nversion.workspace = true\n", + "[package]\nname = \"a\"\nversion = { workspace = true }\n", + "[package]\nname = \"a\"\n", + "[package]\nversion = \"1.0.0\"\n", + "[package]\nname = \"\"\nversion = \"1.0.0\"\n", + "[dependencies]\nname = \"a\"\nversion = \"1.0.0\"\n", + "[package]\nname = \"a\"\n[dependencies]\nversion = \"1.0.0\"\n", + // Not TOML: cargo rejects it, so it names no package. + "[package] junk\nname = \"a\"\nversion = \"1.0.0\"\n", + "[package]\nname = \"a\"\n[oops\nversion = \"1.0.0\"\n", + ] { + assert_eq!(package_name_version(text), None, "{text:?}"); + } + } +} diff --git a/crates/socket-patch-core/src/formats/cargo/mod.rs b/crates/socket-patch-core/src/formats/cargo/mod.rs index 3e9cb9c5b..30142b7e4 100644 --- a/crates/socket-patch-core/src/formats/cargo/mod.rs +++ b/crates/socket-patch-core/src/formats/cargo/mod.rs @@ -21,9 +21,12 @@ //! * the vendored planner (`vendor::cargo_lock`) edits the same document //! with `toml_edit`; //! +//! [`manifest`] is the one reader of a `Cargo.toml`'s `[package]` identity. +//! //! Everything here is pure; the callers own the reads. pub(crate) mod hosted; +pub mod manifest; use std::ops::Range; diff --git a/crates/socket-patch-core/src/vendor/cargo_tag.rs b/crates/socket-patch-core/src/vendor/cargo_tag.rs index f060c019b..ba2746ebb 100644 --- a/crates/socket-patch-core/src/vendor/cargo_tag.rs +++ b/crates/socket-patch-core/src/vendor/cargo_tag.rs @@ -131,11 +131,7 @@ impl std::fmt::Display for TagError { /// literal (quotes included) and the quote character. fn version_literal(text: &str) -> Result<(String, std::ops::Range, char), TagError> { let doc = toml_edit::Document::parse(text).map_err(|e| TagError::Unparseable(e.to_string()))?; - // `[project]` is the legacy spelling of `[package]` cargo still accepts - // (crates published before manifest normalization ship it verbatim). - let item = ["package", "project"] - .iter() - .find_map(|table| doc.get(table).and_then(toml_edit::Item::as_table_like)) + let item = crate::formats::cargo::manifest::package_table(doc.as_table()) .and_then(|p| p.get("version")) .ok_or(TagError::NoVersion)?; let value = item.as_str().ok_or(TagError::NoVersion)?.to_string(); diff --git a/crates/socket-patch-core/src/vex/product.rs b/crates/socket-patch-core/src/vex/product.rs index 9cf434738..69fbcdf4d 100644 --- a/crates/socket-patch-core/src/vex/product.rs +++ b/crates/socket-patch-core/src/vex/product.rs @@ -166,7 +166,7 @@ fn parse_pyproject(content: &str) -> Option { } fn parse_cargo_toml(content: &str) -> Option { - let (name, version) = scan_toml_section(strip_bom(content), "package")?; + let (name, version) = crate::formats::cargo::manifest::package_name_version(content)?; Some(format!("pkg:cargo/{name}@{version}")) } @@ -358,13 +358,13 @@ async fn single_root_file_with_extension(cwd: &Path, ext: &str) -> Option]` blocks. Reads /// `name = "..."` and `version = "..."` from the named section and /// stops at the next `[` header. Robust enough for the well-formed -/// `pyproject.toml` / `Cargo.toml` files we expect at the top level — -/// no full TOML parser dependency. +/// `pyproject.toml` files we expect at the top level — no full TOML +/// parser dependency. (`Cargo.toml` reads through +/// [`crate::formats::cargo::manifest`].) /// /// Returns `None` if either key is missing, both keys appear outside -/// the section, the value is empty, or the value is `version.workspace -/// = true` (matches the cargo crawler's behavior of skipping workspace -/// inheritance). +/// the section, the value is empty, or the value is not a quoted +/// string (`version.workspace = true`). fn scan_toml_section(content: &str, section: &str) -> Option<(String, String)> { let mut in_section = false; let mut name: Option = None; @@ -2060,6 +2060,45 @@ mod tests { // Cargo.toml BOM fix: a file the user's own toolchain accepts must // not silently yield no PURL. + /// #693: VEX product detection, the cargo crawler and `cargo_tag` read + /// `Cargo.toml` through one reader, so they agree on every spelling — + /// including the rows where the old line scanners drifted (`[project]`, + /// dotted keys, inline table, invalid TOML). + #[test] + fn cargo_manifest_readers_agree() { + use crate::formats::cargo::manifest::package_name_version; + use crate::vendor::cargo_tag::tag_manifest_text; + const UUID: &str = "80630680-4da6-45f9-bba8-b888e0ffd58c"; + let rows: [(&str, Option<&str>); 8] = [ + ("[package]\nname = \"old\"\nversion = \"0.1.0\"\n", Some("0.1.0")), + ("\u{feff}[package]\nname = \"old\"\nversion = \"0.1.0\"\n", Some("0.1.0")), + ("[project]\nname = \"old\"\nversion = \"0.1.0\"\n", Some("0.1.0")), + ("package.name = \"old\"\npackage.version = \"0.1.0\"\n", Some("0.1.0")), + ("package = { name = \"old\", version = \"0.1.0\" }\n", Some("0.1.0")), + ("[package]\nname = \"old\"\nversion.workspace = true\n", None), + ("[package] junk\nname = \"old\"\nversion = \"0.1.0\"\n", None), + ("[dependencies]\nold = \"0.1.0\"\n", None), + ]; + for (text, version) in rows { + let crawler = package_name_version(text); + assert_eq!( + crawler, + version.map(|v| ("old".to_string(), v.to_string())), + "crawler: {text:?}" + ); + assert_eq!( + parse_cargo_toml(text), + version.map(|v| format!("pkg:cargo/old@{v}")), + "VEX product: {text:?}" + ); + assert_eq!( + tag_manifest_text(text, "0.1.0", UUID).is_ok(), + version.is_some(), + "cargo_tag: {text:?}" + ); + } + } + #[test] fn scan_origin_url_tolerates_leading_bom() { let cfg = "\u{feff}[remote \"origin\"]\n\turl = git@github.com:foo/bar.git\n"; diff --git a/crates/socket-patch-core/tests/crawler_cargo_e2e.rs b/crates/socket-patch-core/tests/crawler_cargo_e2e.rs index 50fafc9f0..b1a071957 100644 --- a/crates/socket-patch-core/tests/crawler_cargo_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_cargo_e2e.rs @@ -2,7 +2,7 @@ use std::path::Path; -use socket_patch_core::crawlers::cargo_crawler::parse_cargo_toml_name_version; +use socket_patch_core::formats::cargo::manifest::package_name_version; use socket_patch_core::crawlers::types::CrawlerOptions; use socket_patch_core::crawlers::CargoCrawler; @@ -41,14 +41,14 @@ async fn stage_vendor_crate(src: &Path, name: &str, version: &str) -> std::path: pkg } -// ── parse_cargo_toml_name_version ────────────────────────────── +// ── package_name_version ────────────────────────────── #[test] #[serial_test::parallel] fn parse_cargo_toml_well_formed() { let toml = "[package]\nname = \"serde\"\nversion = \"1.0.200\"\nedition = \"2021\"\n"; assert_eq!( - parse_cargo_toml_name_version(toml), + package_name_version(toml), Some(("serde".to_string(), "1.0.200".to_string())) ); } @@ -57,21 +57,21 @@ fn parse_cargo_toml_well_formed() { #[serial_test::parallel] fn parse_cargo_toml_missing_name_returns_none() { let toml = "[package]\nversion = \"1.0.200\"\n"; - assert_eq!(parse_cargo_toml_name_version(toml), None); + assert_eq!(package_name_version(toml), None); } #[test] #[serial_test::parallel] fn parse_cargo_toml_missing_version_returns_none() { let toml = "[package]\nname = \"serde\"\n"; - assert_eq!(parse_cargo_toml_name_version(toml), None); + assert_eq!(package_name_version(toml), None); } #[test] #[serial_test::parallel] fn parse_cargo_toml_malformed_returns_none() { let toml = "this is not toml at all"; - assert_eq!(parse_cargo_toml_name_version(toml), None); + assert_eq!(package_name_version(toml), None); } /// Parser must stop scanning when it leaves the `[package]` table. @@ -82,7 +82,7 @@ fn parse_cargo_toml_malformed_returns_none() { fn parse_cargo_toml_stops_at_next_section() { let toml = "[package]\nname = \"foo\"\nversion = \"1.0.0\"\n\n[dependencies]\nname = \"bar\"\n"; assert_eq!( - parse_cargo_toml_name_version(toml), + package_name_version(toml), Some(("foo".to_string(), "1.0.0".to_string())) ); } @@ -95,7 +95,7 @@ fn parse_cargo_toml_ignores_lines_before_package_section() { let toml = "[profile.release]\nname = \"wrong\"\n\n[package]\nname = \"foo\"\nversion = \"1.0.0\"\n"; assert_eq!( - parse_cargo_toml_name_version(toml), + package_name_version(toml), Some(("foo".to_string(), "1.0.0".to_string())) ); } @@ -253,6 +253,34 @@ async fn find_by_purls_vendor_layout_finds_crate() { // read from the manifest, not invented from the directory name. } +/// #693: a vendored crate whose manifest cargo accepts in any spelling +/// (a BOM, the legacy `[project]` table, dotted keys) is found. The vendor +/// dir name carries no version, so the old line scanner left these crates +/// undiscoverable. +#[tokio::test] +#[serial_test::parallel] +async fn find_by_purls_vendor_layout_reads_every_manifest_spelling() { + let manifests = [ + ("bom", "\u{feff}[package]\nname = \"bom\"\nversion = \"1.0.0\"\n"), + ("legacy", "[project]\nname = \"legacy\"\nversion = \"1.0.0\"\n"), + ("dotted", "package.name = \"dotted\"\npackage.version = \"1.0.0\"\n"), + ]; + let tmp = tempfile::tempdir().unwrap(); + for (name, text) in manifests { + let pkg = tmp.path().join(name); + tokio::fs::create_dir_all(&pkg).await.unwrap(); + tokio::fs::write(pkg.join("Cargo.toml"), text).await.unwrap(); + } + let purls: Vec = manifests + .iter() + .map(|(name, _)| format!("pkg:cargo/{name}@1.0.0")) + .collect(); + let result = CargoCrawler.find_by_purls(tmp.path(), &purls).await.unwrap(); + for purl in &purls { + assert!(result.contains_key(purl), "{purl} not found: {result:?}"); + } +} + #[tokio::test] #[serial_test::parallel] async fn find_by_purls_vendor_version_mismatch_returns_empty() { @@ -505,13 +533,13 @@ async fn find_by_purls_verify_fallback_via_dir_name() { /// `version.workspace = true` in a top-level `[package]` block must /// bail: the crawler can't infer the actual version from /// just this file. `find_by_purls` then has to fall back to dir-name -/// parsing — but `parse_cargo_toml_name_version` itself must return +/// parsing — but `package_name_version` itself must return /// None up front. #[test] #[serial_test::parallel] fn parse_cargo_toml_version_workspace_returns_none() { let toml = "[package]\nname = \"foo\"\nversion.workspace = true\n"; - assert_eq!(parse_cargo_toml_name_version(toml), None); + assert_eq!(package_name_version(toml), None); } /// `verify_crate_at_path` with a dir-name-only parse (workspace @@ -669,7 +697,7 @@ async fn crawl_all_skips_top_level_files() { } /// A crate directory with a broken `Cargo.toml` AND a non-conforming -/// directory name → `parse_cargo_toml_name_version` returns None +/// directory name → `package_name_version` returns None /// (broken toml) AND `parse_dir_name_version` returns None (no `-` /// followed by digit), so the chain short-circuits and /// the package is silently skipped. From 3f4b7417ccabb9577718e53ecaa97893cffc4a7e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 06:15:53 +0000 Subject: [PATCH 3/3] Drop the deleted scanner's header test; format Assisted-by: Claude Code:claude-opus-5-5 --- .../src/crawlers/cargo_crawler.rs | 21 +---------- crates/socket-patch-core/src/vex/product.rs | 35 +++++++++++++++---- .../tests/crawler_cargo_e2e.rs | 26 ++++++++++---- 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/crates/socket-patch-core/src/crawlers/cargo_crawler.rs b/crates/socket-patch-core/src/crawlers/cargo_crawler.rs index 34fc0ee43..5aa4154ff 100644 --- a/crates/socket-patch-core/src/crawlers/cargo_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/cargo_crawler.rs @@ -602,25 +602,6 @@ mod tests { // --- regression: table-header parsing tolerance -------------------- - #[test] - fn test_parse_table_header_variants() { - assert_eq!(parse_table_header("[package]"), Some("package")); - assert_eq!( - parse_table_header("[package] # main crate"), - Some("package") - ); - assert_eq!(parse_table_header("[ package ]"), Some("package")); - assert_eq!( - parse_table_header("[package.metadata]"), - Some("package.metadata") - ); - // Not a header line. - assert_eq!(parse_table_header("name = \"x\""), None); - // Array value lines don't start with '[' once trimmed by the caller, - // but a bare unterminated bracket is rejected. - assert_eq!(parse_table_header("[oops"), None); - } - /// A `[package]` header with a trailing inline comment is valid TOML. /// The parser must still recognize it and read name/version — a /// too-strict `== "[package]"` would drop the crate, and in the @@ -663,7 +644,7 @@ version = "fake" // --- regression: single-quoted (literal) string values ------------- /// TOML literal strings use single quotes and are valid in a - /// `Cargo.toml`. The minimal parser must read `name`/`version` from + /// `Cargo.toml`. The reader must read `name`/`version` from /// them just as it does from basic (double-quoted) strings. #[test] fn test_parse_cargo_toml_single_quoted_values() { diff --git a/crates/socket-patch-core/src/vex/product.rs b/crates/socket-patch-core/src/vex/product.rs index 69fbcdf4d..3e00f563f 100644 --- a/crates/socket-patch-core/src/vex/product.rs +++ b/crates/socket-patch-core/src/vex/product.rs @@ -2070,13 +2070,34 @@ mod tests { use crate::vendor::cargo_tag::tag_manifest_text; const UUID: &str = "80630680-4da6-45f9-bba8-b888e0ffd58c"; let rows: [(&str, Option<&str>); 8] = [ - ("[package]\nname = \"old\"\nversion = \"0.1.0\"\n", Some("0.1.0")), - ("\u{feff}[package]\nname = \"old\"\nversion = \"0.1.0\"\n", Some("0.1.0")), - ("[project]\nname = \"old\"\nversion = \"0.1.0\"\n", Some("0.1.0")), - ("package.name = \"old\"\npackage.version = \"0.1.0\"\n", Some("0.1.0")), - ("package = { name = \"old\", version = \"0.1.0\" }\n", Some("0.1.0")), - ("[package]\nname = \"old\"\nversion.workspace = true\n", None), - ("[package] junk\nname = \"old\"\nversion = \"0.1.0\"\n", None), + ( + "[package]\nname = \"old\"\nversion = \"0.1.0\"\n", + Some("0.1.0"), + ), + ( + "\u{feff}[package]\nname = \"old\"\nversion = \"0.1.0\"\n", + Some("0.1.0"), + ), + ( + "[project]\nname = \"old\"\nversion = \"0.1.0\"\n", + Some("0.1.0"), + ), + ( + "package.name = \"old\"\npackage.version = \"0.1.0\"\n", + Some("0.1.0"), + ), + ( + "package = { name = \"old\", version = \"0.1.0\" }\n", + Some("0.1.0"), + ), + ( + "[package]\nname = \"old\"\nversion.workspace = true\n", + None, + ), + ( + "[package] junk\nname = \"old\"\nversion = \"0.1.0\"\n", + None, + ), ("[dependencies]\nold = \"0.1.0\"\n", None), ]; for (text, version) in rows { diff --git a/crates/socket-patch-core/tests/crawler_cargo_e2e.rs b/crates/socket-patch-core/tests/crawler_cargo_e2e.rs index b1a071957..7976e153f 100644 --- a/crates/socket-patch-core/tests/crawler_cargo_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_cargo_e2e.rs @@ -2,9 +2,9 @@ use std::path::Path; -use socket_patch_core::formats::cargo::manifest::package_name_version; use socket_patch_core::crawlers::types::CrawlerOptions; use socket_patch_core::crawlers::CargoCrawler; +use socket_patch_core::formats::cargo::manifest::package_name_version; const ORG_PURL: &str = "pkg:cargo/serde@1.0.200"; @@ -261,21 +261,35 @@ async fn find_by_purls_vendor_layout_finds_crate() { #[serial_test::parallel] async fn find_by_purls_vendor_layout_reads_every_manifest_spelling() { let manifests = [ - ("bom", "\u{feff}[package]\nname = \"bom\"\nversion = \"1.0.0\"\n"), - ("legacy", "[project]\nname = \"legacy\"\nversion = \"1.0.0\"\n"), - ("dotted", "package.name = \"dotted\"\npackage.version = \"1.0.0\"\n"), + ( + "bom", + "\u{feff}[package]\nname = \"bom\"\nversion = \"1.0.0\"\n", + ), + ( + "legacy", + "[project]\nname = \"legacy\"\nversion = \"1.0.0\"\n", + ), + ( + "dotted", + "package.name = \"dotted\"\npackage.version = \"1.0.0\"\n", + ), ]; let tmp = tempfile::tempdir().unwrap(); for (name, text) in manifests { let pkg = tmp.path().join(name); tokio::fs::create_dir_all(&pkg).await.unwrap(); - tokio::fs::write(pkg.join("Cargo.toml"), text).await.unwrap(); + tokio::fs::write(pkg.join("Cargo.toml"), text) + .await + .unwrap(); } let purls: Vec = manifests .iter() .map(|(name, _)| format!("pkg:cargo/{name}@1.0.0")) .collect(); - let result = CargoCrawler.find_by_purls(tmp.path(), &purls).await.unwrap(); + let result = CargoCrawler + .find_by_purls(tmp.path(), &purls) + .await + .unwrap(); for purl in &purls { assert!(result.contains_key(purl), "{purl} not found: {result:?}"); }