Skip to content

Commit 4eec336

Browse files
mikolalysenkoclaude
andcommitted
Refuse conflicting checksums in the Go go.sum backup
save() unioned live go.sum lines with an existing .upstream-go.sum, so a different h1 for the same module and version was kept beside the old one, and a rollback after go mod tidy wrote both back. Refuse the conflict at save time, and refuse a backup that already holds conflicting lines at restore time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7353cc0 commit 4eec336

1 file changed

Lines changed: 83 additions & 0 deletions

File tree

‎crates/socket-patch-core/src/vendor/go_sum_backup.rs‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@ fn selected(text: &str, module: &str, version: &str) -> String {
2727
.join("\n")
2828
}
2929

30+
/// Whether some line of `b` pins the same module and version as a line of
31+
/// `a` with a different hash. Merging the two would leave go.sum with both.
32+
fn conflicts(a: &str, b: &str) -> bool {
33+
b.lines().any(|line| {
34+
let fields: Vec<_> = line.split_whitespace().collect();
35+
a.lines().any(|other| {
36+
let other: Vec<_> = other.split_whitespace().collect();
37+
fields.len() == 3
38+
&& other.len() == 3
39+
&& other[..2] == fields[..2]
40+
&& other[2] != fields[2]
41+
})
42+
})
43+
}
44+
3045
pub(super) async fn save(
3146
root: &Path,
3247
base: &str,
@@ -38,6 +53,12 @@ pub(super) async fn save(
3853
if entry.module == module && entry.version.as_deref() == Some(version) {
3954
if let Some(path) = backup(root, &entry) {
4055
let saved = selected(&read(&path).await?, module, version);
56+
if conflicts(&sums, &saved) {
57+
return Err(format!(
58+
"go.sum conflicts with the checksum saved in {} for {module} {version}",
59+
path.display()
60+
));
61+
}
4162
if let Some(merged) = reinsert_lines(&sums, &saved) {
4263
sums = merged;
4364
}
@@ -49,6 +70,12 @@ pub(super) async fn save(
4970
}
5071
let path = root.join(base).join(NAME);
5172
let old = read(&path).await?;
73+
if conflicts(&old, &sums) {
74+
return Err(format!(
75+
"go.sum conflicts with the checksum saved in {} for {module} {version}",
76+
path.display()
77+
));
78+
}
5279
if let Some(new) = reinsert_lines(&old, &sums) {
5380
tokio::fs::create_dir_all(root.join(base))
5481
.await
@@ -105,6 +132,12 @@ pub(super) async fn restore(
105132
continue;
106133
}
107134
let saved = selected(&read(&backup).await?, &entry.module, version);
135+
if conflicts(&saved, &saved) {
136+
return Err(format!(
137+
"conflicting saved checksums in {}",
138+
backup.display()
139+
));
140+
}
108141
for line in saved.lines() {
109142
let fields: Vec<_> = line.split_whitespace().collect();
110143
if fields.len() != 3 || !fields[2].starts_with("h1:") {
@@ -236,6 +269,56 @@ mod tests {
236269
assert_eq!(read(&root.join("go.sum")).await.unwrap(), conflict);
237270
}
238271

272+
/// Re-pinning over a backup that saved a different hash refuses instead
273+
/// of keeping both, which a rollback after tidy would write to go.sum.
274+
#[tokio::test]
275+
async fn save_refuses_a_checksum_conflicting_with_the_backup() {
276+
let tmp = tempfile::tempdir().unwrap();
277+
let root = tmp.path();
278+
tokio::fs::write(root.join("go.mod"), "module consumer\ngo 1.21\n")
279+
.await
280+
.unwrap();
281+
tokio::fs::write(root.join("go.sum"), SUMS).await.unwrap();
282+
save(root, GO_PATCHES_DIR, MODULE, VERSION).await.unwrap();
283+
let backup = root.join(GO_PATCHES_DIR).join(NAME);
284+
let saved = read(&backup).await.unwrap();
285+
tokio::fs::write(root.join("go.sum"), SUMS.replace("h1:aaaa", "h1:zzzz"))
286+
.await
287+
.unwrap();
288+
assert!(save(root, GO_PATCHES_DIR, MODULE, VERSION)
289+
.await
290+
.unwrap_err()
291+
.contains("conflicts"));
292+
assert_eq!(read(&backup).await.unwrap(), saved);
293+
}
294+
295+
#[tokio::test]
296+
async fn restore_refuses_a_backup_with_conflicting_lines() {
297+
let tmp = tempfile::tempdir().unwrap();
298+
let root = tmp.path();
299+
tokio::fs::write(
300+
root.join("go.mod"),
301+
"module consumer\ngo 1.21\nrequire example.com/upstream v1.0.0\n",
302+
)
303+
.await
304+
.unwrap();
305+
tokio::fs::write(root.join("go.sum"), SUMS).await.unwrap();
306+
ensure_replace_entry(root, MODULE, VERSION, GO_PATCHES_DIR, false)
307+
.await
308+
.unwrap();
309+
let backup = root.join(GO_PATCHES_DIR).join(NAME);
310+
let both = format!("{SUMS}{}", SUMS.replace("h1:aaaa", "h1:zzzz"));
311+
tokio::fs::write(&backup, both).await.unwrap();
312+
tokio::fs::write(root.join("go.sum"), "").await.unwrap();
313+
assert!(
314+
drop_replace_entry(root, MODULE, ReplaceOwner::GoPatches, false)
315+
.await
316+
.unwrap_err()
317+
.contains("conflicting")
318+
);
319+
assert_eq!(read(&root.join("go.sum")).await.unwrap(), "");
320+
}
321+
239322
#[tokio::test]
240323
async fn dry_run_drop_restores_nothing() {
241324
let tmp = tempfile::tempdir().unwrap();

0 commit comments

Comments
 (0)