Implement and test one path constructor and join (#398)
Implement and test one path constructor and join
This commit is contained in:
parent
5a532e808e
commit
ea3c9c035e
@ -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))
|
||||||
|
}
|
||||||
|
|||||||
111
src/internal/path/path_test.go
Normal file
111
src/internal/path/path_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user