diff --git a/src/internal/path/path.go b/src/internal/path/path.go index 06a56fab1..7bd5d49db 100644 --- a/src/internal/path/path.go +++ b/src/internal/path/path.go @@ -88,7 +88,7 @@ func newPath(segments [][]string) Base { res := Base{segmentIdx: make([]int, 0, len(segments))} idx := 0 for _, s := range segments { - res.segmentIdx = append(res.segmentIdx, idx) + sIdx := idx for _, e := range s { if len(e) == 0 { @@ -98,6 +98,10 @@ func newPath(segments [][]string) Base { res.elements = append(res.elements, escapeElement(e)) idx++ } + + if sIdx != idx { + res.segmentIdx = append(res.segmentIdx, sIdx) + } } return res @@ -155,9 +159,23 @@ func (b Base) segment(n int) string { } // unescapedSegmentElements returns the unescaped version of the elements that -// comprise the requested segment. -func (p Base) unescapedSegmentElements(n int) []string { - return nil +// comprise the requested segment. Path segment indices are 0-based. +func (b Base) unescapedSegmentElements(n int) []string { + 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 @@ -167,6 +185,9 @@ func (b Base) TransformedSegments() []string { 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 { escapeIdx := make([]int, 0) @@ -196,6 +217,32 @@ func escapeElement(element string) 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 // sequences match the set of characters that need escaping and that there // aren't hanging escape characters at the end of a segment. diff --git a/src/internal/path/path_test.go b/src/internal/path/path_test.go index 075e4116a..74efb8dcb 100644 --- a/src/internal/path/path_test.go +++ b/src/internal/path/path_test.go @@ -2,6 +2,7 @@ package path import ( "fmt" + "reflect" "strings" "testing" @@ -10,6 +11,141 @@ import ( "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 { suite.Suite } @@ -19,49 +155,33 @@ func TestPathUnitSuite(t *testing.T) { } 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 { name string input [][]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", input: [][]string{ @@ -74,36 +194,43 @@ func (suite *PathUnitSuite) TestPathEscapingAndSegments() { expected: "this/is/a/path", }, { - name: "SeparatorAtEndOfElement", - input: [][]string{ - {`this`}, - {`is/`}, - {`a`}, - {`path`}, - }, - expected: `this/is\//a/path`, - }, - { - name: "SeparatorAtEndOfPath", + name: "EmptyInternalElement2", input: [][]string{ {`this`}, {`is`}, + {"", "", ""}, {`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 { suite.T().Run(test.name, func(t *testing.T) { p := newPath(test.input) - assert.Equal(t, test.expected, p.String()) + idx := 0 for i := 0; i < len(test.input); i++ { + if i == 2 { + continue + } + assert.NotPanics(t, func() { - _ = p.segment(i) + _ = p.segment(idx) }) + idx++ } 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() { table := []struct { name string