feat(zeta): manifest model, validation, package.lua emitter

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
This commit is contained in:
2026-08-19 16:36:15 -04:00
co-authored by Sisyphus
parent 5823b35373
commit 970e002fbb
3 changed files with 536 additions and 0 deletions
+251
View File
@@ -0,0 +1,251 @@
// Package zeta models the Zeta package formats (package.lua manifests and
// .recipe files) and emits them byte-for-byte the way the Zeta toolchain does.
//
// The Manifest here covers archive mode only (v1 of zeta-reconstruct emits
// only archive-mode package.lua); install/build strategies are out of scope.
package zeta
import (
"fmt"
"regexp"
"strings"
)
// Manifest is the Go model of a Zeta package.lua manifest (archive mode).
//
// Arch and Files mirror Zeta's KNOWN_KEYS (ZETA/lib/manifest.lua) and are kept
// for forward compatibility; they are not populated in v1 and never emitted.
// Archive is the (v1-only) install strategy: `archive = { strip = N }`.
type Manifest struct {
Name string
Version string
Summary string
URL string
SHA256 string
Arch string
Deps []string
Files []string
Archive *Archive
}
// Archive declares the tarball strip level, mirroring Zeta's
// `archive = { strip = N }` install strategy.
type Archive struct {
Strip int
}
var (
// nameRe mirrors ZETA/lib/path.lua sanitize_name's character class
// ([%w%._+%-]+, i.e. [A-Za-z0-9_.+-]).
nameRe = regexp.MustCompile(`^[A-Za-z0-9_.+-]+$`)
// sha256Re mirrors manifest.lua's HEX64: ^ + 64 × %x + $.
sha256Re = regexp.MustCompile(`^[0-9a-f]{64}$`)
// httpRe mirrors manifest.lua's `^https?://` remote-URL test.
httpRe = regexp.MustCompile(`^https?://`)
// unsafeURLRe mirrors manifest.lua's local-URL safety test: a URL that is
// not http(s) may not contain whitespace or backslashes.
unsafeURLRe = regexp.MustCompile(`[\s\\]`)
// depSpecRe mirrors vercmp.lua parse_dep's first step:
// leading whitespace, a name of [%w%._%+%-]+ characters, whitespace, and
// the rest of the spec (constraint or empty).
depSpecRe = regexp.MustCompile(`^\s*([A-Za-z0-9_.+-]+)\s*(.*)$`)
// depOpRe and depVerRe mirror parse_dep's operator/version extraction.
depOpRe = regexp.MustCompile(`^([<>=~]+)\s*`)
depVerRe = regexp.MustCompile(`^[<>=~]+\s*([A-Za-z0-9_.+-]+)\s*$`)
// validDepOps mirrors ZETA/lib/vercmp.lua's OPS table.
validDepOps = map[string]bool{
">=": true, "<=": true, "==": true, "~=": true,
">": true, "<": true, "=": true,
}
// cleanReDotSlash/cleanReLeadSlash/cleanReTrailSlash mirror the three
// anchored gsubs of manifest.lua's clean_rel, in the same order.
cleanReDotSlash = regexp.MustCompile(`^\./+`)
cleanReLeadSlash = regexp.MustCompile(`^/+`)
cleanReTrailSlash = regexp.MustCompile(`/+$`)
)
// validName mirrors ZETA/lib/path.lua sanitize_name: non-empty, no leading
// dot, and every character in [A-Za-z0-9_.+-].
func validName(name string) bool {
if name == "" || strings.HasPrefix(name, ".") {
return false
}
return nameRe.MatchString(name)
}
// cleanRel mirrors manifest.lua's clean_rel: strip a "./" prefix, a "/"
// prefix, and a "/" suffix (in that order), producing the canonical
// root-relative form.
func cleanRel(p string) string {
p = cleanReDotSlash.ReplaceAllString(p, "")
p = cleanReLeadSlash.ReplaceAllString(p, "")
p = cleanReTrailSlash.ReplaceAllString(p, "")
return p
}
// relativeInside mirrors ZETA/lib/path.lua relative_inside: true if the
// relative path never climbs above its root via ".." segments.
func relativeInside(p string) bool {
depth := 0
for _, seg := range strings.Split(p, "/") {
switch seg {
case "..":
depth--
if depth < 0 {
return false
}
case ".", "":
// "." and empty segments (from "//") neither climb nor descend.
default:
depth++
}
}
return true
}
// escape mirrors packager.lua's escape: quote backslash-quoting first, then
// newline escaping, in the same order as the Lua gsub chain.
func escape(s string) string {
s = strings.ReplaceAll(s, `"`, `\"`)
s = strings.ReplaceAll(s, "\n", `\n`)
return s
}
// parseDep mirrors ZETA/lib/vercmp.lua parse_dep:
// "pcre2>=10.42" -> ("pcre2", ">=", "10.42"), "libffi" -> ("libffi", "", "").
// A bare "=" operator is normalized to "==", as in vercmp.lua.
func parseDep(spec string) (name, op, version string, err error) {
sm := depSpecRe.FindStringSubmatch(spec)
if sm == nil {
return "", "", "", fmt.Errorf("bad dependency %q", spec)
}
name, rest := sm[1], sm[2]
if rest == "" {
return name, "", "", nil
}
om := depOpRe.FindStringSubmatch(rest)
vm := depVerRe.FindStringSubmatch(rest)
if om == nil || vm == nil || !validDepOps[om[1]] {
return "", "", "", fmt.Errorf("bad dependency constraint %q (expected NAME OP VERSION)", spec)
}
op = om[1]
version = vm[1]
if op == "=" {
op = "=="
}
return name, op, version, nil
}
// Validate checks the manifest against Zeta's own rules, mirroring
// ZETA/lib/manifest.lua normalize exactly (archive path only):
//
// 1. name: non-empty, no leading dot, all of [A-Za-z0-9_.+-]
// 2. version: non-empty
// 3. sha256: when set, exactly 64 hex characters (uppercase accepted and
// normalized to lowercase, as manifest.lua does)
// 4. url: non-http(s) URLs must not contain whitespace or backslashes
// 5. deps: each entry must parse as NAME [OP VERSION] with a known operator,
// and must not depend on the package itself
// 6. files: entries must be non-empty, clean to a path that stays inside the
// install root, and appear only once
// 7. archive.strip: non-negative integer
// 8. archive mode requires a url (v1 is archive-only, so url is always
// required)
// 9. remote http(s) urls must declare sha256
func (m *Manifest) Validate() error {
if !validName(m.Name) {
return fmt.Errorf("manifest: missing or invalid package name")
}
if m.Version == "" {
return fmt.Errorf("manifest: package %q missing version", m.Name)
}
if m.SHA256 != "" {
lower := strings.ToLower(m.SHA256)
if !sha256Re.MatchString(lower) {
return fmt.Errorf("manifest: package %q sha256 must be exactly 64 hex characters", m.Name)
}
m.SHA256 = lower
}
if m.URL != "" && !httpRe.MatchString(m.URL) && unsafeURLRe.MatchString(m.URL) {
return fmt.Errorf("manifest: package %q url %q looks unsafe", m.Name, m.URL)
}
for _, d := range m.Deps {
depName, _, _, err := parseDep(d)
if err != nil {
return fmt.Errorf("manifest: package %q: %w", m.Name, err)
}
if depName == m.Name {
return fmt.Errorf("manifest: package %q depends on itself", m.Name)
}
}
seen := make(map[string]bool, len(m.Files))
for _, f := range m.Files {
if f == "" {
return fmt.Errorf("manifest: package %q has an empty files entry", m.Name)
}
rel := cleanRel(f)
if rel == "" || !relativeInside(rel) {
return fmt.Errorf("manifest: package %q files entry %q escapes the root", m.Name, f)
}
if seen[rel] {
return fmt.Errorf("manifest: package %q lists %q twice in files", m.Name, rel)
}
seen[rel] = true
}
if m.Archive != nil && m.Archive.Strip < 0 {
return fmt.Errorf("manifest: package %q archive.strip must be a non-negative integer", m.Name)
}
if m.URL == "" {
return fmt.Errorf("manifest: package %q archive mode requires a url", m.Name)
}
if httpRe.MatchString(m.URL) && m.SHA256 == "" {
return fmt.Errorf("manifest: package %q must declare sha256 for a remote url", m.Name)
}
return nil
}
// Emit renders the archive-mode package.lua with the exact byte format of
// packager.lua's write_manifest: 2-space indent, "="-aligned field names in
// the order name/version/summary/url/sha256/deps/archive, `archive = { strip = 1 }`,
// and no test function. Summary and deps values are escaped (quote/newline);
// name/version/url/sha256 are emitted raw (they are validated-safe). Emit
// fails if the manifest does not Validate.
func (m *Manifest) Emit() (string, error) {
if err := m.Validate(); err != nil {
return "", err
}
var b strings.Builder
b.WriteString("return {\n")
b.WriteString(fmt.Sprintf(" %-8s= \"%s\",\n", "name", m.Name))
b.WriteString(fmt.Sprintf(" %-8s= \"%s\",\n", "version", m.Version))
b.WriteString(fmt.Sprintf(" %-8s= \"%s\",\n", "summary", escape(m.Summary)))
b.WriteString(fmt.Sprintf(" %-8s= \"%s\",\n", "url", m.URL))
b.WriteString(fmt.Sprintf(" %-8s= \"%s\",\n", "sha256", m.SHA256))
if len(m.Deps) > 0 {
items := make([]string, len(m.Deps))
for i, d := range m.Deps {
items[i] = `"` + escape(d) + `"`
}
b.WriteString(fmt.Sprintf(" %-8s= { %s },\n", "deps", strings.Join(items, ", ")))
} else {
b.WriteString(fmt.Sprintf(" %-8s= {},\n", "deps"))
}
b.WriteString(fmt.Sprintf(" %-8s= { strip = 1 },\n", "archive"))
b.WriteString("}\n")
return b.String(), nil
}