Skip to content

Commit cb7780b

Browse files
committed
Merge pull request #84 from vcaputo/systemd-escape
unit: add systemd-escape unit name escaping/unescaping
2 parents a317b84 + 118df93 commit cb7780b

File tree

2 files changed

+327
-0
lines changed

2 files changed

+327
-0
lines changed

unit/escape.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
15+
// Implements systemd-escape [--unescape] [--path]
16+
17+
package unit
18+
19+
import (
20+
"fmt"
21+
"strconv"
22+
"strings"
23+
)
24+
25+
const (
26+
allowed = `:_.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`
27+
)
28+
29+
// If isPath is true:
30+
// We remove redundant '/'s, the leading '/', and trailing '/'.
31+
// If the result is empty, a '/' is inserted.
32+
//
33+
// We always:
34+
// Replace the following characters with `\x%x`:
35+
// Leading `.`
36+
// `-`, `\`, and anything not in this set: `:-_.\[0-9a-zA-Z]`
37+
// Replace '/' with '-'.
38+
func escape(unescaped string, isPath bool) string {
39+
e := []byte{}
40+
inSlashes := false
41+
start := true
42+
for i := 0; i < len(unescaped); i++ {
43+
c := unescaped[i]
44+
if isPath {
45+
if c == '/' {
46+
inSlashes = true
47+
continue
48+
} else if inSlashes {
49+
inSlashes = false
50+
if !start {
51+
e = append(e, '-')
52+
}
53+
}
54+
}
55+
56+
if c == '/' {
57+
e = append(e, '-')
58+
} else if start && c == '.' || strings.IndexByte(allowed, c) == -1 {
59+
e = append(e, []byte(fmt.Sprintf(`\x%x`, c))...)
60+
} else {
61+
e = append(e, c)
62+
}
63+
start = false
64+
}
65+
if isPath && len(e) == 0 {
66+
e = append(e, '-')
67+
}
68+
return string(e)
69+
}
70+
71+
// If isPath is true:
72+
// We always return a string beginning with '/'.
73+
//
74+
// We always:
75+
// Replace '-' with '/'.
76+
// Replace `\x%x` with the value represented in hex.
77+
func unescape(escaped string, isPath bool) string {
78+
u := []byte{}
79+
for i := 0; i < len(escaped); i++ {
80+
c := escaped[i]
81+
if c == '-' {
82+
c = '/'
83+
} else if c == '\\' && len(escaped)-i >= 4 && escaped[i+1] == 'x' {
84+
n, err := strconv.ParseInt(escaped[i+2:i+4], 16, 8)
85+
if err == nil {
86+
c = byte(n)
87+
i += 3
88+
}
89+
}
90+
u = append(u, c)
91+
}
92+
if isPath && (len(u) == 0 || u[0] != '/') {
93+
u = append([]byte("/"), u...)
94+
}
95+
return string(u)
96+
}
97+
98+
// UnitNameEscape escapes a string as `systemd-escape` would
99+
func UnitNameEscape(unescaped string) string {
100+
return escape(unescaped, false)
101+
}
102+
103+
// UnitNameUnescape unescapes a string as `systemd-escape --unescape` would
104+
func UnitNameUnescape(escaped string) string {
105+
return unescape(escaped, false)
106+
}
107+
108+
// UnitNamePathEscape escapes a string as `systemd-escape --path` would
109+
func UnitNamePathEscape(unescaped string) string {
110+
return escape(unescaped, true)
111+
}
112+
113+
// UnitNamePathUnescape unescapes a string as `systemd-escape --path --unescape` would
114+
func UnitNamePathUnescape(escaped string) string {
115+
return unescape(escaped, true)
116+
}

