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: {},
}
var errMissingSegment = errors.New("missing required path segment")
// 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
// for resource types.
@ -67,121 +65,90 @@ type Path interface {
Item() string
}
type Base struct {
// Escaped path elements.
// Builder is a simple path representation that only tracks path elements. It
// 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
// Contains starting index in elements of each segment.
segmentIdx []int
}
// newPath takes a path that is broken into segments and elements in the segment
// and returns a Base. Each element in the input will get escaped.
// Example: [this, is\, a, path] will transform into [this, is\\, a, path].
func newPath(segments [][]string) Base {
if len(segments) == 0 {
return Base{}
// UnescapeAndAppend creates a copy of this Builder and adds one or more already
// escaped path elements to the end of the new Builder. Elements are added in
// the order they are passed.
func (pb Builder) UnescapeAndAppend(elements ...string) (*Builder, error) {
res := &Builder{elements: make([]string, 0, len(pb.elements))}
copy(res.elements, pb.elements)
if err := res.appendElements(true, elements); err != nil {
return nil, err
}
res := Base{segmentIdx: make([]int, 0, len(segments))}
idx := 0
for _, s := range segments {
sIdx := idx
return res, nil
}
for _, e := range s {
if len(e) == 0 {
continue
}
// Append creates a copy of this Builder and adds the given elements them to the
// end of the new Builder. Elements are added in the order they are passed.
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))
idx++
}
if sIdx != idx {
res.segmentIdx = append(res.segmentIdx, sIdx)
}
}
// Unescaped elements can't fail validation.
//nolint:errcheck
res.appendElements(false, elements)
return res
}
// NewPathFromEscapedSegments takes already escaped segments of a path, verifies
// the segments are escaped properly, and returns a new Base struct. If there is
// an unescaped trailing '/' it is removed. This function is safe to use with
// 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 {
func (pb *Builder) appendElements(escaped bool, elements []string) error {
for _, e := range elements {
if len(e) == 0 {
continue
}
b.segmentIdx = append(b.segmentIdx, len(b.elements))
b.elements = append(b.elements, newElems...)
tmp := e
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
}
// 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
// require an escape sequence. If there are no characters that need escaping,
// the input is returned unchanged.
@ -198,13 +165,14 @@ func escapeElement(element string) string {
return element
}
startIdx := 0
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
}
@ -220,9 +188,9 @@ func escapeElement(element string) string {
// 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
@ -240,33 +208,37 @@ func unescape(element string) string {
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.
func validateSegments(segments []string) error {
for _, segment := range segments {
prevWasEscape := false
// validateEscapedElement takes an escaped element that has had trailing
// separators trimmed and ensures that no characters requiring escaping are
// unescaped and that no escape characters are combined with characters that
// don't need escaping.
func validateEscapedElement(element string) error {
prevWasEscape := false
for _, c := range segment {
switch prevWasEscape {
case true:
prevWasEscape = false
for _, c := range element {
switch prevWasEscape {
case true:
prevWasEscape = false
if _, ok := charactersToEscape[c]; !ok {
return errors.Errorf(
"bad escape sequence in path: '%c%c'", escapeCharacter, c)
}
if _, ok := charactersToEscape[c]; !ok {
return errors.Errorf(
"bad escape sequence in path: '%c%c'", escapeCharacter, c)
}
case false:
if c == escapeCharacter {
prevWasEscape = true
}
case false:
if c == escapeCharacter {
prevWasEscape = true
continue
}
if _, ok := charactersToEscape[c]; ok {
return errors.Errorf("unescaped '%c' in path", c)
}
}
}
if prevWasEscape {
return errors.New("trailing escape character in segment")
}
if prevWasEscape {
return errors.New("trailing escape character")
}
return nil
@ -284,6 +256,7 @@ func trimTrailingSlash(element string) string {
}
numSlashes := 0
for i := lastIdx - 1; i >= 0; i-- {
if element[i] != escapeCharacter {
break
@ -306,52 +279,3 @@ func join(elements []string) string {
// '\' according to the escaping rules.
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 (
"fmt"
"reflect"
"strings"
"testing"
@ -11,138 +10,185 @@ import (
"github.com/stretchr/testify/suite"
)
var basicInputs = []struct {
name string
input [][]string
expectedString string
expectedEscapedSegments []string
expectedUnescapedElements [][]string
}{
type testData struct {
name string
input []string
expectedString string
}
// Test cases that are the same with and without escaping by the
// system-under-test.
var genericCases = []testData{
{
name: "SimplePath",
input: [][]string{
{`this`},
{`is`},
{`a`},
{`path`},
},
expectedString: "this/is/a/path",
expectedEscapedSegments: []string{
input: []string{
`this`,
`is`,
`a`,
`path`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is`},
{`a`},
{`path`},
},
expectedString: "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",
input: [][]string{
{`this`},
{`is/a`},
{`path`},
input: []string{
`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`,
`is\/a`,
`path`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is/a`},
{`path`},
},
expectedString: `this/is\\\/a/path`,
},
{
name: "EscapeEscapeChar",
input: [][]string{
{`this`},
{`is\`},
{`a`},
{`path`},
name: "SeparatorAtEndOfElement",
input: []string{
`this`,
`is/`,
`a`,
`path`,
},
expectedString: `this/is\\/a/path`,
expectedEscapedSegments: []string{
expectedString: `this/is\//a/path`,
},
{
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`,
`is\\`,
`a`,
`path`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is\`},
{`a`},
{`path`},
},
expectedString: `this/is\\/a/path`,
},
{
name: "EscapeEscapeAndSeparator",
input: [][]string{
{`this`},
{`is\/a`},
{`path`},
},
expectedString: `this/is\\\/a/path`,
expectedEscapedSegments: []string{
name: "EscapedEscapeAndSeparator",
input: []string{
`this`,
`is\\\/a`,
`path`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is\/a`},
{`path`},
},
expectedString: `this/is\\\/a/path`,
},
{
name: "SeparatorAtEndOfElement",
input: [][]string{
{`this`},
{`is/`},
{`a`},
{`path`},
},
expectedString: `this/is\//a/path`,
expectedEscapedSegments: []string{
name: "EscapedSeparatorAtEndOfElement",
input: []string{
`this`,
`is\/`,
`a`,
`path`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is/`},
{`a`},
{`path`},
},
expectedString: `this/is\//a/path`,
},
{
name: "SeparatorAtEndOfPath",
input: [][]string{
{`this`},
{`is`},
{`a`},
{`path/`},
},
expectedString: `this/is/a/path\/`,
expectedEscapedSegments: []string{
name: "EscapedSeparatorAtEndOfPath",
input: []string{
`this`,
`is`,
`a`,
`path\/`,
},
expectedUnescapedElements: [][]string{
{`this`},
{`is`},
{`a`},
{`path/`},
expectedString: `this/is/a/path\/`,
},
{
name: "ElementOfSeparator",
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))
}
func (suite *PathUnitSuite) TestPathEscapingAndSegments() {
for _, test := range basicInputs {
func (suite *PathUnitSuite) TestAppend() {
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
for _, test := range table {
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())
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: "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",
},
}
func (suite *PathUnitSuite) TestUnescapeAndAppend() {
table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
p := newPath(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)
p, err := Builder{}.UnescapeAndAppend(test.input...)
require.NoError(t, err)
assert.Equal(t, test.expected, p.String())
for i, s := range test.expectedSegments {
segment := ""
require.NotPanics(t, func() {
segment = p.segment(i)
})
assert.Equal(t, s, segment)
}
assert.Equal(t, test.expectedString, p.String())
})
}
}
func (suite *PathUnitSuite) TestEscapedFailure() {
target := "i_s/a"
target := "i_s"
for c := range charactersToEscape {
if c == pathSeparator {
// Extra path separators in the path will just lead to more segments, not
// a validation error.
continue
}
suite.T().Run(fmt.Sprintf("Unescaped-%c", c), func(t *testing.T) {
tmp := strings.ReplaceAll(target, "_", string(c))
tmp := strings.ReplaceAll(target, "_", string(c))
basePath := []string{"this", tmp, "path"}
_, err := newPathFromEscapedSegments(basePath)
assert.Error(suite.T(), err, "path with unescaped %s did not error", string(c))
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
assert.Error(t, err, "path with unescaped %s did not error", string(c))
})
}
}
@ -370,16 +240,18 @@ func (suite *PathUnitSuite) TestBadEscapeSequenceErrors() {
notEscapes := []rune{'a', 'b', '#', '%'}
for _, c := range notEscapes {
tmp := strings.ReplaceAll(target, "_", string(c))
basePath := []string{"this", tmp, "path"}
_, err := newPathFromEscapedSegments(basePath)
assert.Error(
suite.T(),
err,
"path with bad escape sequence %c%c did not error",
escapeCharacter,
c,
)
suite.T().Run(fmt.Sprintf("Escaped-%c", c), func(t *testing.T) {
tmp := strings.ReplaceAll(target, "_", string(c))
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
assert.Error(
t,
err,
"path with bad escape sequence %c%c did not error",
escapeCharacter,
c,
)
})
}
}
@ -387,13 +259,17 @@ func (suite *PathUnitSuite) TestTrailingEscapeChar() {
base := []string{"this", "is", "a", "path"}
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))
copy(path, base)
path[i] = path[i] + string(escapeCharacter)
_, err := newPathFromEscapedSegments(path)
assert.Error(suite.T(), err)
_, err := Builder{}.UnescapeAndAppend(path...)
assert.Error(
t,
err,
"path with trailing escape character did not error",
)
})
}
}