@@ -36,6 +36,17 @@ fn module_lines(module: &str, version: &str, zip_h1: &str, gomod_h1: &str) -> [S
3636 ]
3737}
3838
39+ /// True when `line` is a `go.sum` line (zip or `/go.mod` form) for exactly
40+ /// `module@version`: `"{module} {version} "` or `"{module} {version}/go.mod "`
41+ /// starts it. The trailing space or `/go.mod` keeps `v1.0.0` from matching a
42+ /// longer version such as `v1.0.0-socketpatch.1`.
43+ fn is_version_line ( line : & str , module : & str , version : & str ) -> bool {
44+ line. strip_prefix ( module)
45+ . and_then ( |rest| rest. strip_prefix ( ' ' ) )
46+ . and_then ( |rest| rest. strip_prefix ( version) )
47+ . is_some_and ( |rest| rest. starts_with ( ' ' ) || rest. starts_with ( "/go.mod " ) )
48+ }
49+
3950/// Upsert the two `go.sum` lines for `module@version`. Any existing lines for
4051/// exactly that module+version (either suffix form) are replaced; everything
4152/// else — including stale lines for the *replaced* original module, which go
@@ -310,7 +321,22 @@ impl GoSumEditor {
310321 } ;
311322 }
312323
313- /// [`upsert_module_lines`] in place; `true` when it changed the content.
324+ /// The current content's lines, as `str::lines` splits them.
325+ fn current_lines ( & self ) -> impl Iterator < Item = & str > {
326+ let ( text, lines) = match & self . state {
327+ GoSumState :: Text ( text) => ( Some ( text. lines ( ) ) , None ) ,
328+ GoSumState :: Lines { lines, .. } => ( None , Some ( lines. iter ( ) . map ( String :: as_str) ) ) ,
329+ } ;
330+ text. into_iter ( ) . flatten ( ) . chain ( lines. into_iter ( ) . flatten ( ) )
331+ }
332+
333+ /// Upsert the two `go.sum` lines for `module@version`. Any existing lines
334+ /// for exactly that module+version (either suffix form) are replaced;
335+ /// everything else — including stale lines for the *replaced* original
336+ /// module, which go tolerates and `go mod tidy` prunes — is preserved
337+ /// verbatim. The content may be empty (a project whose `go.sum` does not
338+ /// exist yet). `true` when it changed the content, `false` when the file
339+ /// already carries exactly these lines.
314340 pub ( crate ) fn upsert_module_lines (
315341 & mut self ,
316342 module : & str ,
@@ -319,34 +345,32 @@ impl GoSumEditor {
319345 gomod_h1 : & str ,
320346 ) -> bool {
321347 let want = module_lines ( module, version, zip_h1, gomod_h1) ;
322- let zip_key = format ! ( "{module} {version} " ) ;
323- let gomod_key = format ! ( "{module} {version}/go.mod " ) ;
324- let is_key = |l : & str | l. starts_with ( & zip_key) || l. starts_with ( & gomod_key) ;
325- let applied = {
326- let mut want_seen = [ 0usize ; 2 ] ;
327- let mut stale_key_line = false ;
328- let mut scan = |l : & str | {
329- if l == want[ 0 ] {
330- want_seen[ 0 ] += 1 ;
331- } else if l == want[ 1 ] {
332- want_seen[ 1 ] += 1 ;
333- } else if is_key ( l) {
334- stale_key_line = true ;
335- }
336- } ;
337- match & self . state {
338- GoSumState :: Text ( text) => text. lines ( ) . for_each ( & mut scan) ,
339- GoSumState :: Lines { lines, .. } => {
340- lines. iter ( ) . map ( String :: as_str) . for_each ( & mut scan)
341- }
348+ let is_key = |l : & str | is_version_line ( l, module, version) ;
349+ // "Already applied" means the key-matching lines are exactly the two
350+ // wanted ones — a bare match count is not enough: a stale same-key
351+ // line with a different hash (a union-merged go.sum straddling a
352+ // republish) is a hard go `SECURITY ERROR`, and a duplicated zip line
353+ // can stand in for a missing /go.mod line. Both still need the
354+ // rewrite below.
355+ let mut want_seen = [ 0usize ; 2 ] ;
356+ let mut stale_key_line = false ;
357+ for l in self . current_lines ( ) {
358+ if l == want[ 0 ] {
359+ want_seen[ 0 ] += 1 ;
360+ } else if l == want[ 1 ] {
361+ want_seen[ 1 ] += 1 ;
362+ } else if is_key ( l) {
363+ stale_key_line = true ;
342364 }
343- want_seen == [ 1 , 1 ] && !stale_key_line
344- } ;
345- if applied {
365+ }
366+ if want_seen == [ 1 , 1 ] && !stale_key_line {
346367 return false ;
347368 }
348369 let ( mut lines, eol) = self . take_lines ( ) ;
349370 lines. retain ( |l| !is_key ( l) ) ;
371+ // Insert both lines at their sorted position (stable against an
372+ // unsorted user file: first line strictly greater wins; ties cannot
373+ // occur — the exact-key duplicates were just removed).
350374 let mut out: Vec < String > = Vec :: with_capacity ( lines. len ( ) + 2 ) ;
351375 let mut pending = want. into_iter ( ) . peekable ( ) ;
352376 for line in lines {
@@ -364,37 +388,33 @@ impl GoSumEditor {
364388 true
365389 }
366390
367- /// [`has_module_version`] over the current content.
391+ /// True when the content carries a line (zip or `/go.mod` form) for
392+ /// exactly `module@version` — go records one for every module version
393+ /// its build graph loads.
368394 pub ( crate ) fn has_module_version ( & self , module : & str , version : & str ) -> bool {
369- let zip_key = format ! ( "{module} {version} " ) ;
370- let gomod_key = format ! ( "{module} {version}/go.mod " ) ;
371- let is_key = |l : & str | l. starts_with ( & zip_key) || l. starts_with ( & gomod_key) ;
372- match & self . state {
373- GoSumState :: Text ( text) => text. lines ( ) . any ( is_key) ,
374- GoSumState :: Lines { lines, .. } => lines. iter ( ) . any ( |l| is_key ( l) ) ,
375- }
395+ self . current_lines ( )
396+ . any ( |l| is_version_line ( l, module, version) )
376397 }
377398
378- /// [`remove_exact_module_version_lines`] in place: the removed lines, or
379- /// `None` when nothing matched.
399+ /// Remove the lines for exactly `module@version` (both the zip and
400+ /// `/go.mod` forms). Used to prune the REPLACED original's lines: once a
401+ /// version-pinned `replace` covers the resolved version, go never fetches
402+ /// (or verifies) the original at all, and `go mod tidy` prunes exactly
403+ /// these lines — writing that state up front keeps the first day-2 tidy
404+ /// a byte-level no-op. Returns the removed lines, or `None` when nothing
405+ /// matched.
380406 pub ( crate ) fn remove_exact_module_version_lines (
381407 & mut self ,
382408 module : & str ,
383409 version : & str ,
384410 ) -> Option < Vec < String > > {
385- let zip_key = format ! ( "{module} {version} " ) ;
386- let gomod_key = format ! ( "{module} {version}/go.mod " ) ;
387- let is_key = |l : & str | l. starts_with ( & zip_key) || l. starts_with ( & gomod_key) ;
388- let any = match & self . state {
389- GoSumState :: Text ( text) => text. lines ( ) . any ( is_key) ,
390- GoSumState :: Lines { lines, .. } => lines. iter ( ) . any ( |l| is_key ( l) ) ,
391- } ;
392- if !any {
411+ if !self . has_module_version ( module, version) {
393412 return None ;
394413 }
395414 let ( lines, eol) = self . take_lines ( ) ;
396- let ( removed, kept) : ( Vec < String > , Vec < String > ) =
397- lines. into_iter ( ) . partition ( |l| is_key ( l) ) ;
415+ let ( removed, kept) : ( Vec < String > , Vec < String > ) = lines
416+ . into_iter ( )
417+ . partition ( |l| is_version_line ( l, module, version) ) ;
398418 self . commit ( kept, eol) ;
399419 Some ( removed)
400420 }
0 commit comments