diff --git a/driver/netconf/driver.go b/driver/netconf/driver.go index 14f61b8..176e392 100644 --- a/driver/netconf/driver.go +++ b/driver/netconf/driver.go @@ -48,7 +48,10 @@ const ( subscriptionResultPattern = `(?i)notif-bis:(.+)` - emptyTagPattern = `<(\w+)>` + // emptyTagPattern matches netconf empty tags to allow + // forcing of self-closing tags. + // See https://regex101.com/r/rmsS2E/3. + emptyTagPattern = `<([^>/]+?)(\s+[^>]+?)?>\s*` defaultNamespace = "urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults" diff --git a/driver/netconf/rpc.go b/driver/netconf/rpc.go index e0fef07..0cf0e0d 100644 --- a/driver/netconf/rpc.go +++ b/driver/netconf/rpc.go @@ -26,24 +26,17 @@ func (d *Driver) RPC(opts ...util.Option) (*response.NetconfResponse, error) { return d.sendRPC(d.buildRPCElem(op.Filter), op) } -func forceSelfClosingTags(b []byte) []byte { +// ForceSelfClosingTags accepts a netconf looking xml byte slice and forces any "empty" tags (tags +// without attributes) to use self-closing tags. For example: +// ` ` +// Would be converted to: +// ``. +func ForceSelfClosingTags(b []byte) []byte { ncPatterns := getNetconfPatterns() - emptyTagIdxs := ncPatterns.emptyTags.FindAllSubmatchIndex(b, -1) + r := ncPatterns.emptyTags.ReplaceAll(b, []byte("<$1$2/>")) - var nb []byte - - for _, idx := range emptyTagIdxs { - // get everything in b up till the first of the submatch indexes (this is the start of an - // "empty" tag), then get the name of the tag and put it in a self-closing - // tag. - nb = append(b[0:idx[0]], fmt.Sprintf("<%s/>", b[idx[2]:idx[3]])...) //nolint: gocritic - - // finally, append everything *after* the submatch indexes - nb = append(nb, b[len(b)-(len(b)-idx[1]):]...) - } - - return nb + return r } func (d *Driver) sendRPC( @@ -58,7 +51,7 @@ func (d *Driver) sendRPC( if d.ForceSelfClosingTags { d.Logger.Debug("ForceSelfClosingTags is true, enforcing...") - b = forceSelfClosingTags(b) + b = ForceSelfClosingTags(b) } d.Logger.Debugf("sending finalized rpc payload:\n%s", string(b)) diff --git a/driver/netconf/rpc_test.go b/driver/netconf/rpc_test.go new file mode 100644 index 0000000..64df694 --- /dev/null +++ b/driver/netconf/rpc_test.go @@ -0,0 +1,57 @@ +package netconf_test + +import ( + "testing" + + "github.com/scrapli/scrapligo/driver/netconf" + + "github.com/google/go-cmp/cmp" +) + +func TestForceSelfClosingTags(t *testing.T) { + tests := map[string]struct { + got []byte + want []byte + }{ + "empty_tag_no_attrs": { + got: []byte( + `]]>]]>`, //nolint: lll + ), + want: []byte( + `]]>]]>`, //nolint: lll + ), + }, + "empty_tag_with_attrs": { + got: []byte( + `]]>]]>`, //nolint: lll + ), + want: []byte( + `]]>]]>`, //nolint: lll + ), + }, + "empty_tag_with_attrs_and_spaces": { + got: []byte( + ` `, + ), + want: []byte(``), + }, + "empty_tag_no_attrs_and_spaces": { + got: []byte(` `), + want: []byte(``), + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got := netconf.ForceSelfClosingTags(tt.got) + if !cmp.Equal(got, tt.want) { + t.Fatalf( + "%s: actual and expected values do not match\nactual: %s\nexpected:%s", + name, + got, + tt.want, + ) + } + }) + } +}