Skip to content

Commit 92ad228

Browse files
mikolalysenkoclaude
andcommitted
fix(redirect): refuse a lockless cargo pin
A `registry = …` pin reaches only the declarations it sits on, so a crate another package also pulls in is refused — but that check reads Cargo.lock, and a project without one was pinned unconditionally. Rust libraries routinely gitignore their lock, so the silent shape was the common one: the scan reported the crate redirected, vex attested it, and the next build resolved the patched copy for the root and an unpatched crates.io copy for whatever else depends on the crate. Without a lock the dependents question is now answered from the manifests instead: a crate declared beside any other dependency — or beside a workspace member this run could not read (a members glob, or a member outside the project or behind a symbolic link) — is refused `redirect_cargo_lockless_dependents`, whose detail names both remedies. A path dependency on a manifest the same run pins is not company, and a project whose only dependency is the patched crate has nothing that could pull it in and still redirects. Real cargo: e2e_redirect_cargo_shapes lockless-other-dependencies, the direct-and-transitive project with its lock deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent cc04e0f commit 92ad228

10 files changed

Lines changed: 379 additions & 13 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,14 @@ into the new version's section — see docs/releasing.md.
984984
requirements the patched version does not satisfy (cargo resolves those
985985
declarations to another version) is refused
986986
`redirect_cargo_toml_dep_unrewritable`, the Socket backend's code for the
987-
same shape, instead of `redirect_cargo_toml_dep_not_found`.
987+
same shape, instead of `redirect_cargo_toml_dep_not_found`. A project with
988+
NO `Cargo.lock` has no resolved graph to ask, so a crate declared beside
989+
any other dependency — anything but a path dependency on a manifest the
990+
same run pins — or beside a workspace member this run did not read (a
991+
glob, a member outside the project or behind a symbolic link) is refused
992+
`redirect_cargo_lockless_dependents` (commit a lockfile, or use `--mode
993+
vendored`); a project whose only dependency is the patched crate has
994+
nothing that could pull it in and still redirects.
988995
- **CRLF cargo projects redirect in hosted mode.** All-CRLF `Cargo.toml`,
989996
`Cargo.lock` and cargo configs are rewritten with their endings kept
990997
(they were refused), and `remove` / rollback still find the recorded

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs‎

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
//! `[dev-dependencies]`: both pins revert on `remove`.
1818
//! * `direct_and_transitive` — the crate is also a dependency of another
1919
//! crates.io crate: hosted mode refuses it loudly and rewrites nothing.
20+
//! * `lockless_other_dependencies` — the same project with no committed
21+
//! `Cargo.lock`: with no resolved graph to read, a crate declared beside
22+
//! any other dependency is refused just as loudly.
2023
//!
2124
//! Every shape runs the same chain against the real cargo: a baseline build
2225
//! with a private CARGO_HOME (network to crates.io for fixture setup only),
@@ -91,6 +94,9 @@ struct Shape {
9194
/// Re-encode every `Cargo.toml` and the generated `Cargo.lock` with CRLF
9295
/// line endings (a Windows checkout) before the scan.
9396
crlf: bool,
97+
/// Delete `Cargo.lock` after the baseline build, before the scan: the
98+
/// shape a Rust library that gitignores its lock presents.
99+
lockless: bool,
94100
/// A shape hosted mode must REFUSE: the rewriter warning code every
95101
/// patch is skipped with. The scan must leave every file untouched.
96102
refused: Option<&'static str>,
@@ -416,6 +422,9 @@ async fn run_shape(shape: Shape) -> Option<()> {
416422
);
417423
let _ = std::fs::remove_dir_all(proj.join("target"));
418424
}
425+
if shape.lockless {
426+
std::fs::remove_file(proj.join("Cargo.lock")).unwrap();
427+
}
419428
let before = snapshot(&proj);
420429

