1package deb
2
3import (
4	"bufio"
5	"errors"
6	"io"
7	"sort"
8	"strings"
9	"unicode"
10)
11
12// Stanza or paragraph of Debian control file
13type Stanza map[string]string
14
15// MaxFieldSize is maximum stanza field size in bytes
16const MaxFieldSize = 2 * 1024 * 1024
17
18// Canonical order of fields in stanza
19// Taken from: http://bazaar.launchpad.net/~ubuntu-branches/ubuntu/vivid/apt/vivid/view/head:/apt-pkg/tagfile.cc#L504
20var (
21	canonicalOrderRelease = []string{
22		"Origin",
23		"Label",
24		"Archive",
25		"Suite",
26		"Version",
27		"Codename",
28		"Date",
29		"NotAutomatic",
30		"ButAutomaticUpgrades",
31		"Architectures",
32		"Architecture",
33		"Components",
34		"Component",
35		"Description",
36		"MD5Sum",
37		"SHA1",
38		"SHA256",
39		"SHA512",
40	}
41
42	canonicalOrderBinary = []string{
43		"Package",
44		"Essential",
45		"Status",
46		"Priority",
47		"Section",
48		"Installed-Size",
49		"Maintainer",
50		"Original-Maintainer",
51		"Architecture",
52		"Source",
53		"Version",
54		"Replaces",
55		"Provides",
56		"Depends",
57		"Pre-Depends",
58		"Recommends",
59		"Suggests",
60		"Conflicts",
61		"Breaks",
62		"Conffiles",
63		"Filename",
64		"Size",
65		"MD5Sum",
66		"MD5sum",
67		"SHA1",
68		"SHA256",
69		"SHA512",
70		"Description",
71	}
72
73	canonicalOrderSource = []string{
74		"Package",
75		"Source",
76		"Binary",
77		"Version",
78		"Priority",
79		"Section",
80		"Maintainer",
81		"Original-Maintainer",
82		"Build-Depends",
83		"Build-Depends-Indep",
84		"Build-Conflicts",
85		"Build-Conflicts-Indep",
86		"Architecture",
87		"Standards-Version",
88		"Format",
89		"Directory",
90		"Files",
91	}
92	canonicalOrderInstaller = []string{
93		"",
94	}
95)
96
97// Copy returns copy of Stanza
98func (s Stanza) Copy() (result Stanza) {
99	result = make(Stanza, len(s))
100	for k, v := range s {
101		result[k] = v
102	}
103	return
104}
105
106func isMultilineField(field string, isRelease bool) bool {
107	switch field {
108	// file without a section
109	case "":
110		return true
111	case "Description":
112		return true
113	case "Files":
114		return true
115	case "Changes":
116		return true
117	case "Checksums-Sha1":
118		return true
119	case "Checksums-Sha256":
120		return true
121	case "Checksums-Sha512":
122		return true
123	case "Package-List":
124		return true
125	case "MD5Sum":
126		return isRelease
127	case "SHA1":
128		return isRelease
129	case "SHA256":
130		return isRelease
131	case "SHA512":
132		return isRelease
133	}
134	return false
135}
136
137// Write single field from Stanza to writer.
138//
139//nolint: interfacer
140func writeField(w *bufio.Writer, field, value string, isRelease bool) (err error) {
141	if !isMultilineField(field, isRelease) {
142		_, err = w.WriteString(field + ": " + value + "\n")
143	} else {
144		if field != "" && !strings.HasSuffix(value, "\n") {
145			value = value + "\n"
146		}
147
148		if field != "Description" && field != "" {
149			value = "\n" + value
150		}
151
152		if field != "" {
153			_, err = w.WriteString(field + ":" + value)
154		} else {
155			_, err = w.WriteString(value)
156		}
157	}
158
159	return
160}
161
162// WriteTo saves stanza back to stream, modifying itself on the fly
163func (s Stanza) WriteTo(w *bufio.Writer, isSource, isRelease, isInstaller bool) error {
164	canonicalOrder := canonicalOrderBinary
165	if isSource {
166		canonicalOrder = canonicalOrderSource
167	}
168	if isRelease {
169		canonicalOrder = canonicalOrderRelease
170	}
171	if isInstaller {
172		canonicalOrder = canonicalOrderInstaller
173	}
174
175	for _, field := range canonicalOrder {
176		value, ok := s[field]
177		if ok {
178			delete(s, field)
179			err := writeField(w, field, value, isRelease)
180			if err != nil {
181				return err
182			}
183		}
184	}
185
186	// no extra fields in installer
187	if !isInstaller {
188		// Print extra fields in deterministic order (alphabetical)
189		keys := make([]string, len(s))
190		i := 0
191		for field := range s {
192			keys[i] = field
193			i++
194		}
195		sort.Strings(keys)
196		for _, field := range keys {
197			err := writeField(w, field, s[field], isRelease)
198			if err != nil {
199				return err
200			}
201		}
202	}
203
204	return nil
205}
206
207// Parsing errors
208var (
209	ErrMalformedStanza = errors.New("malformed stanza syntax")
210)
211
212func canonicalCase(field string) string {
213	upper := strings.ToUpper(field)
214	switch upper {
215	case "SHA1", "SHA256", "SHA512":
216		return upper
217	case "MD5SUM":
218		return "MD5Sum"
219	case "NOTAUTOMATIC":
220		return "NotAutomatic"
221	case "BUTAUTOMATICUPGRADES":
222		return "ButAutomaticUpgrades"
223	}
224
225	startOfWord := true
226
227	return strings.Map(func(r rune) rune {
228		if startOfWord {
229			startOfWord = false
230			return unicode.ToUpper(r)
231		}
232
233		if r == '-' {
234			startOfWord = true
235		}
236
237		return unicode.ToLower(r)
238	}, field)
239}
240
241// ControlFileReader implements reading of control files stanza by stanza
242type ControlFileReader struct {
243	scanner     *bufio.Scanner
244	isRelease   bool
245	isInstaller bool
246}
247
248// NewControlFileReader creates ControlFileReader, it wraps with buffering
249func NewControlFileReader(r io.Reader, isRelease, isInstaller bool) *ControlFileReader {
250	scnr := bufio.NewScanner(bufio.NewReaderSize(r, 32768))
251	scnr.Buffer(nil, MaxFieldSize)
252
253	return &ControlFileReader{
254		scanner:     scnr,
255		isRelease:   isRelease,
256		isInstaller: isInstaller,
257	}
258}
259
260// ReadStanza reeads one stanza from control file
261func (c *ControlFileReader) ReadStanza() (Stanza, error) {
262	stanza := make(Stanza, 32)
263	lastField := ""
264	lastFieldMultiline := c.isInstaller
265
266	for c.scanner.Scan() {
267		line := c.scanner.Text()
268
269		// Current stanza ends with empty line
270		if line == "" {
271			if len(stanza) > 0 {
272				return stanza, nil
273			}
274			continue
275		}
276
277		if line[0] == ' ' || line[0] == '\t' || c.isInstaller {
278			if lastFieldMultiline {
279				stanza[lastField] += line + "\n"
280			} else {
281				stanza[lastField] += " " + strings.TrimSpace(line)
282			}
283		} else {
284			parts := strings.SplitN(line, ":", 2)
285			if len(parts) != 2 {
286				return nil, ErrMalformedStanza
287			}
288			lastField = canonicalCase(parts[0])
289			lastFieldMultiline = isMultilineField(lastField, c.isRelease)
290			if lastFieldMultiline {
291				stanza[lastField] = parts[1]
292				if parts[1] != "" {
293					stanza[lastField] += "\n"
294				}
295			} else {
296				stanza[lastField] = strings.TrimSpace(parts[1])
297			}
298		}
299	}
300	if err := c.scanner.Err(); err != nil {
301		return nil, err
302	}
303	if len(stanza) > 0 {
304		return stanza, nil
305	}
306	return nil, nil
307}
308