mirror of https://github.com/aptly-dev/aptly
308 lines
5.8 KiB
Go
308 lines
5.8 KiB
Go
package deb
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// Stanza or paragraph of Debian control file
|
|
type Stanza map[string]string
|
|
|
|
// MaxFieldSize is maximum stanza field size in bytes
|
|
const MaxFieldSize = 2 * 1024 * 1024
|
|
|
|
// Canonical order of fields in stanza
|
|
// Taken from: http://bazaar.launchpad.net/~ubuntu-branches/ubuntu/vivid/apt/vivid/view/head:/apt-pkg/tagfile.cc#L504
|
|
var (
|
|
canonicalOrderRelease = []string{
|
|
"Origin",
|
|
"Label",
|
|
"Archive",
|
|
"Suite",
|
|
"Version",
|
|
"Codename",
|
|
"Date",
|
|
"NotAutomatic",
|
|
"ButAutomaticUpgrades",
|
|
"Architectures",
|
|
"Architecture",
|
|
"Components",
|
|
"Component",
|
|
"Description",
|
|
"MD5Sum",
|
|
"SHA1",
|
|
"SHA256",
|
|
"SHA512",
|
|
}
|
|
|
|
canonicalOrderBinary = []string{
|
|
"Package",
|
|
"Essential",
|
|
"Status",
|
|
"Priority",
|
|
"Section",
|
|
"Installed-Size",
|
|
"Maintainer",
|
|
"Original-Maintainer",
|
|
"Architecture",
|
|
"Source",
|
|
"Version",
|
|
"Replaces",
|
|
"Provides",
|
|
"Depends",
|
|
"Pre-Depends",
|
|
"Recommends",
|
|
"Suggests",
|
|
"Conflicts",
|
|
"Breaks",
|
|
"Conffiles",
|
|
"Filename",
|
|
"Size",
|
|
"MD5Sum",
|
|
"MD5sum",
|
|
"SHA1",
|
|
"SHA256",
|
|
"SHA512",
|
|
"Description",
|
|
}
|
|
|
|
canonicalOrderSource = []string{
|
|
"Package",
|
|
"Source",
|
|
"Binary",
|
|
"Version",
|
|
"Priority",
|
|
"Section",
|
|
"Maintainer",
|
|
"Original-Maintainer",
|
|
"Build-Depends",
|
|
"Build-Depends-Indep",
|
|
"Build-Conflicts",
|
|
"Build-Conflicts-Indep",
|
|
"Architecture",
|
|
"Standards-Version",
|
|
"Format",
|
|
"Directory",
|
|
"Files",
|
|
}
|
|
canonicalOrderInstaller = []string{
|
|
"",
|
|
}
|
|
)
|
|
|
|
// Copy returns copy of Stanza
|
|
func (s Stanza) Copy() (result Stanza) {
|
|
result = make(Stanza, len(s))
|
|
for k, v := range s {
|
|
result[k] = v
|
|
}
|
|
return
|
|
}
|
|
|
|
func isMultilineField(field string, isRelease bool) bool {
|
|
switch field {
|
|
// file without a section
|
|
case "":
|
|
return true
|
|
case "Description":
|
|
return true
|
|
case "Files":
|
|
return true
|
|
case "Changes":
|
|
return true
|
|
case "Checksums-Sha1":
|
|
return true
|
|
case "Checksums-Sha256":
|
|
return true
|
|
case "Checksums-Sha512":
|
|
return true
|
|
case "Package-List":
|
|
return true
|
|
case "MD5Sum":
|
|
return isRelease
|
|
case "SHA1":
|
|
return isRelease
|
|
case "SHA256":
|
|
return isRelease
|
|
case "SHA512":
|
|
return isRelease
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Write single field from Stanza to writer.
|
|
//
|
|
// nolint: interfacer
|
|
func writeField(w *bufio.Writer, field, value string, isRelease bool) (err error) {
|
|
if !isMultilineField(field, isRelease) {
|
|
_, err = w.WriteString(field + ": " + value + "\n")
|
|
} else {
|
|
if field != "" && !strings.HasSuffix(value, "\n") {
|
|
value = value + "\n"
|
|
}
|
|
|
|
if field != "Description" && field != "" {
|
|
value = "\n" + value
|
|
}
|
|
|
|
if field != "" {
|
|
_, err = w.WriteString(field + ":" + value)
|
|
} else {
|
|
_, err = w.WriteString(value)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// WriteTo saves stanza back to stream, modifying itself on the fly
|
|
func (s Stanza) WriteTo(w *bufio.Writer, isSource, isRelease, isInstaller bool) error {
|
|
canonicalOrder := canonicalOrderBinary
|
|
if isSource {
|
|
canonicalOrder = canonicalOrderSource
|
|
}
|
|
if isRelease {
|
|
canonicalOrder = canonicalOrderRelease
|
|
}
|
|
if isInstaller {
|
|
canonicalOrder = canonicalOrderInstaller
|
|
}
|
|
|
|
for _, field := range canonicalOrder {
|
|
value, ok := s[field]
|
|
if ok {
|
|
delete(s, field)
|
|
err := writeField(w, field, value, isRelease)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// no extra fields in installer
|
|
if !isInstaller {
|
|
// Print extra fields in deterministic order (alphabetical)
|
|
keys := make([]string, len(s))
|
|
i := 0
|
|
for field := range s {
|
|
keys[i] = field
|
|
i++
|
|
}
|
|
sort.Strings(keys)
|
|
for _, field := range keys {
|
|
err := writeField(w, field, s[field], isRelease)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Parsing errors
|
|
var (
|
|
ErrMalformedStanza = errors.New("malformed stanza syntax")
|
|
)
|
|
|
|
func canonicalCase(field string) string {
|
|
upper := strings.ToUpper(field)
|
|
switch upper {
|
|
case "SHA1", "SHA256", "SHA512":
|
|
return upper
|
|
case "MD5SUM":
|
|
return "MD5Sum"
|
|
case "NOTAUTOMATIC":
|
|
return "NotAutomatic"
|
|
case "BUTAUTOMATICUPGRADES":
|
|
return "ButAutomaticUpgrades"
|
|
}
|
|
|
|
startOfWord := true
|
|
|
|
return strings.Map(func(r rune) rune {
|
|
if startOfWord {
|
|
startOfWord = false
|
|
return unicode.ToUpper(r)
|
|
}
|
|
|
|
if r == '-' {
|
|
startOfWord = true
|
|
}
|
|
|
|
return unicode.ToLower(r)
|
|
}, field)
|
|
}
|
|
|
|
// ControlFileReader implements reading of control files stanza by stanza
|
|
type ControlFileReader struct {
|
|
scanner *bufio.Scanner
|
|
isRelease bool
|
|
isInstaller bool
|
|
}
|
|
|
|
// NewControlFileReader creates ControlFileReader, it wraps with buffering
|
|
func NewControlFileReader(r io.Reader, isRelease, isInstaller bool) *ControlFileReader {
|
|
scnr := bufio.NewScanner(bufio.NewReaderSize(r, 32768))
|
|
scnr.Buffer(nil, MaxFieldSize)
|
|
|
|
return &ControlFileReader{
|
|
scanner: scnr,
|
|
isRelease: isRelease,
|
|
isInstaller: isInstaller,
|
|
}
|
|
}
|
|
|
|
// ReadStanza reeads one stanza from control file
|
|
func (c *ControlFileReader) ReadStanza() (Stanza, error) {
|
|
stanza := make(Stanza, 32)
|
|
lastField := ""
|
|
lastFieldMultiline := c.isInstaller
|
|
|
|
for c.scanner.Scan() {
|
|
line := c.scanner.Text()
|
|
|
|
// Current stanza ends with empty line
|
|
if line == "" {
|
|
if len(stanza) > 0 {
|
|
return stanza, nil
|
|
}
|
|
continue
|
|
}
|
|
|
|
if line[0] == ' ' || line[0] == '\t' || c.isInstaller {
|
|
if lastFieldMultiline {
|
|
stanza[lastField] += line + "\n"
|
|
} else {
|
|
stanza[lastField] += " " + strings.TrimSpace(line)
|
|
}
|
|
} else {
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) != 2 {
|
|
return nil, ErrMalformedStanza
|
|
}
|
|
lastField = canonicalCase(parts[0])
|
|
lastFieldMultiline = isMultilineField(lastField, c.isRelease)
|
|
if lastFieldMultiline {
|
|
stanza[lastField] = parts[1]
|
|
if parts[1] != "" {
|
|
stanza[lastField] += "\n"
|
|
}
|
|
} else {
|
|
stanza[lastField] = strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
}
|
|
if err := c.scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(stanza) > 0 {
|
|
return stanza, nil
|
|
}
|
|
return nil, nil
|
|
}
|