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:
parent
c4e9046870
commit
f24ad6ccbd
@ -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)
|
||||
}
|
||||
@ -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())
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user