package updates import ( "sort" "strings" ) // isSecuritySuite reports whether an apt suite carries security fixes. Debian // 11+ and every supported Ubuntu name them "-security". func isSecuritySuite(s string) bool { return strings.HasSuffix(s, "-security") } // securitySources reduces a host's apt source files to only the entries that // point at a security suite, so an upgrade run against them installs security // fixes and nothing else. // // It is a pure function of file contents so it is tested on any platform. The // caller writes list to a *.list file and deb822 to a *.sources file in a // temporary SourceParts directory: keeping deb822 paragraphs as deb822 means // an inline Signed-By key block survives verbatim, which a conversion to // one-line format could not carry. // // ok is false when no security suite exists at all. The caller must then // report unsupported, never fall back to installing everything. func securitySources(files map[string]string) (list string, deb822 string, ok bool) { paths := make([]string, 0, len(files)) for p := range files { paths = append(paths, p) } sort.Strings(paths) // deterministic output var lb, db strings.Builder for _, p := range paths { if strings.HasSuffix(p, ".sources") { db.WriteString(filterDeb822(files[p])) } else { lb.WriteString(filterOneLine(files[p])) } } list, deb822 = lb.String(), db.String() return list, deb822, list != "" || deb822 != "" } func filterOneLine(content string) string { var b strings.Builder for _, raw := range strings.Split(content, "\n") { line := strings.TrimSpace(raw) if line == "" || strings.HasPrefix(line, "#") { continue } fields := strings.Fields(line) if len(fields) < 3 || fields[0] != "deb" { continue } i := 1 if strings.HasPrefix(fields[i], "[") { // Options run until the token that closes the bracket. for i < len(fields) && !strings.HasSuffix(fields[i], "]") { i++ } i++ } // fields[i] is the URI, fields[i+1] the suite. if i+1 < len(fields) && isSecuritySuite(fields[i+1]) { b.WriteString(line) b.WriteString("\n") } } return b.String() } func filterDeb822(content string) string { var b strings.Builder for _, para := range strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n\n") { lines := strings.Split(strings.Trim(para, "\n"), "\n") var out []string isDeb, enabled, kept := false, true, false for _, l := range lines { key, val, found := strings.Cut(l, ":") k := strings.ToLower(strings.TrimSpace(key)) v := strings.TrimSpace(val) switch { case found && k == "types": for _, t := range strings.Fields(v) { if t == "deb" { isDeb = true } } case found && k == "enabled": enabled = strings.ToLower(v) != "no" case found && k == "suites": var sec []string for _, s := range strings.Fields(v) { if isSecuritySuite(s) { sec = append(sec, s) } } if len(sec) == 0 { continue // drop the line; the paragraph is dropped below } kept = true l = "Suites: " + strings.Join(sec, " ") } out = append(out, l) } if isDeb && enabled && kept { b.WriteString(strings.Join(out, "\n")) b.WriteString("\n\n") } } return b.String() }