Skip to content

Commit cb8b647

Browse files
author
Luca Bruno
authored
Merge pull request #330 from mergetb/unit-section-fix
unit: deserialize keeps all semantic data
2 parents 0d26d2e + 5e1c393 commit cb8b647

File tree

5 files changed

+423
-15
lines changed

5 files changed

+423
-15
lines changed

unit/deserialize.go

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,37 +43,99 @@ var (
4343
ErrLineTooLong = fmt.Errorf("line too long (max %d bytes)", SYSTEMD_LINE_MAX)
4444
)
4545

46-
// Deserialize parses a systemd unit file into a list of UnitOption objects.
46+
// DeserializeOptions parses a systemd unit file into a list of UnitOptions
47+
func DeserializeOptions(f io.Reader) (opts []*UnitOption, err error) {
48+
_, options, err := deserializeAll(f)
49+
return options, err
50+
}
51+
52+
// DeserializeSections deserializes into a list of UnitSections.
53+
func DeserializeSections(f io.Reader) ([]*UnitSection, error) {
54+
sections, _, err := deserializeAll(f)
55+
return sections, err
56+
}
57+
58+
// Deserialize parses a systemd unit file into a list of UnitOptions.
59+
// Note: this function is deprecated in favor of DeserializeOptions
60+
// and will be removed at a future date.
4761
func Deserialize(f io.Reader) (opts []*UnitOption, err error) {
48-
lexer, optchan, errchan := newLexer(f)
62+
return DeserializeOptions(f)
63+
}
64+
65+
type lexDataType int
66+
67+
const (
68+
SEC lexDataType = iota
69+
OPT
70+
)
71+
72+
// lexChanData - support either datatype in the lex channel.
73+
// Poor man's union data type.
74+
type lexData struct {
75+
Type lexDataType
76+
Option *UnitOption
77+
Section *UnitSection
78+
}
79+
80+
// deserializeAll deserializes into UnitSections and UnitOptions.
81+
func deserializeAll(f io.Reader) ([]*UnitSection, []*UnitOption, error) {
82+
83+
lexer, lexchan, errchan := newLexer(f)
84+
4985
go lexer.lex()
5086

51-
for opt := range optchan {
52-
opts = append(opts, &(*opt))
87+
sections := []*UnitSection{}
88+
options := []*UnitOption{}
89+
90+
for ld := range lexchan {
91+
switch ld.Type {
92+
case OPT:
93+
if ld.Option != nil {
94+
// add to options
95+
opt := ld.Option
96+
options = append(options, &(*opt))
97+
98+
// sanity check. "should not happen" as SEC is first in code flow.
99+
if len(sections) == 0 {
100+
return nil, nil, fmt.Errorf(
101+
"Unit file misparse: option before section")
102+
}
103+
104+
// add to newest section entries.
105+
s := len(sections) - 1
106+
sections[s].Entries = append(sections[s].Entries,
107+
&UnitEntry{Name: opt.Name, Value: opt.Value})
108+
}
109+
case SEC:
110+
if ld.Section != nil {
111+
sections = append(sections, ld.Section)
112+
}
113+
}
53114
}
54115

55-
err = <-errchan
56-
return opts, err
116+
err := <-errchan
117+
118+
return sections, options, err
57119
}
58120

59-
func newLexer(f io.Reader) (*lexer, <-chan *UnitOption, <-chan error) {
60-
optchan := make(chan *UnitOption)
121+
func newLexer(f io.Reader) (*lexer, <-chan *lexData, <-chan error) {
122+
lexchan := make(chan *lexData)
61123
errchan := make(chan error, 1)
62124
buf := bufio.NewReader(f)
63125

64-
return &lexer{buf, optchan, errchan, ""}, optchan, errchan
126+
return &lexer{buf, lexchan, errchan, ""}, lexchan, errchan
65127
}
66128

67129
type lexer struct {
68130
buf *bufio.Reader
69-
optchan chan *UnitOption
131+
lexchan chan *lexData
70132
errchan chan error
71133
section string
72134
}
73135

74136
func (l *lexer) lex() {
75137
defer func() {
76-
close(l.optchan)
138+
close(l.lexchan)
77139
close(l.errchan)
78140
}()
79141
next := l.lexNextSection
@@ -126,6 +188,12 @@ func (l *lexer) lexSectionSuffixFunc(section string) lexStep {
126188
return nil, fmt.Errorf("found garbage after section name %s: %q", l.section, garbage)
127189
}
128190

191+
l.lexchan <- &lexData{
192+
Type: SEC,
193+
Section: &UnitSection{Section: section, Entries: []*UnitEntry{}},
194+
Option: nil,
195+
}
196+
129197
return l.lexNextSectionOrOptionFunc(section), nil
130198
}
131199
}
@@ -252,7 +320,11 @@ func (l *lexer) lexOptionValueFunc(section, name string, partial bytes.Buffer) l
252320
} else {
253321
val = strings.TrimSpace(val)
254322
}
255-
l.optchan <- &UnitOption{Section: section, Name: name, Value: val}
323+
l.lexchan <- &lexData{
324+
Type: OPT,
325+
Section: nil,
326+
Option: &UnitOption{Section: section, Name: name, Value: val},
327+
}
256328

257329
return l.lexNextSectionOrOptionFunc(section), nil
258330
}

unit/deserialize_test.go

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ Option=value
277277
}
278278

279279
for i, tt := range tests {
280-
output, err := Deserialize(bytes.NewReader(tt.input))
280+
output, err := DeserializeOptions(bytes.NewReader(tt.input))
281281
if err != nil {
282282
t.Errorf("case %d: unexpected error parsing unit: %v", i, err)
283283
continue
@@ -294,6 +294,95 @@ Option=value
294294
}
295295
}
296296