421430
let mut served = Vec::new();
@@ -488,13 +497,15 @@ async fn run_shape(shape: Shape) -> Option<()> {
488497
shape.tag
489498
);
490499
assert!(!proj.join(".socket").exists(), "{}", shape.tag);
491-
let fetch = cargo(&proj, &["fetch", "--locked"], &home);
492-
assert!(
493-
fetch.status.success(),
494-
"{}: the untouched project still fetches --locked:\n{}",
495-
shape.tag,
496-
stderr(&fetch)
497-
);
500+
if !shape.lockless {
501+
let fetch = cargo(&proj, &["fetch", "--locked"], &home);
502+
assert!(
503+
fetch.status.success(),
504+
"{}: the untouched project still fetches --locked:\n{}",
505+
shape.tag,
506+
stderr(&fetch)
507+
);
508+
}
498509
return Some(());
499510
}
500511
assert_eq!(
@@ -713,6 +724,7 @@ async fn cargo_hosted_multi_version_pins_each_declaration_and_removes_cleanly()
713724
.to_string(),
714725
)],
715726
crlf: false,
727+
lockless: false,
716728
refused: None,
717729
};
718730
let _ = run_shape(shape).await;
@@ -735,6 +747,7 @@ async fn cargo_hosted_legacy_config_is_restored_byte_for_byte() {
735747
"fn main() { println!(\"{}\", cfg_if::socket_patched()); }\n".to_string(),
736748
)],
737749
crlf: false,
750+
lockless: false,
738751
refused: None,
739752
};
740753
let _ = run_shape(shape).await;
@@ -770,6 +783,7 @@ async fn cargo_hosted_config_trailing_bytes_are_restored() {
770783
"fn main() { println!(\"{}\", cfg_if::socket_patched()); }\n".to_string(),
771784
)],
772785
crlf: false,
786+
lockless: false,
773787
refused: None,
774788
};
775789
if run_shape(shape).await.is_none() {
@@ -802,6 +816,7 @@ async fn cargo_hosted_same_line_in_two_sections_removes_cleanly() {
802816
"fn main() { println!(\"{}\", cfg_if::socket_patched()); }\n".to_string(),
803817
)],
804818
crlf: false,
819+
lockless: false,
805820
refused: None,
806821
};
807822
let _ = run_shape(shape).await;
@@ -840,6 +855,7 @@ async fn cargo_hosted_workspace_member_declaration_is_pinned() {
840855
("direct/src/lib.rs", oracle),
841856
],
842857
crlf: false,
858+
lockless: false,
843859
refused: None,
844860
};
845861
let _ = run_shape(shape).await;
@@ -861,6 +877,7 @@ async fn cargo_hosted_crlf_project_keeps_its_line_endings() {
861877
"fn main() { println!(\"{}\", cfg_if::socket_patched()); }\n".to_string(),
862878
)],
863879
crlf: true,
880+
lockless: false,
864881
refused: None,
865882
};
866883
let _ = run_shape(shape).await;
@@ -885,7 +902,34 @@ async fn cargo_hosted_refuses_a_crate_another_crate_depends_on() {
885902
patches: vec![CFG_IF_1],
886903
oracle: Vec::new(),
887904
crlf: false,
905+
lockless: false,
888906
refused: Some("redirect_cargo_transitive_dependents"),
889907
};
890908
let _ = run_shape(shape).await;
891909
}
910+
911+
/// The SAME project with its `Cargo.lock` gitignored (the common shape for
912+
/// a Rust library): with no resolved graph the dependents check above has
913+
/// nothing to read, so the manifests answer instead — cfg-if is declared
914+
/// beside crc32fast, which may well pull it in, and the redirect is refused
915+
/// rather than reported as one while the build links an unpatched copy
916+
/// through crc32fast.
917+
#[tokio::test(flavor = "multi_thread")]
918+
async fn cargo_hosted_refuses_a_lockless_project_with_other_dependencies() {
919+
let shape = Shape {
920+
tag: "lockless-other-dependencies",
921+
files: vec![
922+
(
923+
"Cargo.toml",
924+
consumer_manifest("cfg-if = \"1.0.4\"\ncrc32fast = \"=1.5.0\"\n"),
925+
),
926+
("src/main.rs", "fn main() {}\n".to_string()),
927+
],
928+
patches: vec![CFG_IF_1],
929+
oracle: Vec::new(),
930+
crlf: false,
931+
lockless: true,
932+
refused: Some("redirect_cargo_lockless_dependents"),
933+
};
934+
let _ = run_shape(shape).await;
935+
}

0 commit comments

Comments
 (0)