154 lines
3.9 KiB
Go
154 lines
3.9 KiB
Go
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: " <hash> <size> <path>"
|
|
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: " <hash> <size> <path>"
|
|
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()
|
|
}
|