Simplify path struct (#647)

* Rewrite basic path logic to be simpler

Make basic path logic deal only with path elements instead of elements
and segments. Upper-layer logic can deal with elements.

Base path logic does not require a complete resource path as would be
seen by kopia, it just manages splitting/joining/escaping path elements.
Will have transformers to go from a basic path to a resource specific
path in a follow up.

Remove upper-layer logic for now to reduce load while reviewing as it
also changed slightly. Will be re-added in a follow up
This commit is contained in:
ashmrtn 2022-08-26 08:43:46 -07:00 committed by GitHub
parent c4e9046870
commit f24ad6ccbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 261 additions and 733 deletions

View File

@ -1,110 +0,0 @@
package path
import (
"strings"
"github.com/pkg/errors"
)
const (
emailCategory = "email"
)
var _ Path = &ExchangeMail{}
type ExchangeMail struct {
Base
}
// NewExchangeEmailPath creates and returns a new ExchangeEmailPath struct after
// verifying the path is properly escaped and contains information for the
// required segments. The provided segments and folder elements should not be
// escaped prior to calling this.
func NewExchangeMail(
tenant string,
user string,
folder []string,
item string,
) (*ExchangeMail, error) {
tmpFolder := strings.Join(folder, "")
if err := validateExchangeMailSegments(tenant, user, tmpFolder, item); err != nil {
return nil, err
}
p := newPath([][]string{
{tenant},
{emailCategory},
{user},
folder,
{item},
})
return &ExchangeMail{p}, nil
}
// NewExchangeMailFromEscapedSegments takes a series of already escaped segments
// representing the tenant, user, folder, and item validates them and returns a
// *ExchangeMail. The caller is expected to concatenate of all folders
// into a single string like `some/subfolder/structure`. Any special characters
// in the folder path need to be escaped.
func NewExchangeMailFromEscapedSegments(tenant, user, folder, item string) (*ExchangeMail, error) {
if err := validateExchangeMailSegments(tenant, user, folder, item); err != nil {
return nil, err
}
p, err := newPathFromEscapedSegments([]string{tenant, emailCategory, user, folder, item})
if err != nil {
return nil, err
}
return &ExchangeMail{p}, nil
}
func validateExchangeMailSegments(tenant, user, folder, item string) error {
if len(tenant) == 0 {
return errors.Wrap(errMissingSegment, "tenant")
}
if len(user) == 0 {
return errors.Wrap(errMissingSegment, "user")
}
if len(folder) == 0 {
return errors.Wrap(errMissingSegment, "mail folder")
}
if len(item) == 0 {
return errors.Wrap(errMissingSegment, "mail item")
}
return nil
}
// Tenant returns the tenant ID for the referenced email resource.
func (emp ExchangeMail) Tenant() string {
return emp.segment(0)
}
// Cateory returns an identifier noting this is a path for an email resource.
func (emp ExchangeMail) Category() string {
return emp.segment(1)
}
// User returns the user ID for the referenced email resource.
func (emp ExchangeMail) User() string {
return emp.segment(2)
}
// Folder returns the folder segment for the referenced email resource.
func (emp ExchangeMail) Folder() string {
return emp.segment(3)
}
func (emp ExchangeMail) FolderElements() []string {
return emp.unescapedSegmentElements(3)
}
// Mail returns the email ID for the referenced email resource.
func (emp ExchangeMail) Item() string {
return emp.segment(4)
}

View File

@ -1,162 +0,0 @@
package path_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/internal/path"
)
const (
tenant = "aTenant"
user = "aUser"
item = "anItem"
)
var (
// Purposely doesn't have characters that need escaping so it can be easily
// computed using strings.Join().
folder = []string{"some", "folder", "path"}
missingInfo = []struct {
name string
tenant string
user string
folder []string
item string
}{
{
name: "NoTenant",
tenant: "",
user: user,
folder: folder,
item: item,
},
{
name: "NoUser",
tenant: tenant,
user: "",
folder: folder,
item: item,
},
{
name: "NoFolder",
tenant: "",
user: user,
folder: nil,
item: item,
},
{
name: "EmptyFolder",
tenant: "",
user: user,
folder: []string{"", ""},
item: item,
},
{
name: "NoItem",
tenant: tenant,
user: user,
folder: folder,
item: "",
},
}
)
type ExchangeMailUnitSuite struct {
suite.Suite
}
func TestExchangeMailUnitSuite(t *testing.T) {
suite.Run(t, new(ExchangeMailUnitSuite))
}
func (suite *ExchangeMailUnitSuite) TestMissingInfoErrors() {
for _, test := range missingInfo {
suite.T().Run(test.name, func(t *testing.T) {
_, err := path.NewExchangeMail(
test.tenant, test.user, test.folder, test.item)
assert.Error(t, err)
})
}
}
func (suite *ExchangeMailUnitSuite) TestMissingInfoWithSegmentsErrors() {
for _, test := range missingInfo {
suite.T().Run(test.name, func(t *testing.T) {
folders := strings.Join(test.folder, "")
_, err := path.NewExchangeMailFromEscapedSegments(
test.tenant, test.user, folders, test.item)
assert.Error(t, err)
})
}
}
// Some simple escaping examples. Don't want to duplicate everything that is in
// the regular path.Base tests.
func (suite *ExchangeMailUnitSuite) TestNewExchangeMailFromRaw() {
t := suite.T()
localItem := `an\item`
em, err := path.NewExchangeMail(tenant, user, folder, localItem)
require.NoError(t, err)
assert.Equal(t, `an\\item`, em.Item())
}
func (suite *ExchangeMailUnitSuite) TestNewExchangeMailFromEscaped() {
t := suite.T()
localItem := `an\\item`
localFolder := strings.Join(folder, "/")
em, err := path.NewExchangeMailFromEscapedSegments(tenant, user, localFolder, localItem)
require.NoError(t, err)
assert.Equal(t, localItem, em.Item())
}
func (suite *ExchangeMailUnitSuite) TestNewExchangeMailFromEscaped_Errors() {
t := suite.T()
localItem := `an\item`
localFolder := strings.Join(folder, "/")
_, err := path.NewExchangeMailFromEscapedSegments(tenant, user, localFolder, localItem)
assert.Error(t, err)
}
type PopulatedExchangeMailUnitSuite struct {
suite.Suite
em *path.ExchangeMail
}
func TestPopulatedExchangeMailUnitSuite(t *testing.T) {
suite.Run(t, new(PopulatedExchangeMailUnitSuite))
}
func (suite *PopulatedExchangeMailUnitSuite) SetupTest() {
em, err := path.NewExchangeMail(tenant, user, folder, item)
require.NoError(suite.T(), err)
suite.em = em
}
func (suite *PopulatedExchangeMailUnitSuite) TestGetTenant() {
assert.Equal(suite.T(), tenant, suite.em.Tenant())
}
func (suite *PopulatedExchangeMailUnitSuite) TestGetUser() {
assert.Equal(suite.T(), user, suite.em.User())
}
func (suite *PopulatedExchangeMailUnitSuite) TestGetFolder() {
assert.Equal(suite.T(), strings.Join(folder, "/"), suite.em.Folder())
}
func (suite *PopulatedExchangeMailUnitSuite) TestGetItem() {
assert.Equal(suite.T(), item, suite.em.Item())
}

