From 6c46fcb4b6ada92505bbf9440acf338bafb51cbe Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:23:41 +0000 Subject: [PATCH 1/4] Start fix for #1833 Assisted-by: Claude Code:claude-opus-5-5 From a7a938dc4700361a200af44bcd8e3629534108ef Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:38:15 +0000 Subject: [PATCH 2/4] Read [..] classes in Cargo member globs A workspace whose members list uses a character class, such as members = ["crates/[ab]*"], expanded to no members: the member walk's own matcher read only * and ?, while cargo's glob crate also reads [abc], [a-z] and [!x]. Vendored Cargo, the upstream Cargo redirect and hosted Cargo then never saw those crates. Match each glob segment through workspace_globs::segment_glob_matches, the matcher the npm-family walks already use, and drop the copy. Fixes #1833 Assisted-by: Claude Code:claude-opus-5-5 --- .../src/utils/cargo_workspace.rs | 82 +++++++++++++------ 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/crates/socket-patch-core/src/utils/cargo_workspace.rs b/crates/socket-patch-core/src/utils/cargo_workspace.rs index 6209a38fc..766bf381a 100644 --- a/crates/socket-patch-core/src/utils/cargo_workspace.rs +++ b/crates/socket-patch-core/src/utils/cargo_workspace.rs @@ -17,6 +17,7 @@ use std::sync::Arc; use toml_edit::{DocumentMut, Item, Table}; +use crate::utils::workspace_globs::segment_glob_matches; use crate::vendor::lock_inventory::view::{MemoryProject, ProjectView}; use crate::vendor::parse_memo::ParseMemo; @@ -349,10 +350,12 @@ pub(crate) fn normalize_rel(base: &str, rel: &str) -> Option { /// directory. const CARGO_SKIP: &[&str] = &["target"]; -/// Expand a cargo `members` / `exclude` glob (`*`, `?`, `**`) to the -/// repo-relative directories it names. A wildcard segment never matches a -/// dot-directory (unless the segment itself starts with `.`, as `.*` does) -/// or a name in `skip`; a literal segment names any real directory. +/// Expand a cargo `members` / `exclude` glob (`*`, `?`, `[..]` classes, +/// `**`) to the repo-relative directories it names. A wildcard segment +/// never matches a dot-directory (unless the segment itself starts with +/// `.`, as `.*` does) or a name in `skip`; a literal segment names any +/// real directory. A segment matches as `workspace_globs` reads one, so a +/// `[` that never closes is a literal character. pub(crate) fn expand_glob(tree: &dyn DirTree, pattern: &str, skip: &[&str]) -> Vec { expand_glob_bounded(tree, pattern, skip).0 } @@ -391,7 +394,7 @@ fn expand_from( out.push(at); return; }; - if !seg.contains(['*', '?']) { + if !seg.contains(['*', '?', '[']) { let next = join_rel(&at, seg); if tree.is_real_dir(&next) { expand_from(tree, next, tail, skip, out); @@ -415,26 +418,15 @@ fn expand_from( } return; } + let pattern: Vec = seg.chars().collect(); for child in children { - if wildcard_match(seg.as_bytes(), child.as_bytes()) { + let name: Vec = child.chars().collect(); + if segment_glob_matches(&pattern, &name) { expand_from(tree, join_rel(&at, &child), tail, skip, out); } } } -fn wildcard_match(pattern: &[u8], name: &[u8]) -> bool { - match (pattern.first(), name.first()) { - (None, None) => true, - (Some(b'*'), _) => { - wildcard_match(&pattern[1..], name) - || (!name.is_empty() && wildcard_match(pattern, &name[1..])) - } - (Some(b'?'), Some(_)) => wildcard_match(&pattern[1..], &name[1..]), - (Some(p), Some(n)) if p == n => wildcard_match(&pattern[1..], &name[1..]), - _ => false, - } -} - #[cfg(test)] mod tests { use super::*; @@ -614,13 +606,53 @@ mod tests { assert_eq!(member_manifests(root), vec!["crates/a/Cargo.toml"]); } + /// #1833: cargo expands `members` with the `glob` crate, so `[ab]`, + /// `[a-b]` and `[!x]` classes name the directories they match, as `*` + /// and `?` do. + #[test] + fn member_globs_read_character_classes() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + for name in ["alpha", "beta", "gamma"] { + write(root, &format!("crates/{name}/Cargo.toml"), &pkg(name)); + write(root, &format!("tools/{name}/Cargo.toml"), &pkg(name)); + write(root, &format!("libs/{name}/Cargo.toml"), &pkg(name)); + } + write( + root, + "Cargo.toml", + "[workspace]\nmembers = [\"crates/[ab]*\", \"tools/[a-b]?*\", \"libs/[!a]*\"]\n", + ); + assert_eq!( + member_manifests(root), + vec![ + "crates/alpha/Cargo.toml", + "crates/beta/Cargo.toml", + "libs/beta/Cargo.toml", + "libs/gamma/Cargo.toml", + "tools/alpha/Cargo.toml", + "tools/beta/Cargo.toml", + ] + ); + } + + /// A segment whose `[` never closes is a literal name, as `*`-free + /// segments are, and a `[[]` class matches a literal `[`. #[test] - fn wildcard_matching() { - assert!(wildcard_match(b"a*c", b"abbc")); - assert!(wildcard_match(b"*", b"")); - assert!(wildcard_match(b"a?c", b"abc")); - assert!(!wildcard_match(b"a?c", b"ac")); - assert!(!wildcard_match(b"a*d", b"abc")); + fn an_unclosed_bracket_is_literal() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + write(root, "odd[dir/Cargo.toml", &pkg("odd")); + write(root, "x[1/Cargo.toml", &pkg("x")); + write( + root, + "Cargo.toml", + "[workspace]\nmembers = [\"odd[dir\", \"x[[]?\"]\n", + ); + assert_eq!( + member_manifests(root), + vec!["odd[dir/Cargo.toml", "x[1/Cargo.toml"] + ); } /// V-7: the memoized member walk returns exactly what the per-call From 54c38dad58f54e9aa5183267dc12d8d0d2a624f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:06:17 +0000 Subject: [PATCH 3/4] Read Cargo [^x] and member patterns as cargo Cargo's glob crate negates a class only on `!`, so members = ["crates/[^x]*"] names crates starting with `^` or `x`; the shared matcher read it as negated and picked the opposite crates. A `^` opening a class is now kept as a member. A segment counts as a glob only when it has `*`, `?` or a class that closes, so an unclosed `[` stays a literal directory, and the lockless Cargo redirect refusal now names `crates/[ab]` as a members pattern instead of a missing member, matching the member walk. Refs #1833 Assisted-by: Claude Code:claude-opus-5-5 --- .../src/patch/redirect/mod.rs | 11 +++- .../src/utils/cargo_workspace.rs | 65 ++++++++++++++----- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index ccb8bb226..aba5487f8 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -2719,7 +2719,7 @@ fn cargo_lockless_other_dependencies( .into_iter() .flat_map(|a| a.iter().filter_map(toml_edit::Value::as_str)); for member in members { - let named = if member.contains(['*', '?']) { + let named = if crate::utils::cargo_workspace::is_member_pattern(member) { format!("the workspace members pattern `{member}`") } else if crate::utils::cargo_workspace::normalize_rel(dir, member) .is_some_and(|d| known.contains(format!("{d}/Cargo.toml").as_str())) @@ -15631,6 +15631,15 @@ mod tests { detail.contains("the workspace members pattern `crates/*`"), "{detail}" ); + // A `[..]` class is a pattern too (#1833), not a literal member. + let detail = refused(one( + "[workspace]\nmembers = [\"crates/[ab]\"]\n\n[package]\nname = \"app\"\n\ + version = \"0.1.0\"\n\n[dependencies]\nserde = \"1.0.190\"\n", + )); + assert!( + detail.contains("the workspace members pattern `crates/[ab]`"), + "{detail}" + ); } /// The lockless shapes that stay redirectable: the patched crate alone, diff --git a/crates/socket-patch-core/src/utils/cargo_workspace.rs b/crates/socket-patch-core/src/utils/cargo_workspace.rs index 766bf381a..3be6415c3 100644 --- a/crates/socket-patch-core/src/utils/cargo_workspace.rs +++ b/crates/socket-patch-core/src/utils/cargo_workspace.rs @@ -380,6 +380,30 @@ pub(crate) fn expand_glob_bounded( (out, truncated) } +/// Whether a `members` entry is a glob rather than a literal path: some +/// segment holds `*`, `?` or a `[..]` class that closes. A `[` that never +/// closes is not a class. +pub(crate) fn is_member_pattern(pattern: &str) -> bool { + pattern.split('/').any(segment_is_pattern) +} + +fn segment_is_pattern(seg: &str) -> bool { + seg.contains(['*', '?']) + || seg.match_indices('[').any(|(at, _)| { + let body = &seg[at + 1..]; + let body = body.strip_prefix(['!', '^']).unwrap_or(body); + body.char_indices().skip(1).any(|(_, c)| c == ']') + }) +} + +/// `seg` as [`segment_glob_matches`] should read it for cargo. Cargo's +/// `glob` crate negates a class only on `!`, so a `^` opening a class is a +/// member; the shared matcher also negates on `^`, so a NUL member (which +/// no directory name holds) goes ahead of it. +fn cargo_segment(seg: &str) -> Vec { + seg.replace("[^", "[\0^").chars().collect() +} + fn expand_from( tree: &dyn DirTree, at: String, @@ -394,7 +418,7 @@ fn expand_from( out.push(at); return; }; - if !seg.contains(['*', '?', '[']) { + if !segment_is_pattern(seg) { let next = join_rel(&at, seg); if tree.is_real_dir(&next) { expand_from(tree, next, tail, skip, out); @@ -418,7 +442,7 @@ fn expand_from( } return; } - let pattern: Vec = seg.chars().collect(); + let pattern = cargo_segment(seg); for child in children { let name: Vec = child.chars().collect(); if segment_glob_matches(&pattern, &name) { @@ -608,7 +632,8 @@ mod tests { /// #1833: cargo expands `members` with the `glob` crate, so `[ab]`, /// `[a-b]` and `[!x]` classes name the directories they match, as `*` - /// and `?` do. + /// and `?` do. The crate negates only on `!`: `[^x]` is the class of + /// `^` and `x`. #[test] fn member_globs_read_character_classes() { let tmp = tempfile::tempdir().unwrap(); @@ -618,14 +643,19 @@ mod tests { write(root, &format!("tools/{name}/Cargo.toml"), &pkg(name)); write(root, &format!("libs/{name}/Cargo.toml"), &pkg(name)); } + write(root, "carets/xtra/Cargo.toml", &pkg("xtra")); + write(root, "carets/^hat/Cargo.toml", &pkg("hat")); write( root, "Cargo.toml", - "[workspace]\nmembers = [\"crates/[ab]*\", \"tools/[a-b]?*\", \"libs/[!a]*\"]\n", + "[workspace]\nmembers = [\"crates/[ab]*\", \"tools/[a-b]?*\", \"libs/[!a]*\", \ + \"carets/[^x]*\"]\n", ); assert_eq!( member_manifests(root), vec![ + "carets/^hat/Cargo.toml", + "carets/xtra/Cargo.toml", "crates/alpha/Cargo.toml", "crates/beta/Cargo.toml", "libs/beta/Cargo.toml", @@ -636,23 +666,24 @@ mod tests { ); } - /// A segment whose `[` never closes is a literal name, as `*`-free - /// segments are, and a `[[]` class matches a literal `[`. + /// A `[[]` class matches a literal `[`. #[test] - fn an_unclosed_bracket_is_literal() { + fn a_bracket_class_matches_a_literal_bracket() { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); - write(root, "odd[dir/Cargo.toml", &pkg("odd")); write(root, "x[1/Cargo.toml", &pkg("x")); - write( - root, - "Cargo.toml", - "[workspace]\nmembers = [\"odd[dir\", \"x[[]?\"]\n", - ); - assert_eq!( - member_manifests(root), - vec!["odd[dir/Cargo.toml", "x[1/Cargo.toml"] - ); + write(root, "Cargo.toml", "[workspace]\nmembers = [\"x[[]?\"]\n"); + assert_eq!(member_manifests(root), vec!["x[1/Cargo.toml"]); + } + + #[test] + fn member_patterns_are_wildcards_or_closed_classes() { + for pattern in ["crates/*", "a?", "crates/[ab]", "x[!a]y", "x[^a]", "[]]"] { + assert!(is_member_pattern(pattern), "{pattern}"); + } + for pattern in ["crates/a", "odd[dir", "x[]", "x[!]", "a]b["] { + assert!(!is_member_pattern(pattern), "{pattern}"); + } } /// V-7: the memoized member walk returns exactly what the per-call From d17715810ae593a1f79b43db53d876d6f5c1e8f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 03:01:14 +0000 Subject: [PATCH 4/4] Read Cargo [^] as a class, as cargo does A members entry like x[^] is a class matching `^` to cargo's glob crate, which negates only on `!`. The pattern check treated the `^` as a negation and read the entry as a literal directory instead. Refs #1833 Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-core/src/utils/cargo_workspace.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/socket-patch-core/src/utils/cargo_workspace.rs b/crates/socket-patch-core/src/utils/cargo_workspace.rs index 3be6415c3..db99948a0 100644 --- a/crates/socket-patch-core/src/utils/cargo_workspace.rs +++ b/crates/socket-patch-core/src/utils/cargo_workspace.rs @@ -391,7 +391,7 @@ fn segment_is_pattern(seg: &str) -> bool { seg.contains(['*', '?']) || seg.match_indices('[').any(|(at, _)| { let body = &seg[at + 1..]; - let body = body.strip_prefix(['!', '^']).unwrap_or(body); + let body = body.strip_prefix('!').unwrap_or(body); body.char_indices().skip(1).any(|(_, c)| c == ']') }) } @@ -678,7 +678,15 @@ mod tests { #[test] fn member_patterns_are_wildcards_or_closed_classes() { - for pattern in ["crates/*", "a?", "crates/[ab]", "x[!a]y", "x[^a]", "[]]"] { + for pattern in [ + "crates/*", + "a?", + "crates/[ab]", + "x[!a]y", + "x[^a]", + "x[^]", + "[]]", + ] { assert!(is_member_pattern(pattern), "{pattern}"); } for pattern in ["crates/a", "odd[dir", "x[]", "x[!]", "a]b["] {