blob: c0c06bdfc224a28c0500c3c5cebce2d81579c5fd [file] [log] [blame]
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package unit
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strings"
"unicode"
)
const (
// SYSTEMD_LINE_MAX mimics the maximum line length that systemd can use.
// On typical systemd platforms (i.e. modern Linux), this will most
// commonly be 2048, so let's use that as a sanity check.
// Technically, we should probably pull this at runtime:
// SYSTEMD_LINE_MAX = int(C.sysconf(C.__SC_LINE_MAX))
// but this would introduce an (unfortunate) dependency on cgo
SYSTEMD_LINE_MAX = 2048
// SYSTEMD_NEWLINE defines characters that systemd considers indicators
// for a newline.
SYSTEMD_NEWLINE = "\r\n"
)
var (
// ErrLineTooLong gets returned when a line is too long for systemd to handle.
ErrLineTooLong = fmt.Errorf("line too long (max %d bytes)", SYSTEMD_LINE_MAX)
)
// Deserialize parses a systemd unit file into a list of UnitOption objects.
func Deserialize(f io.Reader) (opts []*UnitOption, err error) {
lexer, optchan, errchan := newLexer(f)
go lexer.lex()
for opt := range optchan {
opts = append(opts, &(*opt))
}
err = <-errchan
return opts, err
}
func newLexer(f io.Reader) (*lexer, <-chan *UnitOption, <-chan error) {
optchan := make(chan *UnitOption)
errchan := make(chan error, 1)
buf := bufio.NewReader(f)
return &lexer{buf, optchan, errchan, ""}, optchan, errchan
}
type lexer struct {
buf *bufio.Reader
optchan chan *UnitOption
errchan chan error
section string
}
func (l *lexer) lex() {
defer func() {
close(l.optchan)
close(l.errchan)
}()
next := l.lexNextSection
for next != nil {
if l.buf.Buffered() >= SYSTEMD_LINE_MAX {
// systemd truncates lines longer than LINE_MAX
// https://bugs.freedesktop.org/show_bug.cgi?id=85308
// Rather than allowing this to pass silently, let's
// explicitly gate people from encountering this
line, err := l.buf.Peek(SYSTEMD_LINE_MAX)
if err != nil {
l.errchan <- err
return
}
if !bytes.ContainsAny(line, SYSTEMD_NEWLINE) {
l.errchan <- ErrLineTooLong
return
}
}
var err error
next, err = next()
if err != nil {
l.errchan <- err
return
}
}
}
type lexStep func() (lexStep, error)
func (l *lexer) lexSectionName() (lexStep, error) {
sec, err := l.buf.ReadBytes(']')
if err != nil {
return nil, errors.New("unable to find end of section")
}
return l.lexSectionSuffixFunc(string(sec[:len(sec)-1])), nil
}
func (l *lexer) lexSectionSuffixFunc(section string) lexStep {
return func() (lexStep, error) {
garbage, _, err := l.toEOL()
if err != nil {
return nil, err
}
garbage = bytes.TrimSpace(garbage)
if len(garbage) > 0 {
return nil, fmt.Errorf("found garbage after section name %s: %v", l.section, garbage)
}
return l.lexNextSectionOrOptionFunc(section), nil
}
}
func (l *lexer) ignoreLineFunc(next lexStep) lexStep {
return func() (lexStep, error) {
for {
line, _, err := l.toEOL()
if err != nil {
return nil, err
}
line = bytes.TrimSuffix(line, []byte{' '})
// lack of continuation means this line has been exhausted
if !bytes.HasSuffix(line, []byte{'\\'}) {
break
}
}
// reached end of buffer, safe to exit
return next, nil
}
}
func (l *lexer) lexNextSection() (lexStep, error) {
r, _, err := l.buf.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
return nil, err
}
if r == '[' {
return l.lexSectionName, nil
} else if isComment(r) {
return l.ignoreLineFunc(l.lexNextSection), nil
}
return l.lexNextSection, nil
}
func (l *lexer) lexNextSectionOrOptionFunc(section string) lexStep {
return func() (lexStep, error) {
r, _, err := l.buf.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
return nil, err
}
if unicode.IsSpace(r) {
return l.lexNextSectionOrOptionFunc(section), nil
} else if r == '[' {
return l.lexSectionName, nil
} else if isComment(r) {
return l.ignoreLineFunc(l.lexNextSectionOrOptionFunc(section)), nil
}
l.buf.UnreadRune()
return l.lexOptionNameFunc(section), nil
}
}
func (l *lexer) lexOptionNameFunc(section string) lexStep {
return func() (lexStep, error) {
var partial bytes.Buffer
for {
r, _, err := l.buf.ReadRune()
if err != nil {
return nil, err
}
if r == '\n' || r == '\r' {
return nil, errors.New("unexpected newline encountered while parsing option name")
}
if r == '=' {
break
}
partial.WriteRune(r)
}
name := strings.TrimSpace(partial.String())
return l.lexOptionValueFunc(section, name, bytes.Buffer{}), nil
}
}
func (l *lexer) lexOptionValueFunc(section, name string, partial bytes.Buffer) lexStep {
return func() (lexStep, error) {
for {
line, eof, err := l.toEOL()
if err != nil {
return nil, err
}
if len(bytes.TrimSpace(line)) == 0 {
break
}
partial.Write(line)
// lack of continuation means this value has been exhausted
idx := bytes.LastIndex(line, []byte{'\\'})
if idx == -1 || idx != (len(line)-1) {
break
}
if !eof {
partial.WriteRune('\n')
}
return l.lexOptionValueFunc(section, name, partial), nil
}
val := partial.String()
if strings.HasSuffix(val, "\n") {
// A newline was added to the end, so the file didn't end with a backslash.
// => Keep the newline
val = strings.TrimSpace(val) + "\n"
} else {
val = strings.TrimSpace(val)
}
l.optchan <- &UnitOption{Section: section, Name: name, Value: val}
return l.lexNextSectionOrOptionFunc(section), nil
}
}
// toEOL reads until the end-of-line or end-of-file.
// Returns (data, EOFfound, error)
func (l *lexer) toEOL() ([]byte, bool, error) {
line, err := l.buf.ReadBytes('\n')
// ignore EOF here since it's roughly equivalent to EOL
if err != nil && err != io.EOF {
return nil, false, err
}
line = bytes.TrimSuffix(line, []byte{'\r'})
line = bytes.TrimSuffix(line, []byte{'\n'})
return line, err == io.EOF, nil
}
func isComment(r rune) bool {
return r == '#' || r == ';'
}