Skip to content

Commit a15deed

Browse files
committed
Fix YAML patch in some cases #1626
1 parent 39c14e6 commit a15deed

File tree

6 files changed

+230
-241
lines changed

6 files changed

+230
-241
lines changed

internal/app/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ func LoadConfig(v any) {
1818
}
1919
}
2020

21-
func PatchConfig(key string, value any, path ...string) error {
21+
func PatchConfig(path []string, value any) error {
2222
if ConfigPath == "" {
2323
return errors.New("config file disabled")
2424
}
2525

2626
// empty config is OK
2727
b, _ := os.ReadFile(ConfigPath)
2828

29-
b, err := yaml.Patch(b, key, value, path...)
29+
b, err := yaml.Patch(b, path, value)
3030
if err != nil {
3131
return err
3232
}

internal/homekit/api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func apiPair(id, url string) error {
103103

104104
streams.New(id, conn.URL())
105105

106-
return app.PatchConfig(id, conn.URL(), "streams")
106+
return app.PatchConfig([]string{"streams", id}, conn.URL())
107107
}
108108

109109
func apiUnpair(id string) error {
@@ -123,7 +123,7 @@ func apiUnpair(id string) error {
123123

124124
streams.Delete(id)
125125

126-
return app.PatchConfig(id, nil, "streams")
126+
return app.PatchConfig([]string{"streams", id}, nil)
127127
}
128128

129129
func findHomeKitURLs() map[string]*url.URL {

internal/homekit/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func (s *server) DelPair(conn net.Conn, id string) {
222222
}
223223

224224
func (s *server) PatchConfig() {
225-
if err := app.PatchConfig("pairings", s.pairings, "homekit", s.stream); err != nil {
225+
if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil {
226226
log.Error().Err(err).Msgf(
227227
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
228228
)

internal/streams/api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
5353
return
5454
}
5555

56-
if err := app.PatchConfig(name, query["src"], "streams"); err != nil {
56+
if err := app.PatchConfig([]string{"streams", name}, query["src"]); err != nil {
5757
http.Error(w, err.Error(), http.StatusBadRequest)
5858
}
5959

@@ -96,7 +96,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
9696
case "DELETE":
9797
delete(streams, src)
9898

99-
if err := app.PatchConfig(src, nil, "streams"); err != nil {
99+
if err := app.PatchConfig([]string{"streams", src}, nil); err != nil {
100100
http.Error(w, err.Error(), http.StatusBadRequest)
101101
}
102102
}

pkg/yaml/yaml.go

Lines changed: 124 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -23,149 +23,157 @@ func Encode(v any, indent int) ([]byte, error) {
2323
return b.Bytes(), nil
2424
}
2525

26-
// Patch - change key/value pair in YAML file without break formatting
27-
func Patch(src []byte, key string, value any, path ...string) ([]byte, error) {
28-
nodeParent, err := FindParent(src, path...)
26+
func Patch(in []byte, path []string, value any) ([]byte, error) {
27+
out, err := patch(in, path, value)
2928
if err != nil {
3029
return nil, err
3130
}
3231

33-
var dst []byte
34-
35-
if nodeParent != nil {
36-
dst, err = AddOrReplace(src, key, value, nodeParent)
37-
} else {
38-
dst, err = AddToEnd(src, key, value, path...)
39-
}
40-
41-
if err = yaml.Unmarshal(dst, map[string]any{}); err != nil {
32+
// validate
33+
if err = yaml.Unmarshal(out, map[string]any{}); err != nil {
4234
return nil, err
4335
}
4436

45-
return dst, nil
37+
return out, nil
4638
}
4739

48-
// FindParent - return YAML Node from path of keys (tree)
49-
func FindParent(src []byte, path ...string) (*yaml.Node, error) {
50-
if len(src) == 0 {
51-
return nil, nil
52-
}
53-
40+
func patch(in []byte, path []string, value any) ([]byte, error) {
5441
var root yaml.Node
55-
if err := yaml.Unmarshal(src, &root); err != nil {
42+
if err := yaml.Unmarshal(in, &root); err != nil {
43+
// invalid yaml
5644
return nil, err
5745
}
5846

59-
if root.Content == nil {
60-
return nil, nil
47+
// empty in
48+
if len(root.Content) != 1 {
49+
return addToEnd(in, path, value)
6150
}
6251

63-
parent := root.Content[0] // yaml.DocumentNode
64-
for _, name := range path {
65-
if parent == nil {
66-
break
67-
}
68-
_, parent = FindChild(parent, name)
52+
// yaml is not dict
53+
if root.Content[0].Kind != yaml.MappingNode {
54+
return nil, errors.New("yaml: can't patch")
6955
}
70-
return parent, nil
71-
}
7256

73-
// FindChild - search and return YAML key/value pair for current Node
74-
func FindChild(node *yaml.Node, name string) (key, value *yaml.Node) {
75-
for i, child := range node.Content {
76-
if child.Value != name {
77-
continue
78-
}
79-
return child, node.Content[i+1]
80-
}
57+
// dict items list
58+
nodes := root.Content[0].Content
8159

82-
return nil, nil
83-
}
60+
n := len(path) - 1
8461

85-
func FirstChild(node *yaml.Node) *yaml.Node {
86-
if node.Content == nil {
87-
return node
62+
// parent node key/value
63+
pKey, pVal := findNode(nodes, path[:n])
64+
if pKey == nil {
65+
// no parent node
66+
return addToEnd(in, path, value)
8867
}
89-
return node.Content[0]
90-
}
9168

92-
func LastChild(node *yaml.Node) *yaml.Node {
93-
if node.Content == nil {
94-
return node
69+
var paste []byte
70+
71+
if value != nil {
72+
// nil value means delete key
73+
var err error
74+
v := map[string]any{path[n]: value}
75+
if paste, err = Encode(v, 2); err != nil {
76+
return nil, err
77+
}
9578
}
96-
return LastChild(node.Content[len(node.Content)-1])
97-
}
9879

99-
func AddOrReplace(src []byte, key string, value any, nodeParent *yaml.Node) ([]byte, error) {
100-
v := map[string]any{key: value}
101-
put, err := Encode(v, 2)
102-
if err != nil {
103-
return nil, err
80+
iKey, _ := findNode(pVal.Content, path[n:])
81+
if iKey != nil {
82+
// key item not nil (replace value)
83+
paste = addIndent(paste, iKey.Column-1)
84+
85+
i0, i1 := nodeBounds(in, iKey)
86+
return join(in[:i0], paste, in[i1:]), nil
10487
}
10588

106-
if nodeKey, nodeValue := FindChild(nodeParent, key); nodeKey != nil {
107-
put = AddIndent(put, nodeKey.Column-1)
89+
if pVal.Content != nil {
90+
// parent value not nil (use first child indent)
91+
paste = addIndent(paste, pVal.Column-1)
92+
} else {
93+
// parent value is nil (use parent indent + 2)
94+
paste = addIndent(paste, pKey.Column+1)
95+
}
10896

109-
i0 := LineOffset(src, nodeKey.Line)
110-
i1 := LineOffset(src, LastChild(nodeValue).Line+1)
97+
_, i1 := nodeBounds(in, pKey)
98+
return join(in[:i1], paste, in[i1:]), nil
99+
}
111100

112-
if i1 < 0 { // no new line on the end of file
113-
if value != nil {
114-
return append(src[:i0], put...), nil
101+
func findNode(nodes []*yaml.Node, keys []string) (key, value *yaml.Node) {
102+
for i, name := range keys {
103+
for j := 0; j < len(nodes); j += 2 {
104+
if nodes[j].Value == name {
105+
if i < len(keys)-1 {
106+
nodes = nodes[j+1].Content
107+
break
108+
}
109+
return nodes[j], nodes[j+1]
115110
}
116-
return src[:i0], nil
117-
}
118-
119-
dst := make([]byte, 0, len(src)+len(put))
120-
dst = append(dst, src[:i0]...)
121-
if value != nil {
122-
dst = append(dst, put...)
123111
}
124-
return append(dst, src[i1:]...), nil
125112
}
113+
return nil, nil
114+
}
126115

127-
put = AddIndent(put, FirstChild(nodeParent).Column-1)
116+
func nodeBounds(in []byte, node *yaml.Node) (offset0, offset1 int) {
117+
// start from next line after node
118+
offset0 = lineOffset(in, node.Line)
119+
offset1 = lineOffset(in, node.Line+1)
128120

129-
i := LineOffset(src, LastChild(nodeParent).Line+1)
121+
if offset1 < 0 {
122+
return offset0, len(in)
123+
}
130124

131-
if i < 0 { // no new line on the end of file
132-
src = append(src, '\n')
133-
if value != nil {
134-
src = append(src, put...)
125+
for i := offset1; i < len(in); {
126+
indent, length := parseLine(in[i:])
127+
if indent+1 != length {
128+
if node.Column < indent+1 {
129+
offset1 = i + length
130+
} else {
131+
break
132+
}
135133
}
136-
return src, nil
134+
i += length
137135
}
138136

139-
dst := make([]byte, 0, len(src)+len(put))
140-
dst = append(dst, src[:i]...)
141-
if value != nil {
142-
dst = append(dst, put...)
143-
}
144-
return append(dst, src[i:]...), nil
137+
return
145138
}
146139

147-
func AddToEnd(src []byte, key string, value any, path ...string) ([]byte, error) {
148-
if len(path) > 1 || value == nil {
149-
return nil, errors.New("config: path not exist")
140+
func addToEnd(in []byte, path []string, value any) ([]byte, error) {
141+
if len(path) != 2 || value == nil {
142+
return nil, errors.New("yaml: path not exist")
150143
}
151144

152145
v := map[string]map[string]any{
153-
path[0]: {key: value},
146+
path[0]: {path[1]: value},
154147
}
155-
put, err := Encode(v, 2)
148+
paste, err := Encode(v, 2)
156149
if err != nil {
157150
return nil, err
158151
}
159152

160-
dst := make([]byte, 0, len(src)+len(put)+10)
161-
dst = append(dst, src...)
162-
if l := len(src); l > 0 && src[l-1] != '\n' {
163-
dst = append(dst, '\n')
153+
return join(in, paste), nil
154+
}
155+
156+
func join(items ...[]byte) []byte {
157+
n := len(items) - 1
158+
for _, b := range items {
159+
n += len(b)
160+
}
161+
162+
buf := make([]byte, 0, n)
163+
for _, b := range items {
164+
if len(b) == 0 {
165+
continue
166+
}
167+
if n = len(buf); n > 0 && buf[n-1] != '\n' {
168+
buf = append(buf, '\n')
169+
}
170+
buf = append(buf, b...)
164171
}
165-
return append(dst, put...), nil
172+
173+
return buf
166174
}
167175

168-
func AddPrefix(src, pre []byte) (dst []byte) {
176+
func addPrefix(src, pre []byte) (dst []byte) {
169177
for len(src) > 0 {
170178
dst = append(dst, pre...)
171179
i := bytes.IndexByte(src, '\n') + 1
@@ -180,25 +188,43 @@ func AddPrefix(src, pre []byte) (dst []byte) {
180188
return
181189
}
182190

183-
func AddIndent(src []byte, indent int) (dst []byte) {
191+
func addIndent(in []byte, indent int) (dst []byte) {
184192
pre := make([]byte, indent)
185193
for i := 0; i < indent; i++ {
186194
pre[i] = ' '
187195
}
188-
return AddPrefix(src, pre)
196+
return addPrefix(in, pre)
189197
}
190198

191-
func LineOffset(b []byte, line int) (offset int) {
199+
func lineOffset(in []byte, line int) (offset int) {
192200
for l := 1; ; l++ {
193201
if l == line {
194202
return offset
195203
}
196204

197-
i := bytes.IndexByte(b[offset:], '\n') + 1
205+
i := bytes.IndexByte(in[offset:], '\n') + 1
198206
if i == 0 {
199207
break
200208
}
201209
offset += i
202210
}
203211
return -1
204212
}
213+
214+
func parseLine(b []byte) (indent int, length int) {
215+
prefix := true
216+
for ; length < len(b); length++ {
217+
switch b[length] {
218+
case ' ':
219+
if prefix {
220+
indent++
221+
}
222+
case '\n':
223+
length++
224+
return
225+
default:
226+
prefix = false
227+
}
228+
}
229+
return
230+
}

0 commit comments

Comments
 (0)