You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
277 lines
6.3 KiB
277 lines
6.3 KiB
// 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
|
|
|
|
// characters that systemd considers indicate a newline
|
|
SYSTEMD_NEWLINE = "\r\n"
|
|
)
|
|
|
|
var (
|
|
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() {
|
|
var err error
|
|
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.IndexAny(line, SYSTEMD_NEWLINE) == -1 {
|
|
l.errchan <- ErrLineTooLong
|
|
return
|
|
}
|
|
}
|
|
|
|
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 == ';'
|
|
}
|