Add sanity check to prevent xml reader panic

HierarchyPath parser now returns nil when
the path is empty (instead of a string slice with one
EmptyString element).

If the prefix is set to true, when calling the AsString
method on a nil HierarchyPath, only the separator will
be returned. This avoids a nil expr error coming from
the xmlquery library.

Use the Query and QueryAll functions from the xmlquery
package to be able to handle the errors ourselves and
avoid panics.

Added an integration tes for the special case where the
xml_root_path field is left empty. Before the change it
used to trim the root element from the path slice when
attempting to retrieve a the relative path slice.
This commit is contained in:
ionutboangiu
2023-10-10 11:55:14 -04:00
committed by Dan Christian Bogos
parent f36ea1676f
commit dd82bb3c4b
13 changed files with 221 additions and 44 deletions

View File

@@ -162,7 +162,11 @@ func (hU *httpXmlDP) FieldAsInterface(fldPath []string) (data any, err error) {
}
//convert workPath to HierarchyPath
hrPath := utils.HierarchyPath(workPath)
elmnt := xmlquery.FindOne(hU.xmlDoc, hrPath.AsString("/", false))
var elmnt *xmlquery.Node
elmnt, err = xmlquery.Query(hU.xmlDoc, hrPath.AsString("/", false))
if err != nil {
return nil, err
}
if elmnt == nil {
return
}

View File

@@ -466,7 +466,6 @@ func testCGRConfigReloadERs(t *testing.T) {
Flags: flagsDefault,
Fields: content,
CacheDumpFields: []*FCTemplate{},
XmlRootPath: utils.HierarchyPath{utils.EmptyString},
},
{
ID: "file_reader1",
@@ -479,7 +478,6 @@ func testCGRConfigReloadERs(t *testing.T) {
Flags: flags,
Fields: content,
CacheDumpFields: []*FCTemplate{},
XmlRootPath: utils.HierarchyPath{utils.EmptyString},
},
},
}
@@ -858,7 +856,7 @@ func testCgrCfgV1ReloadConfigSection(t *testing.T) {
"Tenant": nil,
"Timezone": "",
"Type": "*none",
"XmlRootPath": []any{utils.EmptyString},
"XmlRootPath": nil,
},
map[string]any{
"CacheDumpFields": []any{},
@@ -879,7 +877,7 @@ func testCgrCfgV1ReloadConfigSection(t *testing.T) {
"Tenant": nil,
"Timezone": "",
"Type": "*file_csv",
"XmlRootPath": []any{utils.EmptyString},
"XmlRootPath": nil,
"Fields": content,
},
},

View File

