From dd82bb3c4ba11d83f64ee9b28605905e0ff5461f Mon Sep 17 00:00:00 2001 From: ionutboangiu Date: Tue, 10 Oct 2023 11:55:14 -0400 Subject: [PATCH] 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. --- agents/libhttpagent.go | 6 ++- config/config_it_test.go | 6 +-- config/config_test.go | 4 +- config/configsanity.go | 21 +++++++++ config/erscfg_test.go | 8 ++-- config/xmldp.go | 12 +++-- data/conf/samples/ers_internal/cgrates.json | 25 ++++++++++- data/conf/samples/ers_mongo/cgrates.json | 25 ++++++++++- data/conf/samples/ers_mysql/cgrates.json | 25 ++++++++++- data/conf/samples/ers_postgres/cgrates.json | 25 ++++++++++- ers/filexml.go | 25 ++++++----- ers/filexml_it_test.go | 50 ++++++++++++++++++++- utils/coreutils.go | 33 ++++++++------ 13 files changed, 221 insertions(+), 44 deletions(-) diff --git a/agents/libhttpagent.go b/agents/libhttpagent.go index a660e15d2..635b2a0d4 100644 --- a/agents/libhttpagent.go +++ b/agents/libhttpagent.go @@ -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 } diff --git a/config/config_it_test.go b/config/config_it_test.go index 0247eed8b..ab745ca21 100644 --- a/config/config_it_test.go +++ b/config/config_it_test.go @@ -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, }, }, diff --git a/config/config_test.go b/config/config_test.go index 62b482e38..a177e756f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -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, diff --git a/config/configsanity.go b/config/configsanity.go index 3a4c0b3eb..b71e625c1 100644 --- a/config/configsanity.go +++ b/config/configsanity.go @@ -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) + } + } } } } diff --git a/config/erscfg_test.go b/config/erscfg_test.go index fbc48f4bc..4b1c5d984 100644 --- a/config/erscfg_test.go +++ b/config/erscfg_test.go @@ -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{}, }, }, } diff --git a/config/xmldp.go b/config/xmldp.go index d78f8db3e..eb43ed6a9 100644 --- a/config/xmldp.go +++ b/config/xmldp.go @@ -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 } diff --git a/data/conf/samples/ers_internal/cgrates.json b/data/conf/samples/ers_internal/cgrates.json index 14357ce01..da313a220 100644 --- a/data/conf/samples/ers_internal/cgrates.json +++ b/data/conf/samples/ers_internal/cgrates.json @@ -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", diff --git a/data/conf/samples/ers_mongo/cgrates.json b/data/conf/samples/ers_mongo/cgrates.json index 21b9f3eea..896725100 100644 --- a/data/conf/samples/ers_mongo/cgrates.json +++ b/data/conf/samples/ers_mongo/cgrates.json @@ -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", diff --git a/data/conf/samples/ers_mysql/cgrates.json b/data/conf/samples/ers_mysql/cgrates.json index e1e3d674d..fe7da8de7 100644 --- a/data/conf/samples/ers_mysql/cgrates.json +++ b/data/conf/samples/ers_mysql/cgrates.json @@ -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", diff --git a/data/conf/samples/ers_postgres/cgrates.json b/data/conf/samples/ers_postgres/cgrates.json index 5eb366552..b320b95d9 100644 --- a/data/conf/samples/ers_postgres/cgrates.json +++ b/data/conf/samples/ers_postgres/cgrates.json @@ -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", diff --git a/ers/filexml.go b/ers/filexml.go index 569a4e56d..b7b646c64 100644 --- a/ers/filexml.go +++ b/ers/filexml.go @@ -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 } diff --git a/ers/filexml_it_test.go b/ers/filexml_it_test.go index 0cdccf769..1822217e2 100644 --- a/ers/filexml_it_test.go +++ b/ers/filexml_it_test.go @@ -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 = ` + + + 1 + 1001 + 1002 + cgrates.org + 20231011125833.158 + + + 2 + *postpaid + *voice + + + EmptyRootCDR + 3 + ignored + 20231011125800 + 20231011125801 + +` + +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) } diff --git a/utils/coreutils.go b/utils/coreutils.go index a4d719a6d..ffb6b2d3b 100644 --- a/utils/coreutils.go +++ b/utils/coreutils.go @@ -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