feat(arch/binary): .PKGINFO parser
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]>
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
package binary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PKGInfo is the parsed content of an ALPM .PKGINFO file, the metadata
|
||||||
|
// member at the root of an Arch .pkg.tar.zst package.
|
||||||
|
type PKGInfo struct {
|
||||||
|
Name, Version, Desc, URL, Arch string
|
||||||
|
Depends, OptDepends, Provides, Conflicts, Replaces []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses ALPM .PKGINFO content: lines of "key = value", full-line
|
||||||
|
// `#` comments and blank lines ignored. Multi-value keys (depend,
|
||||||
|
// optdepend, provides, conflicts, replaces, license) accumulate across
|
||||||
|
// lines; single-value keys (pkgname, pkgver, pkgdesc, url, arch, pkgbase)
|
||||||
|
// take the last occurrence. pkgver is the FULL version, already in
|
||||||
|
// epoch+pkgrel form (e.g. "1.0-1" or "1:1.0.0-1"), and is passed through
|
||||||
|
// verbatim. Unknown keys are ignored. Malformed lines (no '=') are
|
||||||
|
// reported through warn and skipped, not fatal.
|
||||||
|
//
|
||||||
|
// Parse returns an error if pkgname is missing: a .PKGINFO without a name
|
||||||
|
// is invalid for conversion.
|
||||||
|
func Parse(src string, warn func(string)) (*PKGInfo, error) {
|
||||||
|
info := &PKGInfo{}
|
||||||
|
haveName := false
|
||||||
|
for _, line := range strings.Split(src, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eq := strings.Index(line, "=")
|
||||||
|
if eq < 0 {
|
||||||
|
if warn != nil {
|
||||||
|
warn("malformed .PKGINFO line (no '='): " + line)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(line[:eq])
|
||||||
|
value := strings.TrimSpace(line[eq+1:])
|
||||||
|
switch key {
|
||||||
|
case "pkgname":
|
||||||
|
info.Name = value
|
||||||
|
haveName = true
|
||||||
|
case "pkgver":
|
||||||
|
info.Version = value
|
||||||
|
case "pkgdesc":
|
||||||
|
info.Desc = value
|
||||||
|
case "url":
|
||||||
|
info.URL = value
|
||||||
|
case "arch":
|
||||||
|
info.Arch = value
|
||||||
|
case "depend":
|
||||||
|
info.Depends = append(info.Depends, value)
|
||||||
|
case "optdepend":
|
||||||
|
info.OptDepends = append(info.OptDepends, value)
|
||||||
|
case "provides":
|
||||||
|
info.Provides = append(info.Provides, value)
|
||||||
|
case "conflicts":
|
||||||
|
info.Conflicts = append(info.Conflicts, value)
|
||||||
|
case "replaces":
|
||||||
|
info.Replaces = append(info.Replaces, value)
|
||||||
|
default:
|
||||||
|
// pkgbase, license, builddate, packager, size, xdata and any
|
||||||
|
// unknown keys are not represented in PKGInfo; ignore them.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !haveName {
|
||||||
|
return nil, errors.New("pkginfo: missing pkgname")
|
||||||
|
}
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package binary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// readFixture loads a committed .PKGINFO fixture relative to the repo root.
|
||||||
|
func readFixture(t *testing.T, path string) string {
|
||||||
|
t.Helper()
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read fixture %s: %v", path, err)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPKGInfo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
src string
|
||||||
|
want *PKGInfo
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "hello fixture",
|
||||||
|
src: readFixture(t, "../../../testdata/pkginfo/hello.PKGINFO"),
|
||||||
|
want: &PKGInfo{
|
||||||
|
Name: "hello",
|
||||||
|
Version: "1.0-1",
|
||||||
|
Desc: "A tiny demonstration program",
|
||||||
|
URL: "https://example.org",
|
||||||
|
Arch: "x86_64",
|
||||||
|
Depends: []string{"glibc", "zlib"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi fixture",
|
||||||
|
src: readFixture(t, "../../../testdata/pkginfo/multi.PKGINFO"),
|
||||||
|
want: &PKGInfo{
|
||||||
|
Name: "multiload",
|
||||||
|
Version: "2.3-1",
|
||||||
|
Desc: "Multi dep test",
|
||||||
|
URL: "https://example.org",
|
||||||
|
Arch: "x86_64",
|
||||||
|
Depends: []string{"glibc", "pcre2>=10.42"},
|
||||||
|
OptDepends: []string{"python: for scripting"},
|
||||||
|
Provides: []string{"multiload"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "epoch fixture keeps colon",
|
||||||
|
src: readFixture(t, "../../../testdata/pkginfo/epoch.PKGINFO"),
|
||||||
|
want: &PKGInfo{
|
||||||
|
Name: "epochtool",
|
||||||
|
Version: "1:1.0.0-1",
|
||||||
|
Desc: "Epoch test",
|
||||||
|
URL: "https://example.org",
|
||||||
|
Arch: "x86_64",
|
||||||
|
Depends: []string{"glibc"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comments blank lines unknown keys and whitespace",
|
||||||
|
src: strings.Join([]string{
|
||||||
|
"# full-line comment",
|
||||||
|
"",
|
||||||
|
" pkgname = spaced ",
|
||||||
|
"pkgver = 9.9-9",
|
||||||
|
"pkgbase = ignored-base",
|
||||||
|
"license = GPL2",
|
||||||
|
"license = MIT",
|
||||||
|
"builddate = 1724000000",
|
||||||
|
"unknown = whatever",
|
||||||
|
"conflicts = oldtool",
|
||||||
|
"replaces = replaced",
|
||||||
|
}, "\n") + "\n",
|
||||||
|
want: &PKGInfo{
|
||||||
|
Name: "spaced",
|
||||||
|
Version: "9.9-9",
|
||||||
|
Conflicts: []string{"oldtool"},
|
||||||
|
Replaces: []string{"replaced"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last occurrence wins for single-value keys",
|
||||||
|
src: "pkgname = first\npkgver = 1.0-1\npkgname = second\npkgver = 2.0-2\n",
|
||||||
|
want: &PKGInfo{
|
||||||
|
Name: "second",
|
||||||
|
Version: "2.0-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := Parse(tt.src, func(msg string) {
|
||||||
|
t.Errorf("unexpected warn: %s", msg)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("Parse() = %#v, want %#v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPKGInfoMalformedLineWarns(t *testing.T) {
|
||||||
|
src := "pkgname = ok\npkgver = 1.0-1\ngarbage no equals\n"
|
||||||
|
var warns []string
|
||||||
|
info, err := Parse(src, func(msg string) {
|
||||||
|
warns = append(warns, msg)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
if len(warns) != 1 {
|
||||||
|
t.Fatalf("warn called %d times, want exactly 1: %v", len(warns), warns)
|
||||||
|
}
|
||||||
|
if !strings.Contains(warns[0], "garbage no equals") {
|
||||||
|
t.Errorf("warn message %q does not mention the malformed line", warns[0])
|
||||||
|
}
|
||||||
|
if info.Name != "ok" || info.Version != "1.0-1" {
|
||||||
|
t.Errorf("Parse() = %#v, want name ok / version 1.0-1 despite malformed line", info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPKGInfoMissingName(t *testing.T) {
|
||||||
|
_, err := Parse("pkgver = 1.0-1\n", func(string) {})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Parse() error = nil, want error for missing pkgname")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user