format.go 5.13 KB
Newer Older
1 2 3 4 5 6 7
package deb

import (
	"bufio"
	"errors"
	"io"
	"strings"
8
	"unicode"
9 10 11 12 13
)

// Stanza or paragraph of Debian control file
type Stanza map[string]string

14 15 16
// MaxFieldSize is maximum stanza field size in bytes
const MaxFieldSize = 2 * 1024 * 1024

17
// Canonical order of fields in stanza
18 19 20 21 22 23 24 25 26 27
// 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",
28 29
		"NotAutomatic",
		"ButAutomaticUpgrades",
30 31 32 33 34 35 36 37
		"Architectures",
		"Architecture",
		"Components",
		"Component",
		"Description",
		"MD5Sum",
		"SHA1",
		"SHA256",
38
		"SHA512",
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
	}

	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",
68
		"SHA512",
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
		"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",
	}
)
92 93 94 95 96 97 98 99 100 101

// 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
}

102 103 104 105 106 107 108 109 110 111 112 113
func isMultilineField(field string, isRelease bool) bool {
	switch field {
	case "Description":
		return true
	case "Files":
		return true
	case "Changes":
		return true
	case "Checksums-Sha1":
		return true
	case "Checksums-Sha256":
		return true
114 115
	case "Checksums-Sha512":
		return true
116 117 118 119 120 121 122 123
	case "Package-List":
		return true
	case "MD5Sum":
		return isRelease
	case "SHA1":
		return isRelease
	case "SHA256":
		return isRelease
124 125
	case "SHA512":
		return isRelease
126 127 128
	}
	return false
}
129

130 131 132
// Write single field from Stanza to writer
func writeField(w *bufio.Writer, field, value string, isRelease bool) (err error) {
	if !isMultilineField(field, isRelease) {
133 134 135 136 137
		_, err = w.WriteString(field + ": " + value + "\n")
	} else {
		if !strings.HasSuffix(value, "\n") {
			value = value + "\n"
		}
138 139 140 141

		if field != "Description" {
			value = "\n" + value
		}
142 143 144 145 146 147 148
		_, err = w.WriteString(field + ":" + value)
	}

	return
}

// WriteTo saves stanza back to stream, modifying itself on the fly
149 150 151 152 153 154 155 156 157 158
func (s Stanza) WriteTo(w *bufio.Writer, isSource, isRelease bool) error {
	canonicalOrder := canonicalOrderBinary
	if isSource {
		canonicalOrder = canonicalOrderSource
	}
	if isRelease {
		canonicalOrder = canonicalOrderRelease
	}

	for _, field := range canonicalOrder {
159 160 161
		value, ok := s[field]
		if ok {
			delete(s, field)
162
			err := writeField(w, field, value, isRelease)
163 164 165 166 167 168 169
			if err != nil {
				return err
			}
		}
	}

	for field, value := range s {
170
		err := writeField(w, field, value, isRelease)
171 172 173 174 175 176 177 178 179 180 181 182 183
		if err != nil {
			return err
		}
	}

	return nil
}

// Parsing errors
var (
	ErrMalformedStanza = errors.New("malformed stanza syntax")
)

184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
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)
}

213 214 215 216 217 218 219
// ControlFileReader implements reading of control files stanza by stanza
type ControlFileReader struct {
	scanner *bufio.Scanner
}

// NewControlFileReader creates ControlFileReader, it wraps with buffering
func NewControlFileReader(r io.Reader) *ControlFileReader {
220 221 222 223
	scnr := bufio.NewScanner(bufio.NewReaderSize(r, 32768))
	scnr.Buffer(nil, MaxFieldSize)

	return &ControlFileReader{scanner: scnr}
224 225 226
}

// ReadStanza reeads one stanza from control file
227
func (c *ControlFileReader) ReadStanza(isRelease bool) (Stanza, error) {
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
	stanza := make(Stanza, 32)
	lastField := ""
	lastFieldMultiline := false

	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' {
			if lastFieldMultiline {
				stanza[lastField] += line + "\n"
			} else {
247
				stanza[lastField] += " " + strings.TrimSpace(line)
248 249 250 251 252 253
			}
		} else {
			parts := strings.SplitN(line, ":", 2)
			if len(parts) != 2 {
				return nil, ErrMalformedStanza
			}
254
			lastField = canonicalCase(parts[0])
255
			lastFieldMultiline = isMultilineField(lastField, isRelease)
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
			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
}