clonepack/internal/clone/apt/release.go
GnomeZworc c4776d81dd
add apt gestion
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-04-25 19:03:26 +02:00

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()
}