View File

@ -51,8 +51,6 @@ var charactersToEscape = map[rune]struct{}{
escapeCharacter: {}, escapeCharacter: {},
} }
var errMissingSegment = errors.New("missing required path segment")
// TODO(ashmrtn): Getting the category should either be through type-switches or // TODO(ashmrtn): Getting the category should either be through type-switches or
// through a function, but if it's a function it should re-use existing enums // through a function, but if it's a function it should re-use existing enums
// for resource types. // for resource types.
@ -67,121 +65,90 @@ type Path interface {
Item() string Item() string
} }
type Base struct { // Builder is a simple path representation that only tracks path elements. It
// Escaped path elements. // can join, escape, and unescape elements. Higher-level packages are expected
// to wrap this struct to build resource-speicific contexts (e.x. an
// ExchangeMailPath).
// Resource-specific paths allow access to more information like segments in the
// path. Builders that are turned into resource paths later on do not need to
// manually add prefixes for items that normally appear in the data layer (ex.
// tenant ID, service, user ID, etc).
type Builder struct {
// Unescaped version of elements.
elements []string 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 // UnescapeAndAppend creates a copy of this Builder and adds one or more already
// and returns a Base. Each element in the input will get escaped. // escaped path elements to the end of the new Builder. Elements are added in
// Example: [this, is\, a, path] will transform into [this, is\\, a, path]. // the order they are passed.
func newPath(segments [][]string) Base { func (pb Builder) UnescapeAndAppend(elements ...string) (*Builder, error) {
if len(segments) == 0 { res := &Builder{elements: make([]string, 0, len(pb.elements))}
return Base{} copy(res.elements, pb.elements)
if err := res.appendElements(true, elements); err != nil {
return nil, err
} }
res := Base{segmentIdx: make([]int, 0, len(segments))} return res, nil
idx := 0 }
for _, s := range segments {
sIdx := idx
for _, e := range s { // Append creates a copy of this Builder and adds the given elements them to the
if len(e) == 0 { // end of the new Builder. Elements are added in the order they are passed.
continue func (pb Builder) Append(elements ...string) *Builder {
} res := &Builder{elements: make([]string, len(pb.elements))}
copy(res.elements, pb.elements)
res.elements = append(res.elements, escapeElement(e)) // Unescaped elements can't fail validation.
idx++ //nolint:errcheck
} res.appendElements(false, elements)
if sIdx != idx {
res.segmentIdx = append(res.segmentIdx, sIdx)
}
}
return res return res
} }
// NewPathFromEscapedSegments takes already escaped segments of a path, verifies func (pb *Builder) appendElements(escaped bool, elements []string) error {
// the segments are escaped properly, and returns a new Base struct. If there is for _, e := range elements {
// an unescaped trailing '/' it is removed. This function is safe to use with if len(e) == 0 {
// escaped user input where each chunk is a segment. For example, the input
// [this, is\//a, path] will produce:
// segments: [this, is\//a, path]
// elements: [this, is\/, a, path].
func newPathFromEscapedSegments(segments []string) (Base, error) {
b := Base{}
if err := validateSegments(segments); err != nil {
return b, errors.Wrap(err, "validating escaped path")
}
// Make a copy of the input so we don't modify the original slice.
tmpSegments := make([]string, len(segments))
copy(tmpSegments, segments)
tmpSegments[len(tmpSegments)-1] = trimTrailingSlash(tmpSegments[len(tmpSegments)-1])
for _, s := range tmpSegments {
newElems := split(s)
if len(newElems) == 0 {
continue continue
} }
b.segmentIdx = append(b.segmentIdx, len(b.elements)) tmp := e
b.elements = append(b.elements, newElems...)
if escaped {
tmp = trimTrailingSlash(tmp)
// If tmp was just the path separator then it will be empty now.
if len(tmp) == 0 {
continue
}
if err := validateEscapedElement(tmp); err != nil {
return err
}
tmp = unescape(tmp)
}
pb.elements = append(pb.elements, tmp)
} }
return b, nil
}
// String returns a string that contains all path segments joined
// together. Elements of the path that need escaping will be escaped.
func (b Base) String() string {
return join(b.elements)
}
// segment returns the nth segment of the path. Path segment indices are
// 0-based. As this function is used exclusively by wrappers of path, it does no
// bounds checking. Callers are expected to have validated the number of
// 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
// 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
// has also been transformed such that it contains no characters outside the set
// of acceptable file system path characters.
func (b Base) TransformedSegments() []string {
return nil return nil
} }
// String returns a string that contains all path elements joined together.
// Elements of the path that need escaping are escaped.
func (pb Builder) String() string {
escaped := make([]string, 0, len(pb.elements))
for _, e := range pb.elements {
escaped = append(escaped, escapeElement(e))
}
return join(escaped)
}
func (pb Builder) join(start, end int) string {
return join(pb.elements[start:end])
}
// escapeElement takes a single path element and escapes all characters that // escapeElement takes a single path element and escapes all characters that
// require an escape sequence. If there are no characters that need escaping, // require an escape sequence. If there are no characters that need escaping,
// the input is returned unchanged. // the input is returned unchanged.
@ -198,13 +165,14 @@ func escapeElement(element string) string {
return element return element
} }
startIdx := 0
b := strings.Builder{} b := strings.Builder{}
b.Grow(len(element) + len(escapeIdx)) b.Grow(len(element) + len(escapeIdx))
startIdx := 0
for _, idx := range escapeIdx { for _, idx := range escapeIdx {
b.WriteString(element[startIdx:idx]) b.WriteString(element[startIdx:idx])
b.WriteRune(escapeCharacter) b.WriteRune(escapeCharacter)
startIdx = idx startIdx = idx
} }
@ -220,9 +188,9 @@ func escapeElement(element string) string {
// separators will result in an ambiguous or incorrect segment. // separators will result in an ambiguous or incorrect segment.
func unescape(element string) string { func unescape(element string) string {
b := strings.Builder{} b := strings.Builder{}
startIdx := 0 startIdx := 0
prevWasEscape := false prevWasEscape := false
for i, c := range element { for i, c := range element {
if c != escapeCharacter || prevWasEscape { if c != escapeCharacter || prevWasEscape {
prevWasEscape = false prevWasEscape = false
@ -240,33 +208,37 @@ func unescape(element string) string {
return b.String() return b.String()
} }
// validateSegments takes a slice of segments and ensures that escaped // validateEscapedElement takes an escaped element that has had trailing
// sequences match the set of characters that need escaping and that there // separators trimmed and ensures that no characters requiring escaping are
// aren't hanging escape characters at the end of a segment. // unescaped and that no escape characters are combined with characters that
func validateSegments(segments []string) error { // don't need escaping.
for _, segment := range segments { func validateEscapedElement(element string) error {
prevWasEscape := false prevWasEscape := false
for _, c := range segment { for _, c := range element {
switch prevWasEscape { switch prevWasEscape {
case true: case true:
prevWasEscape = false prevWasEscape = false
if _, ok := charactersToEscape[c]; !ok { if _, ok := charactersToEscape[c]; !ok {
return errors.Errorf( return errors.Errorf(
"bad escape sequence in path: '%c%c'", escapeCharacter, c) "bad escape sequence in path: '%c%c'", escapeCharacter, c)
} }
case false: case false:
if c == escapeCharacter { if c == escapeCharacter {
prevWasEscape = true prevWasEscape = true
} continue
}
if _, ok := charactersToEscape[c]; ok {
return errors.Errorf("unescaped '%c' in path", c)
} }
} }
}
if prevWasEscape { if prevWasEscape {
return errors.New("trailing escape character in segment") return errors.New("trailing escape character")
}
} }
return nil return nil
@ -284,6 +256,7 @@ func trimTrailingSlash(element string) string {
} }
numSlashes := 0 numSlashes := 0
for i := lastIdx - 1; i >= 0; i-- { for i := lastIdx - 1; i >= 0; i-- {
if element[i] != escapeCharacter { if element[i] != escapeCharacter {
break break
@ -306,52 +279,3 @@ func join(elements []string) string {
// '\' according to the escaping rules. // '\' according to the escaping rules.
return strings.Join(elements, string(pathSeparator)) return strings.Join(elements, string(pathSeparator))
} }
// split returns a slice of path elements for the given segment when the segment
// is split on the path separator according to the escaping rules.
func split(segment string) []string {
res := make([]string, 0)
numEscapes := 0
startIdx := 0
// Start with true to ignore leading separator.
prevWasSeparator := true
for i, c := range segment {
if c == escapeCharacter {
numEscapes++
prevWasSeparator = false
continue
}
if c != pathSeparator {
prevWasSeparator = false
numEscapes = 0
continue
}
// Remaining is just path separator handling.
if numEscapes%2 != 0 {
// This is an escaped separator.
prevWasSeparator = false
numEscapes = 0
continue
}
// Ignore leading separator characters and don't add elements that would
// be empty.
if !prevWasSeparator {
res = append(res, segment[startIdx:i])
}
// We don't want to include the path separator in the result.
startIdx = i + 1
prevWasSeparator = true
numEscapes = 0
}
// Add the final segment because the loop above won't catch it. There should
// be no trailing separator character, but do a bounds check to be safe.
res = append(res, segment[startIdx:])
return res
}

View File

@ -2,7 +2,6 @@ package path
import ( import (
"fmt" "fmt"
"reflect"
"strings" "strings"
"testing" "testing"
@ -11,138 +10,185 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
var basicInputs = []struct { type testData struct {
name string name string
input [][]string input []string
expectedString string expectedString string
expectedEscapedSegments []string }
expectedUnescapedElements [][]string
}{ // Test cases that are the same with and without escaping by the
// system-under-test.
var genericCases = []testData{
{ {
name: "SimplePath", name: "SimplePath",
input: [][]string{ input: []string{
{`this`},
{`is`},
{`a`},
{`path`},
},
expectedString: "this/is/a/path",
expectedEscapedSegments: []string{
`this`, `this`,
`is`, `is`,
`a`, `a`,
`path`, `path`,
}, },
expectedUnescapedElements: [][]string{ expectedString: "this/is/a/path",
{`this`},
{`is`},
{`a`},
{`path`},
},
}, },
{
name: "EmptyElement",
input: []string{
`this`,
`is`,
``,
`a`,
`path`,
},
expectedString: `this/is/a/path`,
},
{
name: "EmptyInput",
expectedString: "",
},
}
// Inputs that should be escaped.
var basicUnescapedInputs = []testData{
{ {
name: "EscapeSeparator", name: "EscapeSeparator",
input: [][]string{ input: []string{
{`this`}, `this`,
{`is/a`}, `is/a`,
{`path`}, `path`,
}, },
expectedString: `this/is\/a/path`, expectedString: `this/is\/a/path`,
expectedEscapedSegments: []string{ },
{
name: "EscapeEscapeChar",
input: []string{
`this`,
`is\`,
`a`,
`path`,
},
expectedString: `this/is\\/a/path`,
},
{
name: "EscapeEscapeAndSeparator",
input: []string{
`this`, `this`,
`is\/a`, `is\/a`,
`path`, `path`,
}, },
expectedUnescapedElements: [][]string{ expectedString: `this/is\\\/a/path`,
{`this`},
{`is/a`},
{`path`},
},
}, },
{ {
name: "EscapeEscapeChar", name: "SeparatorAtEndOfElement",
input: [][]string{ input: []string{
{`this`}, `this`,
{`is\`}, `is/`,
{`a`}, `a`,
{`path`}, `path`,
}, },
expectedString: `this/is\\/a/path`, expectedString: `this/is\//a/path`,
expectedEscapedSegments: []string{ },
{
name: "SeparatorAtEndOfPath",
input: []string{
`this`,
`is`,
`a`,
`path/`,
},
expectedString: `this/is/a/path\/`,
},
}
// Inputs that are already escaped.
var basicEscapedInputs = []testData{
{
name: "EscapedSeparator",
input: []string{
`this`,
`is\/a`,
`path`,
},
expectedString: `this/is\/a/path`,
},
{
name: "EscapedEscapeChar",
input: []string{
`this`, `this`,
`is\\`, `is\\`,
`a`, `a`,
`path`, `path`,
}, },
expectedUnescapedElements: [][]string{ expectedString: `this/is\\/a/path`,
{`this`},
{`is\`},
{`a`},
{`path`},
},
}, },
{ {
name: "EscapeEscapeAndSeparator", name: "EscapedEscapeAndSeparator",
input: [][]string{ input: []string{
{`this`},
{`is\/a`},
{`path`},
},
expectedString: `this/is\\\/a/path`,
expectedEscapedSegments: []string{
`this`, `this`,
`is\\\/a`, `is\\\/a`,
`path`, `path`,
}, },
expectedUnescapedElements: [][]string{ expectedString: `this/is\\\/a/path`,
{`this`},
{`is\/a`},
{`path`},
},
}, },
{ {
name: "SeparatorAtEndOfElement", name: "EscapedSeparatorAtEndOfElement",
input: [][]string{ input: []string{
{`this`},
{`is/`},
{`a`},
{`path`},
},
expectedString: `this/is\//a/path`,
expectedEscapedSegments: []string{
`this`, `this`,
`is\/`, `is\/`,
`a`, `a`,
`path`, `path`,
}, },
expectedUnescapedElements: [][]string{ expectedString: `this/is\//a/path`,
{`this`},
{`is/`},
{`a`},
{`path`},
},
}, },
{ {
name: "SeparatorAtEndOfPath", name: "EscapedSeparatorAtEndOfPath",
input: [][]string{ input: []string{
{`this`},
{`is`},
{`a`},
{`path/`},
},
expectedString: `this/is/a/path\/`,
expectedEscapedSegments: []string{
`this`, `this`,
`is`, `is`,
`a`, `a`,
`path\/`, `path\/`,
}, },
expectedUnescapedElements: [][]string{ expectedString: `this/is/a/path\/`,
{`this`}, },
{`is`}, {
{`a`}, name: "ElementOfSeparator",
{`path/`}, input: []string{
`this`,
`is`,
`/`,
`a`,
`path`,
}, },
expectedString: `this/is/a/path`,
},
{
name: "TrailingElementSeparator",
input: []string{
`this`,
`is`,
`a/`,
`path`,
},
expectedString: `this/is/a/path`,
},
{
name: "TrailingSeparatorAtEnd",
input: []string{
`this`,
`is`,
`a`,
`path/`,
},
expectedString: `this/is/a/path`,
},
{
name: "TrailingSeparatorWithEmptyElementAtEnd",
input: []string{
`this`,
`is`,
`a`,
`path/`,
``,
},
expectedString: `this/is/a/path`,
}, },
} }
@ -154,214 +200,38 @@ func TestPathUnitSuite(t *testing.T) {
suite.Run(t, new(PathUnitSuite)) suite.Run(t, new(PathUnitSuite))
} }
func (suite *PathUnitSuite) TestPathEscapingAndSegments() { func (suite *PathUnitSuite) TestAppend() {
for _, test := range basicInputs { table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
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 := Builder{}.Append(test.input...)
assert.Equal(t, test.expectedString, p.String()) 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() { func (suite *PathUnitSuite) TestUnescapeAndAppend() {
table := []struct { table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
name string
input [][]string
expected string
}{
{
name: "EmptyInternalElement",
input: [][]string{
{`this`},
{`is`},
{""},
{`a`},
{`path`},
},
expected: "this/is/a/path",
},
{
name: "EmptyInternalElement2",
input: [][]string{
{`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, err := Builder{}.UnescapeAndAppend(test.input...)
idx := 0
for i := 0; i < len(test.input); i++ {
if i == 2 {
continue
}
assert.NotPanics(t, func() {
_ = p.segment(idx)
})
idx++
}
assert.Panics(t, func() {
_ = p.segment(len(test.input))
})
})
}
}
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
input []string
expected string
expectedSegments []string
}{
{
name: "SimplePath",
input: []string{`this`, `is/a`, `path`},
expected: "this/is/a/path",
expectedSegments: []string{`this`, `is/a`, `path`},
},
{
name: "EscapeSeparator",
input: []string{`this`, `is\/a`, `path`},
expected: `this/is\/a/path`,
expectedSegments: []string{`this`, `is\/a`, `path`},
},
{
name: "EscapeEscapeChar",
input: []string{`this`, `is\\/a`, `path`},
expected: `this/is\\/a/path`,
expectedSegments: []string{`this`, `is\\/a`, `path`},
},
{
name: "EmptyInternalElement",
input: []string{`this`, `is//a`, `path`},
expected: "this/is/a/path",
expectedSegments: []string{`this`, `is/a`, `path`},
},
{
name: "SeparatorAtEndOfElement",
input: []string{`this`, `is\//a`, `path`},
expected: `this/is\//a/path`,
expectedSegments: []string{`this`, `is\//a`, `path`},
},
{
name: "SeparatorAtEndOfPath",
input: []string{`this`, `is/a`, `path\/`},
expected: `this/is/a/path\/`,
expectedSegments: []string{`this`, `is/a`, `path\/`},
},
{
name: "TrailingSeparator",
input: []string{`this`, `is/a`, `path/`},
expected: `this/is/a/path`,
expectedSegments: []string{`this`, `is/a`, `path`},
},
{
name: "TrailingSeparator2",
input: []string{`this`, `is/a`, `path\\\\/`},
expected: `this/is/a/path\\\\`,
expectedSegments: []string{`this`, `is/a`, `path\\\\`},
},
{
name: "ManyEscapesNotSeparator",
input: []string{`this`, `is\\\\/a`, `path/`},
expected: `this/is\\\\/a/path`,
expectedSegments: []string{`this`, `is\\\\/a`, `path`},
},
{
name: "ManyEscapesAndSeparator",
input: []string{`this`, `is\\\/a`, `path`},
expected: `this/is\\\/a/path`,
expectedSegments: []string{`this`, `is\\\/a`, `path`},
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
p, err := newPathFromEscapedSegments(test.input)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, test.expected, p.String())
for i, s := range test.expectedSegments { assert.Equal(t, test.expectedString, p.String())
segment := ""
require.NotPanics(t, func() {
segment = p.segment(i)
})
assert.Equal(t, s, segment)
}
}) })
} }
} }
func (suite *PathUnitSuite) TestEscapedFailure() { func (suite *PathUnitSuite) TestEscapedFailure() {
target := "i_s/a" target := "i_s"
for c := range charactersToEscape { for c := range charactersToEscape {
if c == pathSeparator { suite.T().Run(fmt.Sprintf("Unescaped-%c", c), func(t *testing.T) {
// Extra path separators in the path will just lead to more segments, not tmp := strings.ReplaceAll(target, "_", string(c))
// a validation error.
continue
}
tmp := strings.ReplaceAll(target, "_", string(c)) _, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
basePath := []string{"this", tmp, "path"} assert.Error(t, err, "path with unescaped %s did not error", string(c))
_, err := newPathFromEscapedSegments(basePath) })
assert.Error(suite.T(), err, "path with unescaped %s did not error", string(c))
} }
} }
@ -370,16 +240,18 @@ func (suite *PathUnitSuite) TestBadEscapeSequenceErrors() {
notEscapes := []rune{'a', 'b', '#', '%'} notEscapes := []rune{'a', 'b', '#', '%'}
for _, c := range notEscapes { for _, c := range notEscapes {
tmp := strings.ReplaceAll(target, "_", string(c)) suite.T().Run(fmt.Sprintf("Escaped-%c", c), func(t *testing.T) {
basePath := []string{"this", tmp, "path"} tmp := strings.ReplaceAll(target, "_", string(c))
_, err := newPathFromEscapedSegments(basePath)
assert.Error( _, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
suite.T(), assert.Error(
err, t,
"path with bad escape sequence %c%c did not error", err,
escapeCharacter, "path with bad escape sequence %c%c did not error",
c, escapeCharacter,
) c,
)
})
} }
} }
@ -387,13 +259,17 @@ func (suite *PathUnitSuite) TestTrailingEscapeChar() {
base := []string{"this", "is", "a", "path"} base := []string{"this", "is", "a", "path"}
for i := 0; i < len(base); i++ { for i := 0; i < len(base); i++ {
suite.T().Run(fmt.Sprintf("Segment%v", i), func(t *testing.T) { suite.T().Run(fmt.Sprintf("Element%v", i), func(t *testing.T) {
path := make([]string, len(base)) path := make([]string, len(base))
copy(path, base) copy(path, base)
path[i] = path[i] + string(escapeCharacter) path[i] = path[i] + string(escapeCharacter)
_, err := newPathFromEscapedSegments(path) _, err := Builder{}.UnescapeAndAppend(path...)
assert.Error(suite.T(), err) assert.Error(
t,
err,
"path with trailing escape character did not error",
)
}) })
} }
} }