Add function to get unescaped elements for a path segment (#422)

* Function and tests for returned unescaped elements

* Regression test and fix for empty segment bug

If a raw segment had no elements that had length > 0 or just didn't have
any elements it would still create a segment, throwing everything else
off. Explicitly test for that now.
This commit is contained in:
ashmrtn 2022-07-27 12:53:15 -07:00 committed by GitHub
parent 5fe9cc51aa
commit e50728c0d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 252 additions and 57 deletions

View File

@ -88,7 +88,7 @@ func newPath(segments [][]string) Base {
res := Base{segmentIdx: make([]int, 0, len(segments))} res := Base{segmentIdx: make([]int, 0, len(segments))}
idx := 0 idx := 0
for _, s := range segments { for _, s := range segments {
res.segmentIdx = append(res.segmentIdx, idx) sIdx := idx
for _, e := range s { for _, e := range s {
if len(e) == 0 { if len(e) == 0 {
@ -98,6 +98,10 @@ func newPath(segments [][]string) Base {
res.elements = append(res.elements, escapeElement(e)) res.elements = append(res.elements, escapeElement(e))
idx++ idx++
} }
if sIdx != idx {
res.segmentIdx = append(res.segmentIdx, sIdx)
}
} }
return res return res
@ -155,9 +159,23 @@ func (b Base) segment(n int) string {
} }
// unescapedSegmentElements returns the unescaped version of the elements that // unescapedSegmentElements returns the unescaped version of the elements that
// comprise the requested segment. // comprise the requested segment. Path segment indices are 0-based.
func (p Base) unescapedSegmentElements(n int) []string { func (b Base) unescapedSegmentElements(n int) []string {
return nil var elements []string
if n == len(b.segmentIdx)-1 {
elements = b.elements[b.segmentIdx[n]:]
} else {
elements = b.elements[b.segmentIdx[n]:b.segmentIdx[n+1]]
}
res := make([]string, 0, len(elements))
for _, e := range elements {
res = append(res, unescape(e))
}
return res
} }
// TransformedSegments returns a slice of the path segments where each segments // TransformedSegments returns a slice of the path segments where each segments
@ -167,6 +185,9 @@ func (b Base) TransformedSegments() []string {
return nil return nil
} }
// escapeElement takes a single path element and escapes all characters that
// require an escape sequence. If there are no characters that need escaping,
// the input is returned unchanged.
func escapeElement(element string) string { func escapeElement(element string) string {
escapeIdx := make([]int, 0) escapeIdx := make([]int, 0)
@ -196,6 +217,32 @@ func escapeElement(element string) string {
return b.String() return b.String()
} }
// unescape returns the given element and converts it into a "raw"
// element that does not have escape characters before characters that need
// escaping. Using this function on segments that contain escaped path
// separators will result in an ambiguous or incorrect segment.
func unescape(element string) string {
b := strings.Builder{}
startIdx := 0
prevWasEscape := false
for i, c := range element {
if c != escapeCharacter || prevWasEscape {
prevWasEscape = false
continue
}
// This is an escape character, remove it from the output.
b.WriteString(element[startIdx:i])
startIdx = i + 1
prevWasEscape = true
}
b.WriteString(element[startIdx:])
return b.String()
}
// validateSegments takes a slice of segments and ensures that escaped // validateSegments takes a slice of segments and ensures that escaped
// sequences match the set of characters that need escaping and that there // sequences match the set of characters that need escaping and that there
// aren't hanging escape characters at the end of a segment. // aren't hanging escape characters at the end of a segment.

View File

@ -2,6 +2,7 @@ package path
import ( import (
"fmt" "fmt"
"reflect"
"strings" "strings"
"testing" "testing"
@ -10,6 +11,141 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
var basicInputs = []struct {
name string
input [][]string
expectedString string
expectedEscapedSegments []string
expectedUnescapedElements [][]string
}{
{
name: "SimplePath",
input: [][]string{
{`this`},
{`is`},
{`a`},
{`path`},
},
expectedString: "this/is/a/path",
expectedEscapedSegments: []string{
`this`,
`is`,
`a`,
`path`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is`},
{`a`},
{`path`},
},
},
{
name: "EscapeSeparator",
input: [][]string{
{`this`},
{`is/a`},
{`path`},
},
expectedString: `this/is\/a/path`,
expectedEscapedSegments: []string{
`this`,
`is\/a`,
`path`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is/a`},
{`path`},
},
},
{
name: "EscapeEscapeChar",
input: [][]string{
{`this`},
{`is\`},
{`a`},
{`path`},
},
expectedString: `this/is\\/a/path`,
expectedEscapedSegments: []string{
`this`,
`is\\`,
`a`,
`path`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is\`},
{`a`},
{`path`},
},
},
{
name: "EscapeEscapeAndSeparator",
input: [][]string{
{`this`},
{`is\/a`},
{`path`},
},
expectedString: `this/is\\\/a/path`,
expectedEscapedSegments: []string{
`this`,
`is\\\/a`,
`path`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is\/a`},
{`path`},
},
},
{
name: "SeparatorAtEndOfElement",
input: [][]string{
{`this`},
{`is/`},
{`a`},
{`path`},
},
expectedString: `this/is\//a/path`,
expectedEscapedSegments: []string{
`this`,
`is\/`,
`a`,
`path`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is/`},
{`a`},
{`path`},
},
},
{
name: "SeparatorAtEndOfPath",
input: [][]string{
{`this`},
{`is`},
{`a`},
{`path/`},
},
expectedString: `this/is/a/path\/`,
expectedEscapedSegments: []string{
`this`,
`is`,
`a`,
`path\/`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is`},
{`a`},
{`path/`},
},
},
}
type PathUnitSuite struct { type PathUnitSuite struct {
suite.Suite suite.Suite
} }
@ -19,49 +155,33 @@ func TestPathUnitSuite(t *testing.T) {
} }
func (suite *PathUnitSuite) TestPathEscapingAndSegments() { func (suite *PathUnitSuite) TestPathEscapingAndSegments() {
for _, test := range basicInputs {
suite.T().Run(test.name, func(t *testing.T) {
p := newPath(test.input)
assert.Equal(t, test.expectedString, p.String())
for i, s := range test.expectedEscapedSegments {
segment := ""
assert.NotPanics(t, func() {
segment = p.segment(i)
})
assert.Equal(t, s, segment)
}
assert.Panics(t, func() {
_ = p.segment(len(test.input))
})
})
}
}
func (suite *PathUnitSuite) TestPathEscapingAndSegments_EmpytElements() {
table := []struct { table := []struct {
name string name string
input [][]string input [][]string
expected string expected string
}{ }{
{
name: "SimplePath",
input: [][]string{
{`this`},
{`is`},
{`a`},
{`path`},
},
expected: "this/is/a/path",
},
{
name: "EscapeSeparator",
input: [][]string{
{`this`},
{`is/a`},
{`path`},
},
expected: `this/is\/a/path`,
},
{
name: "EscapeEscapeChar",
input: [][]string{
{`this`},
{`is\`},
{`a`},
{`path`},
},
expected: `this/is\\/a/path`,
},
{
name: "EscapeEscapeAndSeparator",
input: [][]string{
{`this`},
{`is\/a`},
{`path`},
},
expected: `this/is\\\/a/path`,
},
{ {
name: "EmptyInternalElement", name: "EmptyInternalElement",
input: [][]string{ input: [][]string{
@ -74,36 +194,43 @@ func (suite *PathUnitSuite) TestPathEscapingAndSegments() {
expected: "this/is/a/path", expected: "this/is/a/path",
}, },
{ {
name: "SeparatorAtEndOfElement", name: "EmptyInternalElement2",
input: [][]string{
{`this`},
{`is/`},
{`a`},
{`path`},
},
expected: `this/is\//a/path`,
},
{
name: "SeparatorAtEndOfPath",
input: [][]string{ input: [][]string{
{`this`}, {`this`},
{`is`}, {`is`},
{"", "", ""},
{`a`}, {`a`},
{`path/`}, {`path`},
}, },
expected: `this/is/a/path\/`, expected: "this/is/a/path",
},
{
name: "EmptyInternalElement3",
input: [][]string{
{`this`},
{`is`},
{},
{`a`},
{`path`},
},
expected: "this/is/a/path",
}, },
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
p := newPath(test.input) p := newPath(test.input)
assert.Equal(t, test.expected, p.String())
idx := 0
for i := 0; i < len(test.input); i++ { for i := 0; i < len(test.input); i++ {
if i == 2 {
continue
}
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
_ = p.segment(i) _ = p.segment(idx)
}) })
idx++
} }
assert.Panics(t, func() { assert.Panics(t, func() {
@ -113,6 +240,27 @@ func (suite *PathUnitSuite) TestPathEscapingAndSegments() {
} }
} }
func (suite *PathUnitSuite) TestUnescapedSegmentElements() {
for _, test := range basicInputs {
suite.T().Run(test.name, func(t *testing.T) {
p := newPath(test.input)
for i, s := range test.expectedUnescapedElements {
elements := []string{}
require.NotPanics(t, func() {
elements = p.unescapedSegmentElements(i)
})
assert.True(t, reflect.DeepEqual(s, elements))
}
assert.Panics(t, func() {
_ = p.unescapedSegmentElements(len(test.input))
})
})
}
}
func (suite *PathUnitSuite) TestPathSplitsEscapedPath() { func (suite *PathUnitSuite) TestPathSplitsEscapedPath() {
table := []struct { table := []struct {
name string name string