Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,8 @@ func (b *builder) processNode(root node, flags flag, props *builderProp) (q quer
}

// build builds a specified XPath expressions expr.
func build(expr string, namespaces map[string]string) (q query, err error) {
// build returns the query and the parser used for parsing.
func build(expr string, namespaces map[string]string) (q query, p *parser, err error) {
defer func() {
if e := recover(); e != nil {
switch x := e.(type) {
Expand All @@ -711,8 +712,9 @@ func build(expr string, namespaces map[string]string) (q query, err error) {
}
}
}()
root := parse(expr, namespaces)
root, p := parse(expr, namespaces)
b := &builder{}
props := builderProps.None
return b.processNode(root, flagsEnum.None, &props)
q, err = b.processNode(root, flagsEnum.None, &props)
return q, p, err
}
6 changes: 4 additions & 2 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -564,12 +564,14 @@ func (p *parser) parseMethod(n node) node {
}

// Parse parsing the XPath express string expr and returns a tree node.
func parse(expr string, namespaces map[string]string) node {
// parse returns the root node and the parser used for parsing.
func parse(expr string, namespaces map[string]string) (node, *parser) {
r := &scanner{text: expr}
r.nextChar()
r.nextItem()
p := &parser{r: r, namespaces: namespaces}
return p.parseExpression(nil)
root := p.parseExpression(nil)
return root, p
}

// rootNode holds a top-level node of tree.
Expand Down
43 changes: 28 additions & 15 deletions xpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import (
"fmt"
)

// CompileOptions allows customizing the behavior of the XPath parser.
type CompileOptions struct {
StrictEOF bool // If true, require full input consumption (no trailing tokens)
// Future strictness options can be added here
}

// StrictPreset enables all strictness options (update as new options are added)
var StrictPreset = CompileOptions{
StrictEOF: true,
}

// NodeType represents a type of XPath node.
type NodeType int

Expand Down Expand Up @@ -138,17 +149,7 @@ func (expr *Expr) String() string {

// Compile compiles an XPath expression string.
func Compile(expr string) (*Expr, error) {
if expr == "" {
return nil, errors.New("expr expression is nil")
}
qy, err := build(expr, nil)
if err != nil {
return nil, err
}
if qy == nil {
return nil, fmt.Errorf(fmt.Sprintf("undeclared variable in XPath expression: %s", expr))
}
return &Expr{s: expr, q: qy}, nil
return CompileWithOptionsAndNS(expr, CompileOptions{}, nil)
}

// MustCompile compiles an XPath expression string and ignored error.
Expand All @@ -162,15 +163,27 @@ func MustCompile(expr string) *Expr {

// CompileWithNS compiles an XPath expression string, using given namespaces map.
func CompileWithNS(expr string, namespaces map[string]string) (*Expr, error) {
return CompileWithOptionsAndNS(expr, CompileOptions{}, namespaces)
}

// CompileWithOptions compiles an XPath expression string with the given options.
func CompileWithOptions(expr string, opts CompileOptions) (*Expr, error) {
return CompileWithOptionsAndNS(expr, opts, nil)
}

func CompileWithOptionsAndNS(expr string, opts CompileOptions, namespaces map[string]string) (*Expr, error) {
if expr == "" {
return nil, errors.New("expr expression is nil")
}
qy, err := build(expr, namespaces)
q, p, err := build(expr, namespaces)
if err != nil {
return nil, err
}
if qy == nil {
return nil, fmt.Errorf(fmt.Sprintf("undeclared variable in XPath expression: %s", expr))
if opts.StrictEOF && p != nil && p.r.typ != itemEOF {
return nil, fmt.Errorf("unexpected token after end of expression: %s", p.r.text[p.r.pos-p.r.currSize-1:])
}
if q == nil {
return nil, fmt.Errorf("undeclared variable in XPath expression: %s", expr)
}
return &Expr{s: expr, q: qy}, nil
return &Expr{s: expr, q: q}, nil
}
252 changes: 167 additions & 85 deletions xpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,26 +91,26 @@ func test_xpath_eval(t *testing.T, root *TNode, expr string, expected ...interfa
func Test_Predicates_MultiParent(t *testing.T) {
// https://github.com/antchfx/xpath/issues/75
/*
<measCollecFile xmlns="http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec">
<measData>
<measInfo>
<measType p="1">field1</measType>
<measType p="2">field2</measType>
<measValue>
<r p="1">31854</r>
<r p="2">159773</r>
</measValue>
</measInfo>
<measInfo measInfoId="metric_name2">
<measType p="1">field3</measType>
<measType p="2">field4</measType>
<measValue>
<r p="1">1234</r>
<r p="2">567</r>
</measValue>
</measInfo>
</measData>
</measCollecFile>
<measCollecFile xmlns="http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec">
<measData>
<measInfo>
<measType p="1">field1</measType>
<measType p="2">field2</measType>
<measValue>
<r p="1">31854</r>
<r p="2">159773</r>
</measValue>
</measInfo>
<measInfo measInfoId="metric_name2">
<measType p="1">field3</measType>
<measType p="2">field4</measType>
<measValue>
<r p="1">1234</r>
<r p="2">567</r>
</measValue>
</measInfo>
</measData>
</measCollecFile>
*/
doc := createNode("", RootNode)
measCollecFile := doc.createChildNode("measCollecFile", ElementNode)
Expand Down Expand Up @@ -281,6 +281,88 @@ func TestNodeType(t *testing.T) {
assertEqual(t, CommentNode, n.Type)
}

func TestCompileWithOptions_StrictEOF(t *testing.T) {
doc := createBookExample()

testCases := []struct {
name string
expr string
options CompileOptions
wantErr bool
wantLen int // -1 if not applicable
errMsg string // expected error message (substring match)
}{
{
name: "StrictEOF: valid expression",
expr: "//book",
options: CompileOptions{StrictEOF: true},
wantErr: false,
wantLen: 4,
},
{
name: "StrictEOF: valid expression with predicate",
expr: "//book[@category='web']",
options: CompileOptions{StrictEOF: true},
wantErr: false,
wantLen: 2,
},
{
name: "StrictEOF: expression with extra trailing tokens returns error",
expr: "//book,foo",
options: CompileOptions{StrictEOF: true},
wantErr: true,
wantLen: -1,
errMsg: "unexpected token after end of expression: ,foo",
},
{
name: "Default: expression with extra trailing tokens is accepted",
expr: "//book,foo",
options: CompileOptions{},
wantErr: false,
wantLen: -1,
},
{
name: "StrictPreset: valid expression",
expr: "//book/title",
options: StrictPreset,
wantErr: false,
wantLen: 4,
},
{
name: "StrictPreset: expression with extra trailing tokens returns error",
expr: "//book/title,foo",
options: StrictPreset,
wantErr: true,
wantLen: -1,
errMsg: "unexpected token after end of expression: ,foo",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e, err := CompileWithOptions(tc.expr, tc.options)
if tc.wantErr {
if err == nil {
t.Errorf("expected error, got nil")
return
}
if tc.errMsg != "" && !strings.Contains(err.Error(), tc.errMsg) {
t.Errorf("expected error message to contain %q, got: %v", tc.errMsg, err)
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if tc.wantLen >= 0 {
nodes := iterateNodes(e.Select(createNavigator(doc)))
assertEqual(t, tc.wantLen, len(nodes))
}
})
}
}

func iterateNavs(t *NodeIterator) []*TNodeNavigator {
var nodes []*TNodeNavigator
for t.MoveNext() {
Expand Down Expand Up @@ -589,37 +671,37 @@ func (n *TNode) getAttribute(key string) string {

func createBookExample() *TNode {
/*
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book category="cooking">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="children">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="web">
<title lang="en">XQuery Kick Start</title>
<author>James McGovern</author>
<author>Per Bothner</author>
<author>Kurt Cagle</author>
<author>James Linn</author>
<author>Vaidyanathan Nagarajan</author>
<year>2003</year>
<price>49.99</price>
</book>
<book category="web">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book category="cooking">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="children">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="web">
<title lang="en">XQuery Kick Start</title>
<author>James McGovern</author>
<author>Per Bothner</author>
<author>Kurt Cagle</author>
<author>James Linn</author>
<author>Vaidyanathan Nagarajan</author>
<year>2003</year>
<price>49.99</price>
</book>
<book category="web">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>
*/
type Element struct {
Data string
Expand Down Expand Up @@ -708,24 +790,24 @@ func createBookExample() *TNode {
// The example document from https://way2tutorial.com/xml/xpath-node-test.php
func createEmployeeExample() *TNode {
/*
<?xml version="1.0" standalone="yes"?>
<empinfo>
<employee id="1">
<name>Opal Kole</name>
<designation discipline="web" experience="3 year">Senior Engineer</designation>
<email>OpalKole@myemail.com</email>
</employee>
<employee id="2">
<name from="CA">Max Miller</name>
<designation discipline="DBA" experience="2 year">DBA Engineer</designation>
<email>maxmiller@email.com</email>
</employee>
<employee id="3">
<name>Beccaa Moss</name>
<designation discipline="appdev">Application Developer</designation>
<email>beccaamoss@email.com</email>
</employee>
</empinfo>
<?xml version="1.0" standalone="yes"?>
<empinfo>
<employee id="1">
<name>Opal Kole</name>
<designation discipline="web" experience="3 year">Senior Engineer</designation>
<email>OpalKole@myemail.com</email>
</employee>
<employee id="2">
<name from="CA">Max Miller</name>
<designation discipline="DBA" experience="2 year">DBA Engineer</designation>
<email>maxmiller@email.com</email>
</employee>
<employee id="3">
<name>Beccaa Moss</name>
<designation discipline="appdev">Application Developer</designation>
<email>beccaamoss@email.com</email>
</employee>
</empinfo>
*/

type Element struct {
Expand Down Expand Up @@ -808,25 +890,25 @@ func createHtmlExample() *TNode {
/*
<html lang="en">
<head>
<title>My page</title>
<meta name="language" content="en" />
<title>My page</title>
<meta name="language" content="en" />
</head>
<body>
<h2>Welcome to my page</h2>
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/about">About</a>
</li>
<li>
<a href="/account">Login</a>
</li>
<h2>Welcome to my page</h2>
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/about">About</a>
</li>
<li>
<a href="/account">Login</a>
</li>
<li></li>
</ul>
<p>This is the first paragraph.</p>
<!-- this is the end -->
</ul>
<p>This is the first paragraph.</p>
<!-- this is the end -->
</body>
</html>
*/
Expand Down