@@ -3,116 +3,13 @@ use std::path::{Path, PathBuf};
33
44use super :: listing:: list_dir_sync;
55use super :: types:: { CrawledPackage , CrawlerOptions } ;
6+ use crate :: formats:: cargo:: manifest:: package_name_version;
67use crate :: patch:: path_safety;
78use crate :: utils:: fs:: { is_dir, run_blocking} ;
89
910#[ cfg( test) ]
1011mod 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 {
431328mod 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]\n name = \" my-crate\" \n version = { 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]\n name = \" a\" \n [oops\n version = \" 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"
826633name = "serde"
827634version = "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 ]\n name = \" tokio\" \n version = \" 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"
850657version = "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]\n name = 'serde'\n version = '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]\n name = 'tokio'\n version = \" 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]\n name = 'serde' # the lib\n version = '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]\n name = 'my-crate'\n version.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
0 commit comments