297+
func TestDeserializeSections(t *testing.T) {
298+
tests := []struct {
299+
input []byte
300+
output []*UnitSection
301+
}{
302+
// multiple options underneath a section
303+
{
304+
[]byte(`[Unit]
305+
Description=Foo
306+
Description=Bar
307+
Requires=baz.service
308+
After=baz.service
309+
`),
310+
[]*UnitSection{
311+
&UnitSection{
312+
Section: "Unit",
313+
Entries: []*UnitEntry{
314+
&UnitEntry{"Description", "Foo"},
315+
&UnitEntry{"Description", "Bar"},
316+
&UnitEntry{"Requires", "baz.service"},
317+
&UnitEntry{"After", "baz.service"},
318+
},
319+
},
320+
},
321+
},
322+
{
323+
[]byte(`[Route]
324+
Destination=10.0.0.1/24
325+
Gateway=10.0.10.1
326+
327+
[Route]
328+
Destination=10.0.2.1/24
329+
Gateway=10.0.10.1
330+
`),
331+
[]*UnitSection{
332+
&UnitSection{
333+
Section: "Route",
334+
Entries: []*UnitEntry{
335+
&UnitEntry{"Destination", "10.0.0.1/24"},
336+
&UnitEntry{"Gateway", "10.0.10.1"},
337+
},
338+
},
339+
&UnitSection{
340+
Section: "Route",
341+
Entries: []*UnitEntry{
342+
&UnitEntry{"Destination", "10.0.2.1/24"},
343+
&UnitEntry{"Gateway", "10.0.10.1"},
344+
},
345+
},
346+
},
347+
},
348+
}
349+
350+
assert := func(expect, output []*UnitSection) error {
351+
if len(expect) != len(output) {
352+
return fmt.Errorf("expected %d sections, got %d", len(expect), len(output))
353+
}
354+
355+
for i := range expect {
356+
if !reflect.DeepEqual(expect[i], output[i]) {
357+
return fmt.Errorf("item %d: expected %v, got %v", i, expect[i], output[i])
358+
}
359+
}
360+
361+
return nil
362+
}
363+
364+
for i, tt := range tests {
365+
output, err := DeserializeSections(bytes.NewReader(tt.input))
366+
if err != nil {
367+
t.Errorf("case %d: unexpected error parsing unit: %v", i, err)
368+
continue
369+
}
370+
371+
err = assert(tt.output, output)
372+
if err != nil {
373+
t.Errorf("case %d: %v", i, err)
374+
t.Log("Expected options:")
375+
for _, s := range tt.output {
376+
t.Log(s)
377+
}
378+
t.Log("Actual options:")
379+
for _, s := range output {
380+
t.Log(s)
381+
}
382+
}
383+
}
384+
}
385+
297386
func TestDeserializeFail(t *testing.T) {
298387
tests := [][]byte{
299388
// malformed section header
@@ -319,7 +408,7 @@ Description=Foo
319408
}
320409

321410
for i, tt := range tests {
322-
output, err := Deserialize(bytes.NewReader(tt))
411+
output, err := DeserializeOptions(bytes.NewReader(tt))
323412
if err == nil {
324413
t.Errorf("case %d: unexpected nil error", i)
325414
t.Log("Output:")
@@ -371,7 +460,7 @@ ExecStartExecStartExecStartExecStartExecStartExecStartExecStartExecStartExecStar
371460
}
372461

373462
for i, tt := range tests {
374-
output, err := Deserialize(bytes.NewReader(tt))
463+
output, err := DeserializeOptions(bytes.NewReader(tt))
375464
if err != ErrLineTooLong {
376465
t.Errorf("case %d: unexpected err: %v", i, err)
377466
t.Log("Output:")

unit/section.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2020 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+
15+
package unit
16+
17+
// UnitEntry is a single line entry in a Unit file.
18+
type UnitEntry struct {
19+
Name string
20+
Value string
21+
}
22+
23+
// UnitSection is a section in a Unit file. The section name
24+
// and a list of entries in that section.
25+
type UnitSection struct {
26+
Section string
27+
Entries []*UnitEntry
28+
}
29+
30+
// String implements the stringify interface for UnitEntry
31+
func (u *UnitEntry) String() string {
32+
return "{Name: " + u.Name + ", " + "Value: " + u.Value + "}"
33+
}
34+
35+
// String implements the stringify interface for UnitSection
36+
func (s *UnitSection) String() string {
37+
result := "{Section: " + s.Section
38+
for _, e := range s.Entries {
39+
result += e.String()
40+
}
41+
42+
result += "}"
43+
return result
44+
}

unit/serialize.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,29 @@ func Serialize(opts []*UnitOption) io.Reader {
5858
return &buf
5959
}
6060

61+
// SerializeSections will serializes the unit file from the given
62+
// UnitSections.
63+
func SerializeSections(sections []*UnitSection) io.Reader {
64+
65+
var buf bytes.Buffer
66+
67+
for i, s := range sections {
68+
writeSectionHeader(&buf, s.Section)
69+
writeNewline(&buf)
70+
71+
for _, e := range s.Entries {
72+
writeOption(&buf, &UnitOption{s.Section, e.Name, e.Value})
73+
writeNewline(&buf)
74+
}
75+
76+
if i < len(sections)-1 {
77+
writeNewline(&buf)
78+
}
79+
}
80+
81+
return &buf
82+
}
83+
6184
func writeNewline(buf *bytes.Buffer) {
6285
buf.WriteRune('\n')
6386
}

0 commit comments

Comments
 (0)