unit/escape_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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+
15+
package unit
16+
17+
import (
18+
"testing"
19+
)
20+
21+
func TestUnitNameEscape(t *testing.T) {
22+
tests := []struct {
23+
in string
24+
out string
25+
isPath bool
26+
}{
27+
// turn empty string path into escaped /
28+
{
29+
in: "",
30+
out: "-",
31+
isPath: true,
32+
},
33+
// turn redundant ////s into single escaped /
34+
{
35+
in: "/////////",
36+
out: "-",
37+
isPath: true,
38+
},
39+
// remove all redundant ////s
40+
{
41+
in: "///foo////bar/////tail//////",
42+
out: "foo-bar-tail",
43+
isPath: true,
44+
},
45+
// leave empty string empty
46+
{
47+
in: "",
48+
out: "",
49+
isPath: false,
50+
},
51+
// escape leading dot
52+
{
53+
in: ".",
54+
out: `\x2e`,
55+
isPath: true,
56+
},
57+
// escape leading dot
58+
{
59+
in: "/.",
60+
out: `\x2e`,
61+
isPath: true,
62+
},
63+
// escape leading dot
64+
{
65+
in: "/////////.",
66+
out: `\x2e`,
67+
isPath: true,
68+
},
69+
// escape leading dot
70+
{
71+
in: "/////////.///////////////",
72+
out: `\x2e`,
73+
isPath: true,
74+
},
75+
// escape leading dot
76+
{
77+
in: ".....",
78+
out: `\x2e....`,
79+
isPath: true,
80+
},
81+
// escape leading dot
82+
{
83+
in: "/.foo/.bar",
84+
out: `\x2efoo-.bar`,
85+
isPath: true,
86+
},
87+
// escape leading dot
88+
{
89+
in: ".foo/.bar",
90+
out: `\x2efoo-.bar`,
91+
isPath: true,
92+
},
93+
// escape leading dot
94+
{
95+
in: ".foo/.bar",
96+
out: `\x2efoo-.bar`,
97+
isPath: false,
98+
},
99+
// escape disallowed
100+
{
101+
in: `///..\-!#??///`,
102+
out: `---..\x5c\x2d\x21\x23\x3f\x3f---`,
103+
isPath: false,
104+
},
105+
// escape disallowed
106+
{
107+
in: `///..\-!#??///`,
108+
out: `\x2e.\x5c\x2d\x21\x23\x3f\x3f`,
109+
isPath: true,
110+
},
111+
// escape real-world example
112+
{
113+
in: `user-cloudinit@/var/lib/coreos/vagrant/vagrantfile-user-data.service`,
114+
out: `user\x2dcloudinit\x40-var-lib-coreos-vagrant-vagrantfile\x2duser\x2ddata.service`,
115+
isPath: false,
116+
},
117+
}
118+
119+
for i, tt := range tests {
120+
var s string
121+
if tt.isPath {
122+
s = UnitNamePathEscape(tt.in)
123+
} else {
124+
s = UnitNameEscape(tt.in)
125+
}
126+
if s != tt.out {
127+
t.Errorf("case %d: failed escaping %v isPath: %v - expected %v, got %v", i, tt.in, tt.isPath, tt.out, s)
128+
}
129+
}
130+
}
131+
132+
func TestUnitNameUnescape(t *testing.T) {
133+
tests := []struct {
134+
in string
135+
out string
136+
isPath bool
137+
}{
138+
// turn empty string path into /
139+
{
140+
in: "",
141+
out: "/",
142+
isPath: true,
143+
},
144+
// leave empty string empty
145+
{
146+
in: "",
147+
out: "",
148+
isPath: false,
149+
},
150+
// turn ////s into
151+
{
152+
in: "---------",
153+
out: "/////////",
154+
isPath: true,
155+
},
156+
// unescape hex
157+
{
158+
in: `---..\x5c\x2d\x21\x23\x3f\x3f---`,
159+
out: `///..\-!#??///`,
160+
isPath: false,
161+
},
162+
// unescape hex
163+
{
164+
in: `\x2e.\x5c\x2d\x21\x23\x3f\x3f`,
165+
out: `/..\-!#??`,
166+
isPath: true,
167+
},
168+
// unescape hex, retain invalids
169+
{
170+
in: `\x2e.\x5c\x2d\xaZ\x.o\x21\x23\x3f\x3f`,
171+
out: `/..\-\xaZ\x.o!#??`,
172+
isPath: true,
173+
},
174+
// unescape hex, retain invalids, partial tail
175+
{
176+
in: `\x2e.\x5c\x\x2d\xaZ\x.o\x21\x23\x3f\x3f\x3`,
177+
out: `/..\\x-\xaZ\x.o!#??\x3`,
178+
isPath: true,
179+
},
180+
// unescape hex, retain invalids, partial tail
181+
{
182+
in: `\x2e.\x5c\x\x2d\xaZ\x.o\x21\x23\x3f\x3f\x`,
183+
out: `/..\\x-\xaZ\x.o!#??\x`,
184+
isPath: true,
185+
},
186+
// unescape hex, retain invalids, partial tail
187+
{
188+
in: `\x2e.\x5c\x\x2d\xaZ\x.o\x21\x23\x3f\x3f\`,
189+
out: `/..\\x-\xaZ\x.o!#??\`,
190+
isPath: true,
191+
},
192+
// unescape real-world example
193+
{
194+
in: `user\x2dcloudinit\x40-var-lib-coreos-vagrant-vagrantfile\x2duser\x2ddata.service`,
195+
out: `user-cloudinit@/var/lib/coreos/vagrant/vagrantfile-user-data.service`,
196+
isPath: false,
197+
},
198+
}
199+
200+
for i, tt := range tests {
201+
var s string
202+
if tt.isPath {
203+
s = UnitNamePathUnescape(tt.in)
204+
} else {
205+
s = UnitNameUnescape(tt.in)
206+
}
207+
if s != tt.out {
208+
t.Errorf("case %d: failed unescaping %v isPath: %v - expected %v, got %v", i, tt.in, tt.isPath, tt.out, s)
209+
}
210+
}
211+
}

0 commit comments

Comments
 (0)