Implement and test one path constructor and join (#398)

Implement and test one path constructor and join
This commit is contained in:
ashmrtn 2022-07-25 13:31:09 -07:00 committed by GitHub
parent 5a532e808e
commit ea3c9c035e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 199 additions and 8 deletions

View File

@ -37,7 +37,21 @@
package path package path
import ( import (
"errors" "strings"
"github.com/pkg/errors"
)
const (
escapeCharacter = '\\'
pathSeparator = '/'
)
var (
charactersToEscape = map[rune]struct{}{
pathSeparator: {},
escapeCharacter: {},
}
) )
var errMissingSegment = errors.New("missing required path segment") var errMissingSegment = errors.New("missing required path segment")
@ -57,14 +71,37 @@ type Path interface {
} }
type Base struct { type Base struct {
// Escaped path elements.
elements []string
// Contains starting index in elements of each segment.
segmentIdx []int
} }
// newPath takes a path that is broken into segments and elements in the segment // newPath takes a path that is broken into segments and elements in the segment
// and returns a Base. Each element in the input is escaped. // and returns a Base. Each element in the input is escaped.
func newPath(segments [][]string) Base { func newPath(segments [][]string) Base {
if len(segments) == 0 {
return Base{} return Base{}
} }
res := Base{segmentIdx: make([]int, 0, len(segments))}
idx := 0
for _, s := range segments {
res.segmentIdx = append(res.segmentIdx, idx)
for _, e := range s {
if len(e) == 0 {
continue
}
res.elements = append(res.elements, escapeElement(e))
idx++
}
}
return res
}
// NewPathFromEscapedSegments takes already escaped segments of a path, verifies // NewPathFromEscapedSegments takes already escaped segments of a path, verifies
// the segments are escaped properly, and returns a new Base struct. If there is // the segments are escaped properly, and returns a new Base struct. If there is
// an unescaped trailing '/' it is removed. // an unescaped trailing '/' it is removed.
@ -74,14 +111,20 @@ func newPathFromEscapedSegments(segments []string) (Base, error) {
// String returns a string that contains all path segments joined // String returns a string that contains all path segments joined
// together. Elements of the path that need escaping will be escaped. // together. Elements of the path that need escaping will be escaped.
func (p Base) String() string { func (b Base) String() string {
return "" return join(b.elements)
} }
// segment returns the nth segment of the path. Path segment indices are // segment returns the nth segment of the path. Path segment indices are
// 0-based. // 0-based. As this function is used exclusively by wrappers of path, it does no
func (p Base) segment(n int) string { // bounds checking. Callers are expected to have validated the number of
return "" // segments when making the path.
func (b Base) segment(n int) string {
if n == len(b.segmentIdx)-1 {
return join(b.elements[b.segmentIdx[n]:])
}
return join(b.elements[b.segmentIdx[n]:b.segmentIdx[n+1]])
} }
// unescapedSegmentElements returns the unescaped version of the elements that // unescapedSegmentElements returns the unescaped version of the elements that
@ -93,6 +136,43 @@ func (p Base) unescapedSegmentElements(n int) []string {
// TransformedSegments returns a slice of the path segments where each segments // TransformedSegments returns a slice of the path segments where each segments
// has also been transformed such that it contains no characters outside the set // has also been transformed such that it contains no characters outside the set
// of acceptable file system path characters. // of acceptable file system path characters.
func (p Base) TransformedSegments() []string { func (b Base) TransformedSegments() []string {
return nil return nil
} }
func escapeElement(element string) string {
escapeIdx := make([]int, 0)
for i, c := range element {
if _, ok := charactersToEscape[c]; ok {
escapeIdx = append(escapeIdx, i)
}
}
if len(escapeIdx) == 0 {
return element
}
b := strings.Builder{}
b.Grow(len(element) + len(escapeIdx))
startIdx := 0
for _, idx := range escapeIdx {
b.WriteString(element[startIdx:idx])
b.WriteRune(escapeCharacter)
startIdx = idx
}
// Add the end of the element after the last escape character.
b.WriteString(element[startIdx:])
return b.String()
}
// join returns a string containing the given elements joined by the path
// separator '/'.
func join(elements []string) string {
// Have to use strings because path package does not handle escaped '/' and
// '\' according to the escaping rules.
return strings.Join(elements, string(pathSeparator))
}

View File

@ -0,0 +1,111 @@
package path
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type PathUnitSuite struct {
suite.Suite
}
func TestPathUnitSuite(t *testing.T) {
suite.Run(t, new(PathUnitSuite))
}
func (suite *PathUnitSuite) TestPathEscapingAndSegments() {
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{
{`this`},
{`is`},
{""},
{`a`},
{`path`},
},
expected: "this/is/a/path",
},
{
name: "SeparatorAtEndOfElement",
input: [][]string{
{`this`},
{`is/`},
{`a`},
{`path`},
},
expected: `this/is\//a/path`,
},
{
name: "SeparatorAtEndOfPath",
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())
for i := 0; i < len(test.input); i++ {
assert.NotPanics(t, func() {
_ = p.segment(i)
})
}
assert.Panics(t, func() {
_ = p.segment(len(test.input))
})
})
}
}