diff --git a/src/internal/path/exchange_path.go b/src/internal/path/exchange_path.go deleted file mode 100644 index 738a7a30b..000000000 --- a/src/internal/path/exchange_path.go +++ /dev/null @@ -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) -} diff --git a/src/internal/path/exchange_path_test.go b/src/internal/path/exchange_path_test.go deleted file mode 100644 index f44840139..000000000 --- a/src/internal/path/exchange_path_test.go +++ /dev/null @@ -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()) -} diff --git a/src/internal/path/path.go b/src/internal/path/path.go index a2018749f..dacadb457 100644 --- a/src/internal/path/path.go +++ b/src/internal/path/path.go @@ -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 -} diff --git a/src/internal/path/path_test.go b/src/internal/path/path_test.go index 74efb8dcb..586522a0d 100644 --- a/src/internal/path/path_test.go +++ b/src/internal/path/path_test.go @@ -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", + ) }) } }