Skip to content

Commit bfb282e

Browse files
committed
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
1 parent 4cf9908 commit bfb282e

7 files changed

Lines changed: 214 additions & 227 deletions

File tree

‎crates/socket-patch-core/src/crawlers/cargo_crawler.rs‎

Lines changed: 10 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -3,116 +3,13 @@ use std::path::{Path, PathBuf};
33

44
use super::listing::list_dir_sync;
55
use super::types::{CrawledPackage, CrawlerOptions};
6+
use crate::formats::cargo::manifest::package_name_version;
67
use crate::patch::path_safety;
78
use crate::utils::fs::{is_dir, run_blocking};
89

910
#[cfg(test)]
1011
mod oracle;
1112

12-
// ---------------------------------------------------------------------------
13-
// Cargo.toml minimal parser
14-
// ---------------------------------------------------------------------------
15-
16-
/// Parse `name` and `version` from a `Cargo.toml` `[package]` section.
17-
///
18-
/// Uses a simple line-based parser — no TOML crate dependency.
19-
/// Handles `name = "..."` and `version = "..."` within the `[package]` table.
20-
/// Returns `None` if `version.workspace = true` or fields are missing.
21-
pub fn parse_cargo_toml_name_version(content: &str) -> Option<(String, String)> {
22-
let mut in_package = false;
23-
let mut name: Option<String> = None;
24-
let mut version: Option<String> = None;
25-
26-
for line in content.lines() {
27-
let trimmed = line.trim();
28-
29-
// Skip comments and empty lines
30-
if trimmed.starts_with('#') || trimmed.is_empty() {
31-
continue;
32-
}
33-
34-
// Track table headers. Use `parse_table_header` rather than an
35-
// exact `== "[package]"` comparison so a header carrying a
36-
// trailing inline comment (`[package] # ...`) or whitespace
37-
// inside the brackets (`[ package ]`) is still recognized —
38-
// both are valid TOML and a too-strict match would silently
39-
// drop the package's name/version.
40-
if trimmed.starts_with('[') {
41-
if let Some(table) = parse_table_header(trimmed) {
42-
if table == "package" {
43-
in_package = true;
44-
} else if in_package {
45-
// We left the [package] section (a sibling table or
46-
// a `[package.*]` subtable — bare keys can no longer
47-
// follow per TOML, so stop scanning).
48-
break;
49-
}
50-
}
51-
continue;
52-
}
53-
54-
if !in_package {
55-
continue;
56-
}
57-
58-
if let Some(val) = extract_string_value(trimmed, "name") {
59-
name = Some(val);
60-
} else if let Some(val) = extract_string_value(trimmed, "version") {
61-
version = Some(val);
62-
} else if trimmed.starts_with("version") && trimmed.contains("workspace") {
63-
// version.workspace = true — cannot determine version from this file
64-
return None;
65-
}
66-
67-
if name.is_some() && version.is_some() {
68-
break;
69-
}
70-
}
71-
72-
match (name, version) {
73-
(Some(n), Some(v)) if !n.is_empty() && !v.is_empty() => Some((n, v)),
74-
_ => None,
75-
}
76-
}
77-
78-
/// Extract the table name from a TOML header line.
79-
///
80-
/// `[package]` -> `Some("package")`, `[package] # comment` ->
81-
/// `Some("package")`, `[ package ]` -> `Some("package")`. Returns
82-
/// `None` for a line that is not a `[...]` header. Anything after the
83-
/// closing `]` (typically an inline comment) is ignored.
84-
fn parse_table_header(line: &str) -> Option<&str> {
85-
let rest = line.strip_prefix('[')?;
86-
let end = rest.find(']')?;
87-
Some(rest[..end].trim())
88-
}
89-
90-
/// Extract a quoted string value from a `key = "value"` line.
91-
///
92-
/// Handles both TOML string flavors that Cargo accepts for `name` /
93-
/// `version`: basic strings (`"..."`) and literal strings (`'...'`).
94-
/// A too-strict double-quote-only match would silently drop a crate
95-
/// whose manifest uses single quotes — and in the vendor layout, where
96-
/// the directory name carries no version, that crate would become
97-
/// undiscoverable (and thus unpatchable).
98-
fn extract_string_value(line: &str, key: &str) -> Option<String> {
99-
let rest = line.strip_prefix(key)?;
100-
let rest = rest.trim_start();
101-
let rest = rest.strip_prefix('=')?;
102-
let rest = rest.trim_start();
103-
// The value must open with a quote of one kind; the matching close
104-
// is the next quote of the *same* kind (literal strings have no
105-
// escapes, and basic strings used for name/version never contain an
106-
// escaped quote in practice).
107-
let quote = match rest.chars().next()? {
108-
c @ ('"' | '\'') => c,
109-
_ => return None,
110-
};
111-
let rest = &rest[1..];
112-
let end = rest.find(quote)?;
113-
Some(rest[..end].to_string())
114-
}
115-
11613
// ---------------------------------------------------------------------------
11714
// CargoCrawler
11815
// ---------------------------------------------------------------------------
@@ -293,7 +190,7 @@ impl CargoCrawler {
293190
Err(_) => return false,
294191
};
295192

