package apt import ( "bytes" "fmt" "strconv" "strings" "time" ) // ReleaseFile represents a parsed Debian Release / InRelease file. type ReleaseFile struct { Origin string Suite string Codename string Components string Architectures string SHA256 []ReleaseEntry } // ReleaseEntry is a single line from the SHA256 section of a Release file. type ReleaseEntry struct { Hash string Size int64 Path string } // ParseRelease strips PGP armor (if present) and parses an InRelease or Release file. func ParseRelease(data []byte) (*ReleaseFile, error) { text := stripPGPArmor(string(data)) rf := &ReleaseFile{} inSHA256 := false for _, rawLine := range strings.Split(text, "\n") { line := strings.TrimRight(rawLine, " \t\r") if inSHA256 { if line == "" || (len(line) > 0 && line[0] != ' ' && line[0] != '\t') { // A non-indented non-empty line ends the SHA256 block. if line != "" { inSHA256 = false // Fall through to parse this line as a key-value pair. } else { inSHA256 = false continue } } else { // Indented line: " " entry, err := parseReleaseEntry(line) if err == nil { rf.SHA256 = append(rf.SHA256, entry) } continue } } if strings.HasPrefix(line, "SHA256:") { inSHA256 = true continue } key, value, ok := strings.Cut(line, ":") if !ok { continue } key = strings.TrimSpace(key) value = strings.TrimSpace(value) switch key { case "Origin": rf.Origin = value case "Suite": rf.Suite = value case "Codename": rf.Codename = value case "Components": rf.Components = value case "Architectures": rf.Architectures = value } } return rf, nil } // parseReleaseEntry parses a single indented line from the SHA256 section. // Format: " " func parseReleaseEntry(line string) (ReleaseEntry, error) { fields := strings.Fields(line) if len(fields) != 3 { return ReleaseEntry{}, fmt.Errorf("malformed SHA256 entry: %q", line) } size, err := strconv.ParseInt(fields[1], 10, 64) if err != nil { return ReleaseEntry{}, fmt.Errorf("malformed size in SHA256 entry: %w", err) } return ReleaseEntry{Hash: fields[0], Size: size, Path: fields[2]}, nil } // stripPGPArmor removes the PGP signed-message wrapper if present. // It returns only the signed body (between the header and the signature). func stripPGPArmor(text string) string { const beginSigned = "-----BEGIN PGP SIGNED MESSAGE-----" const beginSig = "-----BEGIN PGP SIGNATURE-----" if !strings.Contains(text, beginSigned) { return text } // Drop the header lines (Hash: etc.) up to the first blank line. after, found := strings.CutPrefix(text, beginSigned) if !found { // beginSigned not at start — find it idx := strings.Index(text, beginSigned) if idx < 0 { return text } after = text[idx+len(beginSigned):] } // Skip the armor header lines (e.g. "Hash: SHA512") until the blank line. blankIdx := strings.Index(after, "\n\n") if blankIdx < 0 { return text } body := after[blankIdx+2:] // Cut off at the PGP signature block. if sigIdx := strings.Index(body, beginSig); sigIdx >= 0 { body = body[:sigIdx] } return strings.TrimRight(body, "\n\r ") + "\n" } // GenerateRelease generates a plain unsigned Release file. func GenerateRelease(suite, codename string, components, architectures []string, entries []ReleaseEntry) []byte { var buf bytes.Buffer fmt.Fprintf(&buf, "Origin: ClonePack Mirror\n") fmt.Fprintf(&buf, "Suite: %s\n", suite) fmt.Fprintf(&buf, "Codename: %s\n", codename) fmt.Fprintf(&buf, "Components: %s\n", strings.Join(components, " ")) fmt.Fprintf(&buf, "Architectures: %s\n", strings.Join(architectures, " ")) fmt.Fprintf(&buf, "Date: %s\n", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 UTC")) fmt.Fprintf(&buf, "SHA256:\n") for _, e := range entries { fmt.Fprintf(&buf, " %s %d %s\n", e.Hash, e.Size, e.Path) } return buf.Bytes() }