@@ -1691,7 +1691,7 @@ func TestCgrCdfEventReader(t *testing.T) {
ConcurrentReqs: 1024,
SourcePath: "/var/spool/cgrates/ers/in",
ProcessedPath: "/var/spool/cgrates/ers/out",
XmlRootPath: utils.HierarchyPath{utils.EmptyString},
XmlRootPath: nil,
Tenant: nil,
Timezone: utils.EmptyString,
Filters: []string{},
@@ -1743,7 +1743,7 @@ func TestCgrCfgEventReaderDefault(t *testing.T) {
ConcurrentReqs: 1024,
SourcePath: "/var/spool/cgrates/ers/in",
ProcessedPath: "/var/spool/cgrates/ers/out",
XmlRootPath: utils.HierarchyPath{utils.EmptyString},
XmlRootPath: nil,
Tenant: nil,
Timezone: utils.EmptyString,
Filters: nil,

View File

@@ -20,6 +20,7 @@ package config
import (
"fmt"
"math"
"os"
"strings"
@@ -512,6 +513,26 @@ func (cfg *CGRConfig) checkConfigSanity() error {
if field.Type != utils.META_NONE && field.Path == utils.EmptyString {
return fmt.Errorf("<%s> %s for %s at %s", utils.ERs, utils.NewErrMandatoryIeMissing(utils.Path), rdr.ID, field.Tag)
}
// The following sanity check prevents a "slice bounds out of range" panic.
if rdr.Type == utils.MetaFileXML && !utils.IsSliceMember([]string{utils.META_NONE, utils.META_CONSTANT,
utils.META_FILLER, utils.MetaRemoteHost}, field.Type) && len(field.Value) != 0 {
// Retrieve the number of elements of the parser rule with the fewest elements.
minRuleLength := math.MaxInt
for _, rule := range field.Value {
if ruleLen := len(strings.Split(rule.AttrName(), utils.NestingSep)); minRuleLength > ruleLen {
minRuleLength = ruleLen
}
}
if len(rdr.XmlRootPath) >= minRuleLength {
return fmt.Errorf("<%s> %s for reader %s at %s",
utils.ERs,
"len of xml_root_path elements cannot be equal to or exceed the number of value rule elements",
rdr.ID, field.Tag)
}
}
}
}
}

View File

@@ -113,7 +113,7 @@ func TestEventReaderLoadFromJSON(t *testing.T) {
ConcurrentReqs: 1024,
SourcePath: "/var/spool/cgrates/ers/in",
ProcessedPath: "/var/spool/cgrates/ers/out",
XmlRootPath: utils.HierarchyPath{utils.EmptyString},
XmlRootPath: nil,
Tenant: nil,
Timezone: utils.EmptyString,
Filters: []string{},
@@ -152,7 +152,7 @@ func TestEventReaderLoadFromJSON(t *testing.T) {
ConcurrentReqs: 1024,
SourcePath: "/tmp/ers/in",
ProcessedPath: "/tmp/ers/out",
XmlRootPath: utils.HierarchyPath{utils.EmptyString},
XmlRootPath: nil,
Tenant: nil,
Timezone: utils.EmptyString,
Filters: nil,
@@ -270,7 +270,7 @@ func TestERsCfgAsMapInterface(t *testing.T) {
"source_path": "/var/spool/cgrates/ers/in",
"tenant": "",
"timezone": "",
"xml_root_path": []string{""},
"xml_root_path": []string{},
"cache_dump_fields": []map[string]any{},
"concurrent_requests": 1024,
"type": "*none",
@@ -320,7 +320,7 @@ func TestERsCfgAsMapInterface(t *testing.T) {
"source_path": "/tmp/ers/in",
"tenant": "",
"timezone": "",
"xml_root_path": []string{""},
"xml_root_path": []string{},
},
},
}

View File

@@ -44,6 +44,9 @@ type XmlProvider struct {
// String is part of engine.utils.DataProvider interface
// when called, it will display the already parsed values out of cache
func (xP *XmlProvider) String() string {
// TODO: Find a proper way to display xP as string. Right now it only has
// unexported fields, this will return an empty string.
return utils.ToJSON(xP)
}
@@ -62,11 +65,11 @@ func (xP *XmlProvider) FieldAsInterface(fldPath []string) (data any, err error)
for i := range relPath {
if sIdx := strings.Index(relPath[i], "["); sIdx != -1 {
slctrStr = relPath[i][sIdx:]
if slctrStr[len(slctrStr)-1:] != "]" {
if slctrStr[len(slctrStr)-1] != ']' {
return nil, fmt.Errorf("filter rule <%s> needs to end in ]", slctrStr)
}
relPath[i] = relPath[i][:sIdx]
if slctrStr[1:2] != "@" {
if slctrStr[1] != '@' {
i, err := strconv.Atoi(slctrStr[1 : len(slctrStr)-1])
if err != nil {
return nil, err
@@ -100,7 +103,10 @@ func (xP *XmlProvider) RemoteHost() net.Addr {
// returns utils.ErrNotFound if the element is not found in the node
// Make the method exportable until we remove the ers
func ElementText(xmlElement *xmlquery.Node, elmntPath string) (string, error) {
elmnt := xmlquery.FindOne(xmlElement, elmntPath)
elmnt, err := xmlquery.Query(xmlElement, elmntPath)
if err != nil {
return "", err
}
if elmnt == nil {
return "", utils.ErrNotFound
}

View File

@@ -109,7 +109,7 @@
{"tag": "HDRExtra3", "path": "*cgreq.HDRExtra3", "type": "*variable", "value": "~*req.6", "mandatory": true},
{"tag": "HDRExtra2", "path": "*cgreq.HDRExtra2", "type": "*variable", "value": "~*req.6", "mandatory": true},
{"tag": "HDRExtra1", "path": "*cgreq.HDRExtra1", "type": "*variable", "value": "~*req.6", "mandatory": true},
],
]
},
{
"id": "init_session",
@@ -220,6 +220,29 @@
{"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.broadWorksCDR.cdrData.basicModule.releaseTime;~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true}
],
},
{
"id": "XML_EmptyRoot",
"run_delay": "-1",
"type": "*file_xml",
"source_path": "/tmp/xmlErs2/in",
"flags": ["*cdrs","*log"],
"processed_path": "/tmp/xmlErs2/out",
"xml_root_path": "",
"fields":[
{"tag": "ToR", "path": "*cgreq.ToR", "type": "*variable", "value": "~*req.root.partial_cdr.ToR", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr.TestCase", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "_", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr[1].ID", "mandatory": true},
{"tag": "RequestType", "path": "*cgreq.RequestType", "type": "*variable", "value": "~*req.root.partial_cdr.RequestType", "mandatory": true},
{"tag": "Tenant", "path": "*cgreq.Tenant", "type": "*variable", "value": "~*req.root.partial_cdr.Tenant", "mandatory": true},
{"tag": "Category", "path": "*cgreq.Category", "type": "*constant", "value": "call", "mandatory": true},
{"tag": "Account", "path": "*cgreq.Account", "type": "*variable", "value": "~*req.root.partial_cdr.Account", "mandatory": true},
{"tag": "Destination", "path": "*cgreq.Destination", "type": "*variable", "value": "~*req.root.partial_cdr.Destination", "mandatory": true},
{"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.root.partial_cdr.SetupTime", "mandatory": true},
{"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.root.partial_cdr.AnswerTime", "mandatory": true},
{"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.root.partial_cdr.ReleaseTime;~*req.root.partial_cdr.AnswerTime", "mandatory": true}
]
},
{
"id": "FWV1",
"run_delay": "-1",

View File

@@ -219,7 +219,30 @@
{"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.startTime", "mandatory": true},
{"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true},
{"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.broadWorksCDR.cdrData.basicModule.releaseTime;~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true}
],
]
},
{
"id": "XML_EmptyRoot",
"run_delay": "-1",
"type": "*file_xml",
"source_path": "/tmp/xmlErs2/in",
"flags": ["*cdrs","*log"],
"processed_path": "/tmp/xmlErs2/out",
"xml_root_path": "",
"fields":[
{"tag": "ToR", "path": "*cgreq.ToR", "type": "*variable", "value": "~*req.root.partial_cdr.ToR", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr.TestCase", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "_", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr[1].ID", "mandatory": true},
{"tag": "RequestType", "path": "*cgreq.RequestType", "type": "*variable", "value": "~*req.root.partial_cdr.RequestType", "mandatory": true},
{"tag": "Tenant", "path": "*cgreq.Tenant", "type": "*variable", "value": "~*req.root.partial_cdr.Tenant", "mandatory": true},
{"tag": "Category", "path": "*cgreq.Category", "type": "*constant", "value": "call", "mandatory": true},
{"tag": "Account", "path": "*cgreq.Account", "type": "*variable", "value": "~*req.root.partial_cdr.Account", "mandatory": true},
{"tag": "Destination", "path": "*cgreq.Destination", "type": "*variable", "value": "~*req.root.partial_cdr.Destination", "mandatory": true},
{"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.root.partial_cdr.SetupTime", "mandatory": true},
{"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.root.partial_cdr.AnswerTime", "mandatory": true},
{"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.root.partial_cdr.ReleaseTime;~*req.root.partial_cdr.AnswerTime", "mandatory": true}
]
},
{
"id": "FWV1",

View File

@@ -216,7 +216,30 @@
{"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.startTime", "mandatory": true},
{"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true},
{"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.broadWorksCDR.cdrData.basicModule.releaseTime;~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true}
],
]
},
{
"id": "XML_EmptyRoot",
"run_delay": "-1",
"type": "*file_xml",
"source_path": "/tmp/xmlErs2/in",
"flags": ["*cdrs","*log"],
"processed_path": "/tmp/xmlErs2/out",
"xml_root_path": "",
"fields":[
{"tag": "ToR", "path": "*cgreq.ToR", "type": "*variable", "value": "~*req.root.partial_cdr.ToR", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr.TestCase", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "_", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr[1].ID", "mandatory": true},
{"tag": "RequestType", "path": "*cgreq.RequestType", "type": "*variable", "value": "~*req.root.partial_cdr.RequestType", "mandatory": true},
{"tag": "Tenant", "path": "*cgreq.Tenant", "type": "*variable", "value": "~*req.root.partial_cdr.Tenant", "mandatory": true},
{"tag": "Category", "path": "*cgreq.Category", "type": "*constant", "value": "call", "mandatory": true},
{"tag": "Account", "path": "*cgreq.Account", "type": "*variable", "value": "~*req.root.partial_cdr.Account", "mandatory": true},
{"tag": "Destination", "path": "*cgreq.Destination", "type": "*variable", "value": "~*req.root.partial_cdr.Destination", "mandatory": true},
{"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.root.partial_cdr.SetupTime", "mandatory": true},
{"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.root.partial_cdr.AnswerTime", "mandatory": true},
{"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.root.partial_cdr.ReleaseTime;~*req.root.partial_cdr.AnswerTime", "mandatory": true}
]
},
{
"id": "FWV1",

View File

@@ -213,7 +213,30 @@
{"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.startTime", "mandatory": true},
{"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true},
{"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.broadWorksCDR.cdrData.basicModule.releaseTime;~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true}
],
]
},
{
"id": "XML_EmptyRoot",
"run_delay": "-1",
"type": "*file_xml",
"source_path": "/tmp/xmlErs2/in",
"flags": ["*cdrs","*log"],
"processed_path": "/tmp/xmlErs2/out",
"xml_root_path": "",
"fields":[
{"tag": "ToR", "path": "*cgreq.ToR", "type": "*variable", "value": "~*req.root.partial_cdr.ToR", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr.TestCase", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "_", "mandatory": true},
{"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr[1].ID", "mandatory": true},
{"tag": "RequestType", "path": "*cgreq.RequestType", "type": "*variable", "value": "~*req.root.partial_cdr.RequestType", "mandatory": true},
{"tag": "Tenant", "path": "*cgreq.Tenant", "type": "*variable", "value": "~*req.root.partial_cdr.Tenant", "mandatory": true},
{"tag": "Category", "path": "*cgreq.Category", "type": "*constant", "value": "call", "mandatory": true},
{"tag": "Account", "path": "*cgreq.Account", "type": "*variable", "value": "~*req.root.partial_cdr.Account", "mandatory": true},
{"tag": "Destination", "path": "*cgreq.Destination", "type": "*variable", "value": "~*req.root.partial_cdr.Destination", "mandatory": true},
{"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.root.partial_cdr.SetupTime", "mandatory": true},
{"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.root.partial_cdr.AnswerTime", "mandatory": true},
{"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.root.partial_cdr.ReleaseTime;~*req.root.partial_cdr.AnswerTime", "mandatory": true}
]
},
{
"id": "FWV1",

View File

@@ -115,7 +115,7 @@ func (rdr *XMLFileER) Serve() (err error) {
}
// processFile is called for each file in a directory and dispatches erEvents from it
func (rdr *XMLFileER) processFile(fPath, fName string) (err error) {
func (rdr *XMLFileER) processFile(fPath, fName string) error {
if cap(rdr.conReqs) != 0 { // 0 goes for no limit
processFile := <-rdr.conReqs // Queue here for maxOpenFiles
defer func() { rdr.conReqs <- processFile }()
@@ -123,16 +123,21 @@ func (rdr *XMLFileER) processFile(fPath, fName string) (err error) {
absPath := path.Join(fPath, fName)
utils.Logger.Info(
fmt.Sprintf("<%s> parsing <%s>", utils.ERs, absPath))
var file *os.File
if file, err = os.Open(absPath); err != nil {
return
}
defer file.Close()
doc, err := xmlquery.Parse(file)
file, err := os.Open(absPath)
if err != nil {
return err
}
defer file.Close()
var doc *xmlquery.Node
doc, err = xmlquery.Parse(file)
if err != nil {
return err
}
var xmlElmts []*xmlquery.Node
xmlElmts, err = xmlquery.QueryAll(doc, rdr.Config().XmlRootPath.AsString("/", true))
if err != nil {
return err
}
xmlElmts := xmlquery.Find(doc, rdr.Config().XmlRootPath.AsString("/", true))
rowNr := 0 // This counts the rows in the file, not really number of CDRs
evsPosted := 0
timeStart := time.Now()
@@ -167,12 +172,12 @@ func (rdr *XMLFileER) processFile(fPath, fName string) (err error) {
// Finished with file, move it to processed folder
outPath := path.Join(rdr.Config().ProcessedPath, fName)
if err = os.Rename(absPath, outPath); err != nil {
return
return err
}
}
utils.Logger.Info(
fmt.Sprintf("%s finished processing file <%s>. Total records processed: %d, events posted: %d, run duration: %s",
utils.ERs, absPath, rowNr, evsPosted, time.Now().Sub(timeStart)))
return
return nil
}

View File

@@ -49,6 +49,8 @@ var (
testXMLITLoadTPFromFolder,
testXMLITHandleCdr1File,
testXmlITAnalyseCDRs,
testXMLITEmptyRootPathCase,
testXMLITEmptyRootPathCaseCheckCDRs,
testXMLITCleanupFiles,
testXMLITKillEngine,
}
@@ -79,7 +81,7 @@ func testXMLITCreateCdrDirs(t *testing.T) {
"/tmp/cdrs/out", "/tmp/ers_with_filters/in", "/tmp/ers_with_filters/out",
"/tmp/xmlErs/in", "/tmp/xmlErs/out", "/tmp/fwvErs/in", "/tmp/fwvErs/out",
"/tmp/partErs1/in", "/tmp/partErs1/out", "/tmp/partErs2/in", "/tmp/partErs2/out",
"/tmp/flatstoreErs/in", "/tmp/flatstoreErs/out"} {
"/tmp/flatstoreErs/in", "/tmp/flatstoreErs/out", "/tmp/xmlErs2/in", "/tmp/xmlErs2/out"} {
if err := os.RemoveAll(dir); err != nil {
t.Fatal("Error removing folder: ", dir, err)
}
@@ -297,11 +299,55 @@ func testXmlITAnalyseCDRs(t *testing.T) {
}
}
var xmlPartialCDR = `<?xml version="1.0" encoding="ISO-8859-1"?>
<root>
<partial_cdr>
<ID>1</ID>
<Account>1001</Account>
<Destination>1002</Destination>
<Tenant>cgrates.org</Tenant>
<ReleaseTime>20231011125833.158</ReleaseTime>
</partial_cdr>
<partial_cdr>
<ID>2</ID>
<RequestType>*postpaid</RequestType>
<ToR>*voice</ToR>
</partial_cdr>
<partial_cdr>
<TestCase>EmptyRootCDR</TestCase>
<ID>3</ID>
<Tenant>ignored</Tenant>
<SetupTime>20231011125800</SetupTime>
<AnswerTime>20231011125801</AnswerTime>
</partial_cdr>
</root>`
func testXMLITEmptyRootPathCase(t *testing.T) {
fileName := "partial-cdr.xml"
tmpFilePath := path.Join("/tmp", fileName)
if err := os.WriteFile(tmpFilePath, []byte(xmlPartialCDR), 0644); err != nil {
t.Fatal(err.Error())
}
if err := os.Rename(tmpFilePath, path.Join("/tmp/xmlErs2/in", fileName)); err != nil {
t.Fatal("Error moving file to processing directory: ", err)
}
time.Sleep(100 * time.Millisecond)
}
func testXMLITEmptyRootPathCaseCheckCDRs(t *testing.T) {
var reply []*engine.ExternalCDR
if err := xmlRPC.Call(utils.APIerSv2GetCDRs, utils.RPCCDRsFilter{OriginIDs: []string{"EmptyRootCDR_2"}}, &reply); err != nil {
t.Error("Unexpected error: ", err.Error())
} else if len(reply) != 3 {
t.Error("Unexpected number of CDRs returned:", len(reply))
}
}
func testXMLITCleanupFiles(t *testing.T) {
for _, dir := range []string{"/tmp/ers",
"/tmp/ers2", "/tmp/init_session", "/tmp/terminate_session",
"/tmp/cdrs", "/tmp/ers_with_filters", "/tmp/xmlErs", "/tmp/fwvErs",
"/tmp/partErs1", "/tmp/partErs2", "tmp/flatstoreErs"} {
"/tmp/partErs1", "/tmp/partErs2", "/tmp/flatstoreErs", "/tmp/xmlErs2"} {
if err := os.RemoveAll(dir); err != nil {
t.Fatal("Error removing folder: ", dir, err)
}

View File

@@ -625,9 +625,12 @@ func TimeIs0h(t time.Time) bool {
}
func ParseHierarchyPath(path string, sep string) HierarchyPath {
if path == EmptyString {
return nil
}
if sep == EmptyString {
for _, sep = range []string{"/", NestingSep} {
if idx := strings.Index(path, sep); idx != -1 {
if strings.Contains(path, sep) {
break
}
}
@@ -639,22 +642,24 @@ func ParseHierarchyPath(path string, sep string) HierarchyPath {
// HierarchyPath is used in various places to represent various path hierarchies (eg: in Diameter groups, XML trees)
type HierarchyPath []string
func (h HierarchyPath) AsString(sep string, prefix bool) string {
if len(h) == 0 {
return EmptyString
// AsString converts HierarchyPath to a string.
func (hP HierarchyPath) AsString(sep string, prefix bool) string {
var strHP strings.Builder
// If prefix is true and the HierarchyPath slice is empty, sep will be returned.
if prefix {
strHP.WriteString(sep)
}
retStr := EmptyString
for idx, itm := range h {
if idx == 0 {
if prefix {
retStr += sep
}
} else {
retStr += sep
if len(hP) == 0 {
return strHP.String()
}
for i, elem := range hP {
if i != 0 {
strHP.WriteString(sep)
}
retStr += itm
strHP.WriteString(elem)
}
return retStr
return strHP.String()
}
// Mask a number of characters in the suffix of the destination