package arch import ( "bufio" "bytes" "errors" "fmt" "path" "regexp" "strings" ) // SRCINFO is the parsed content of a packaging repo's .SRCINFO (the output of // `makepkg --printsrcinfo`): the pkgbase section plus one section per split package. Values are // already bash-expanded, so no PKGBUILD interpretation is needed. type SRCINFO struct { PkgBase string PkgVer string PkgRel string Epoch string URL string // Sources lists every source entry of every architecture (source, source_x86_64, ...) in // order of appearance; Source.Arch says which. Sources []Source // PkgNames lists the split packages built from this pkgbase, in order. PkgNames []string ValidPGPKeys []string // Fields holds every other pkgbase-section key (pkgdesc, arch, license, depends, // makedepends, sha512sums, b2sums, options, ...) with its values in order of appearance. Fields map[string][]string // Packages holds the per-pkgname override sections keyed by package name. Packages map[string]map[string][]string } // Version renders the full package version: [epoch:]pkgver-pkgrel. func (s *SRCINFO) Version() string { v := s.PkgVer if s.PkgRel != "" { v += "-" + s.PkgRel } if s.Epoch != "" && s.Epoch != "0" { v = s.Epoch + ":" + v } return v } // Source is one entry of a source array, following the makepkg grammar // // [::][#tag=|#commit=|#branch=|#revision=][?signed] // // where is [vcs+]scheme://... for remote sources or a bare file name for a file kept in // the packaging repo. The ?signed query may appear before or after the fragment. type Source struct { // Raw is the entry exactly as written. Raw string // Arch is empty for `source =` and the architecture for `source_ =`. Arch string // LocalName is the name makepkg saves the source under in $srcdir: the explicit // "name::" prefix, the repository name for VCS sources, or the URL/file base name. LocalName string // Local is true for files that live in the packaging repo (no URL scheme). Local bool // URL is the download URL without the vcs+ prefix, fragment and ?signed query; empty when // Local. URL string // VCS is git, hg, svn, bzr or fossil for VCS sources, else "". VCS string // RefType/Ref hold the fragment selector: tag, commit, branch or revision. RefType string Ref string // Signed is set when the entry asks makepkg to verify a signature (?signed). Signed bool } // Remote returns the fetch spec makepkg would use for a remote source: [vcs+]url[#type=ref]. // It is the right thing to hand to repourl: the vcs prefix marks git URLs that lack a .git // suffix and the fragment carries the pinned ref. Empty for local files. func (s Source) Remote() string { if s.Local { return "" } u := s.URL if s.VCS != "" { u = s.VCS + "+" + u } if s.RefType != "" { u += "#" + s.RefType + "=" + s.Ref } return u } var ( vcsPrefixRe = regexp.MustCompile(`^(git|hg|svn|bzr|fossil)\+`) srcinfoKV = regexp.MustCompile(`^([A-Za-z0-9_]+)\s*=\s*(.*)$`) ) // ParseSource parses one source entry. func ParseSource(raw string) Source { s := Source{Raw: raw} rest := strings.TrimSpace(raw) if i := strings.Index(rest, "::"); i >= 0 && !strings.ContainsAny(rest[:i], "/:") { s.LocalName = rest[:i] rest = rest[i+2:] } if !strings.Contains(rest, "://") { s.Local = true if s.LocalName == "" { s.LocalName = rest } s.URL = "" return s } if m := vcsPrefixRe.FindStringSubmatch(rest); m != nil { s.VCS = m[1] rest = rest[len(m[0]):] } frag := "" if i := strings.Index(rest, "#"); i >= 0 { frag = rest[i+1:] rest = rest[:i] } // "?signed" may sit on the URL (before the fragment) or trail the fragment. if strings.HasSuffix(frag, "?signed") { s.Signed = true frag = strings.TrimSuffix(frag, "?signed") } if strings.HasSuffix(rest, "?signed") { s.Signed = true rest = strings.TrimSuffix(rest, "?signed") } if k, v, ok := strings.Cut(frag, "="); ok { switch k { case "tag", "commit", "branch", "revision": s.RefType, s.Ref = k, v } } s.URL = rest if s.LocalName == "" { base := path.Base(strings.TrimRight(rest, "/")) if i := strings.Index(base, "?"); i >= 0 { base = base[:i] } if s.VCS != "" { base = strings.TrimSuffix(base, ".git") } s.LocalName = base } return s } // ParseSRCINFO parses a .SRCINFO file. func ParseSRCINFO(b []byte) (*SRCINFO, error) { si := &SRCINFO{Fields: map[string][]string{}, Packages: map[string]map[string][]string{}} var cur map[string][]string // nil while in the pkgbase section sc := bufio.NewScanner(bytes.NewReader(b)) sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024) lineNo := 0 for sc.Scan() { lineNo++ line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } m := srcinfoKV.FindStringSubmatch(line) if m == nil { return nil, fmt.Errorf("srcinfo: line %d: cannot parse %q", lineNo, line) } key, val := m[1], strings.TrimSpace(m[2]) switch key { case "pkgbase": if si.PkgBase != "" { return nil, fmt.Errorf("srcinfo: line %d: second pkgbase", lineNo) } si.PkgBase = val cur = nil continue case "pkgname": si.PkgNames = append(si.PkgNames, val) cur = map[string][]string{} si.Packages[val] = cur continue } if cur != nil { cur[key] = append(cur[key], val) continue } switch { case key == "pkgver": si.PkgVer = val case key == "pkgrel": si.PkgRel = val case key == "epoch": si.Epoch = val case key == "url": si.URL = val case key == "validpgpkeys": si.ValidPGPKeys = append(si.ValidPGPKeys, val) case key == "source" || strings.HasPrefix(key, "source_"): s := ParseSource(val) s.Arch = strings.TrimPrefix(strings.TrimPrefix(key, "source"), "_") si.Sources = append(si.Sources, s) default: si.Fields[key] = append(si.Fields[key], val) } } if err := sc.Err(); err != nil { return nil, fmt.Errorf("srcinfo: %w", err) } if si.PkgBase == "" { return nil, errors.New("srcinfo: no pkgbase") } return si, nil } var ( plusWordRe = regexp.MustCompile(`([a-zA-Z0-9]+)\+([a-zA-Z]+)`) plusRe = regexp.MustCompile(`\+`) specialRe = regexp.MustCompile(`[^a-zA-Z0-9_.-]`) dashRunRe = regexp.MustCompile(`[_-]{2,}`) reservedTree = "tree" reservedTrees = "unix-tree" ) // ProjectPath maps a pkgbase onto its GitLab project path under archlinux/packaging/packages, // mirroring devtools' gitlab_project_name_to_path(): a '+' between an alphanumeric and an // alphabetic run becomes '-', any other '+' becomes "plus", other special characters become // '-', runs of '_'/'-' collapse to one '-', and the GitLab-reserved name "tree" becomes // "unix-tree". The project's display name keeps the real pkgbase. func ProjectPath(pkgbase string) string { p := plusWordRe.ReplaceAllString(pkgbase, "$1-$2") p = plusRe.ReplaceAllString(p, "plus") p = specialRe.ReplaceAllString(p, "-") p = dashRunRe.ReplaceAllString(p, "-") if p == reservedTree { p = reservedTrees } return p }