296-
match parse_cargo_toml_name_version(&content) {
193+
match package_name_version(&content) {
297194
Some((n, v)) => n == name && v == version,
298195
// Fallback: check directory name
299196
None => path
@@ -403,7 +300,7 @@ fn read_crate_cargo_toml(crate_path: &Path, dir_name: &str) -> Option<(String, S
403300
let content = crate::utils::fs::read_regular_to_string_sync(&cargo_toml_path).ok()?;
404301

405302
// Fallback: parse directory name as <name>-<version>
406-
parse_cargo_toml_name_version(&content)
303+
package_name_version(&content)
407304
.or_else(|| CargoCrawler::parse_dir_name_version(dir_name))
408305
}
409306

@@ -431,96 +328,6 @@ impl Default for CargoCrawler {
431328
mod tests {
432329
use super::*;
433330

434-
#[test]
435-
fn test_parse_cargo_toml_basic() {
436-
let content = r#"
437-
[package]
438-
name = "serde"
439-
version = "1.0.200"
440-
edition = "2021"
441-
"#;
442-
let (name, version) = parse_cargo_toml_name_version(content).unwrap();
443-
assert_eq!(name, "serde");
444-
assert_eq!(version, "1.0.200");
445-
}
446-
447-
#[test]
448-
fn test_parse_cargo_toml_with_comments() {
449-
let content = r#"
450-
# This is a comment
451-
[package]
452-
name = "tokio" # inline comment ignored since we stop at first "
453-
version = "1.38.0"
454-
"#;
455-
let (name, version) = parse_cargo_toml_name_version(content).unwrap();
456-
assert_eq!(name, "tokio");
457-
assert_eq!(version, "1.38.0");
458-
}
459-
460-
#[test]
461-
fn test_parse_cargo_toml_workspace_version() {
462-
let content = r#"
463-
[package]
464-
name = "my-crate"
465-
version.workspace = true
466-
"#;
467-
assert!(parse_cargo_toml_name_version(content).is_none());
468-
}
469-
470-
/// The inline-table spelling `version = { workspace = true }` is the
471-
/// equally-valid sibling of the tested `version.workspace = true`.
472-
/// `extract_string_value` must bail on the `{` (not a quote), after
473-
/// which the `version`+`workspace` short-circuit returns `None` for
474-
/// the whole manifest — the version cannot be determined here.
475-
#[test]
476-
fn test_parse_cargo_toml_inline_table_workspace_version() {
477-
let content = "[package]\nname = \"my-crate\"\nversion = { workspace = true }\n";
478-
assert!(parse_cargo_toml_name_version(content).is_none());
479-
}
480-
481-
/// A malformed bracket line (`[oops` — no closing `]`) inside
482-
/// `[package]` is not a table header: it must be skipped without
483-
/// terminating the section, so keys after it are still read.
484-
#[test]
485-
fn test_parse_cargo_toml_malformed_header_line_skipped() {
486-
let content = "[package]\nname = \"a\"\n[oops\nversion = \"1.0\"\n";
487-
assert_eq!(
488-
parse_cargo_toml_name_version(content),
489-
Some(("a".to_string(), "1.0".to_string()))
490-
);
491-
}
492-
493-
#[test]
494-
fn test_parse_cargo_toml_missing_fields() {
495-
let content = r#"
496-
[package]
497-
name = "incomplete"
498-
"#;
499-
assert!(parse_cargo_toml_name_version(content).is_none());
500-
}
501-
502-
#[test]
503-
fn test_parse_cargo_toml_no_package_section() {
504-
let content = r#"
505-
[dependencies]
506-
serde = "1.0"
507-
"#;
508-
assert!(parse_cargo_toml_name_version(content).is_none());
509-
}
510-
511-
#[test]
512-
fn test_parse_cargo_toml_stops_at_next_section() {
513-
let content = r#"
514-
[package]
515-
name = "foo"
516-
517-
[dependencies]
518-
version = "fake"
519-
"#;
520-
// Should not find version since it's under [dependencies]
521-
assert!(parse_cargo_toml_name_version(content).is_none());
522-
}
523-
524331
#[test]
525332
fn test_parse_dir_name_version() {
526333
assert_eq!(
@@ -826,15 +633,15 @@ version = "fake"
826633
name = "serde"
827634
version = "1.0.200"
828635
"#;
829-
let (name, version) = parse_cargo_toml_name_version(content).unwrap();
636+
let (name, version) = package_name_version(content).unwrap();
830637
assert_eq!(name, "serde");
831638
assert_eq!(version, "1.0.200");
832639
}
833640

834641
#[test]
835642
fn test_parse_cargo_toml_header_with_inner_spaces() {
836643
let content = "[ package ]\nname = \"tokio\"\nversion = \"1.38.0\"\n";
837-
let (name, version) = parse_cargo_toml_name_version(content).unwrap();
644+
let (name, version) = package_name_version(content).unwrap();
838645
assert_eq!(name, "tokio");
839646
assert_eq!(version, "1.38.0");
840647
}
@@ -850,7 +657,7 @@ name = "foo"
850657
version = "fake"
851658
"#;
852659
// `version` lives under the metadata subtable, not [package].
853-
assert!(parse_cargo_toml_name_version(content).is_none());
660+
assert!(package_name_version(content).is_none());
854661
}
855662

856663
// --- regression: single-quoted (literal) string values -------------
@@ -861,7 +668,7 @@ version = "fake"
861668
#[test]
862669
fn test_parse_cargo_toml_single_quoted_values() {
863670
let content = "[package]\nname = 'serde'\nversion = '1.0.200'\n";
864-
let (name, version) = parse_cargo_toml_name_version(content).unwrap();
671+
let (name, version) = package_name_version(content).unwrap();
865672
assert_eq!(name, "serde");
866673
assert_eq!(version, "1.0.200");
867674
}
@@ -870,7 +677,7 @@ version = "fake"
870677
#[test]
871678
fn test_parse_cargo_toml_mixed_quote_values() {
872679
let content = "[package]\nname = 'tokio'\nversion = \"1.38.0\"\n";
873-
let (name, version) = parse_cargo_toml_name_version(content).unwrap();
680+
let (name, version) = package_name_version(content).unwrap();
874681
assert_eq!(name, "tokio");
875682
assert_eq!(version, "1.38.0");
876683
}
@@ -881,7 +688,7 @@ version = "fake"
881688
#[test]
882689
fn test_parse_cargo_toml_single_quoted_with_comment() {
883690
let content = "[package]\nname = 'serde' # the lib\nversion = '1.0.200'\n";
884-
let (name, version) = parse_cargo_toml_name_version(content).unwrap();
691+
let (name, version) = package_name_version(content).unwrap();
885692
assert_eq!(name, "serde");
886693
assert_eq!(version, "1.0.200");
887694
}
@@ -891,7 +698,7 @@ version = "fake"
891698
#[test]
892699
fn test_parse_cargo_toml_workspace_still_none_after_quote_fix() {
893700
let content = "[package]\nname = 'my-crate'\nversion.workspace = true\n";
894-
assert!(parse_cargo_toml_name_version(content).is_none());
701+
assert!(package_name_version(content).is_none());
895702
}
896703

897704
/// End-to-end: a vendored crate whose `Cargo.toml` uses single-quoted

‎crates/socket-patch-core/src/crawlers/cargo_crawler/oracle.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use std::collections::HashSet;
66
use std::path::Path;
77

8-
use super::{parse_cargo_toml_name_version, CargoCrawler};
8+
use super::{package_name_version, CargoCrawler};
99
use crate::crawlers::types::{CrawledPackage, CrawlerOptions};
1010

1111
pub(super) struct LegacyCargoCrawler;
@@ -60,7 +60,7 @@ impl LegacyCargoCrawler {
6060
let cargo_toml_path = crate_path.join("Cargo.toml");
6161
let content = tokio::fs::read_to_string(&cargo_toml_path).await.ok()?;
6262

63-
let (name, version) = parse_cargo_toml_name_version(&content)
63+
let (name, version) = package_name_version(&content)
6464
.or_else(|| CargoCrawler::parse_dir_name_version(dir_name))?;
6565

6666
let purl = crate::utils::purl::build_cargo_purl(&name, &version);

0 commit comments

Comments
 (0)