1// Copyright 2015 CoreOS, Inc. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package unit 16 17import ( 18 "bufio" 19 "bytes" 20 "errors" 21 "fmt" 22 "io" 23 "strings" 24 "unicode" 25) 26 27const ( 28 // SYSTEMD_LINE_MAX mimics the maximum line length that systemd can use. 29 // On typical systemd platforms (i.e. modern Linux), this will most 30 // commonly be 2048, so let's use that as a sanity check. 31 // Technically, we should probably pull this at runtime: 32 // SYSTEMD_LINE_MAX = int(C.sysconf(C.__SC_LINE_MAX)) 33 // but this would introduce an (unfortunate) dependency on cgo 34 SYSTEMD_LINE_MAX = 2048 35 36 // SYSTEMD_NEWLINE defines characters that systemd considers indicators 37 // for a newline. 38 SYSTEMD_NEWLINE = "\r\n" 39) 40 41var ( 42 // ErrLineTooLong gets returned when a line is too long for systemd to handle. 43 ErrLineTooLong = fmt.Errorf("line too long (max %d bytes)", SYSTEMD_LINE_MAX) 44) 45 46// Deserialize parses a systemd unit file into a list of UnitOption objects. 47func Deserialize(f io.Reader) (opts []*UnitOption, err error) { 48 lexer, optchan, errchan := newLexer(f) 49 go lexer.lex() 50 51 for opt := range optchan { 52 opts = append(opts, &(*opt)) 53 } 54 55 err = <-errchan 56 return opts, err 57} 58 59func newLexer(f io.Reader) (*lexer, <-chan *UnitOption, <-chan error) { 60 optchan := make(chan *UnitOption) 61 errchan := make(chan error, 1) 62 buf := bufio.NewReader(f) 63 64 return &lexer{buf, optchan, errchan, ""}, optchan, errchan 65} 66 67type lexer struct { 68 buf *bufio.Reader 69 optchan chan *UnitOption 70 errchan chan error 71 section string 72} 73 74func (l *lexer) lex() { 75 defer func() { 76 close(l.optchan) 77 close(l.errchan) 78 }() 79 next := l.lexNextSection 80 for next != nil { 81 if l.buf.Buffered() >= SYSTEMD_LINE_MAX { 82 // systemd truncates lines longer than LINE_MAX 83 // https://bugs.freedesktop.org/show_bug.cgi?id=85308 84 // Rather than allowing this to pass silently, let's 85 // explicitly gate people from encountering this 86 line, err := l.buf.Peek(SYSTEMD_LINE_MAX) 87 if err != nil { 88 l.errchan <- err 89 return 90 } 91 if !bytes.ContainsAny(line, SYSTEMD_NEWLINE) { 92 l.errchan <- ErrLineTooLong 93 return 94 } 95 } 96 97 var err error 98 next, err = next() 99 if err != nil { 100 l.errchan <- err 101 return 102 } 103 } 104} 105 106type lexStep func() (lexStep, error) 107 108func (l *lexer) lexSectionName() (lexStep, error) { 109 sec, err := l.buf.ReadBytes(']') 110 if err != nil { 111 return nil, errors.New("unable to find end of section") 112 } 113 114 return l.lexSectionSuffixFunc(string(sec[:len(sec)-1])), nil 115} 116 117func (l *lexer) lexSectionSuffixFunc(section string) lexStep { 118 return func() (lexStep, error) { 119 garbage, _, err := l.toEOL() 120 if err != nil { 121 return nil, err 122 } 123 124 garbage = bytes.TrimSpace(garbage) 125 if len(garbage) > 0 { 126 return nil, fmt.Errorf("found garbage after section name %s: %v", l.section, garbage) 127 } 128 129 return l.lexNextSectionOrOptionFunc(section), nil 130 } 131} 132 133func (l *lexer) ignoreLineFunc(next lexStep) lexStep { 134 return func() (lexStep, error) { 135 for { 136 line, _, err := l.toEOL() 137 if err != nil { 138 return nil, err 139 } 140 141 line = bytes.TrimSuffix(line, []byte{' '}) 142 143 // lack of continuation means this line has been exhausted 144 if !bytes.HasSuffix(line, []byte{'\\'}) { 145 break 146 } 147 } 148 149 // reached end of buffer, safe to exit 150 return next, nil 151 } 152} 153 154func (l *lexer) lexNextSection() (lexStep, error) { 155 r, _, err := l.buf.ReadRune() 156 if err != nil { 157 if err == io.EOF { 158 err = nil 159 } 160 return nil, err 161 } 162 163 if r == '[' { 164 return l.lexSectionName, nil 165 } else if isComment(r) { 166 return l.ignoreLineFunc(l.lexNextSection), nil 167 } 168 169 return l.lexNextSection, nil 170} 171 172func (l *lexer) lexNextSectionOrOptionFunc(section string) lexStep { 173 return func() (lexStep, error) { 174 r, _, err := l.buf.ReadRune() 175 if err != nil { 176 if err == io.EOF { 177 err = nil 178 } 179 return nil, err 180 } 181 182 if unicode.IsSpace(r) { 183 return l.lexNextSectionOrOptionFunc(section), nil 184 } else if r == '[' { 185 return l.lexSectionName, nil 186 } else if isComment(r) { 187 return l.ignoreLineFunc(l.lexNextSectionOrOptionFunc(section)), nil 188 } 189 190 l.buf.UnreadRune() 191 return l.lexOptionNameFunc(section), nil 192 } 193} 194 195func (l *lexer) lexOptionNameFunc(section string) lexStep { 196 return func() (lexStep, error) { 197 var partial bytes.Buffer 198 for { 199 r, _, err := l.buf.ReadRune() 200 if err != nil { 201 return nil, err 202 } 203 204 if r == '\n' || r == '\r' { 205 return nil, errors.New("unexpected newline encountered while parsing option name") 206 } 207 208 if r == '=' { 209 break 210 } 211 212 partial.WriteRune(r) 213 } 214 215 name := strings.TrimSpace(partial.String()) 216 return l.lexOptionValueFunc(section, name, bytes.Buffer{}), nil 217 } 218} 219 220func (l *lexer) lexOptionValueFunc(section, name string, partial bytes.Buffer) lexStep { 221 return func() (lexStep, error) { 222 for { 223 line, eof, err := l.toEOL() 224 if err != nil { 225 return nil, err 226 } 227 228 if len(bytes.TrimSpace(line)) == 0 { 229 break 230 } 231 232 partial.Write(line) 233 234 // lack of continuation means this value has been exhausted 235 idx := bytes.LastIndex(line, []byte{'\\'}) 236 if idx == -1 || idx != (len(line)-1) { 237 break 238 } 239 240 if !eof { 241 partial.WriteRune('\n') 242 } 243 244 return l.lexOptionValueFunc(section, name, partial), nil 245 } 246 247 val := partial.String() 248 if strings.HasSuffix(val, "\n") { 249 // A newline was added to the end, so the file didn't end with a backslash. 250 // => Keep the newline 251 val = strings.TrimSpace(val) + "\n" 252 } else { 253 val = strings.TrimSpace(val) 254 } 255 l.optchan <- &UnitOption{Section: section, Name: name, Value: val} 256 257 return l.lexNextSectionOrOptionFunc(section), nil 258 } 259} 260 261// toEOL reads until the end-of-line or end-of-file. 262// Returns (data, EOFfound, error) 263func (l *lexer) toEOL() ([]byte, bool, error) { 264 line, err := l.buf.ReadBytes('\n') 265 // ignore EOF here since it's roughly equivalent to EOL 266 if err != nil && err != io.EOF { 267 return nil, false, err 268 } 269 270 line = bytes.TrimSuffix(line, []byte{'\r'}) 271 line = bytes.TrimSuffix(line, []byte{'\n'}) 272 273 return line, err == io.EOF, nil 274} 275 276func isComment(r rune) bool { 277 return r == '#' || r == ';' 278} 279