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