small paths code rearrangement (#4007)
small cleanup in paths, primarily splitting files so that file contents are more clearly owned, which should be a little better for readability and code placement. Also renames `ServicePrefix` to `BuildPrefix` in anticipation of multi-service prefixes. no logic changes, just movement/renaming. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🧹 Tech Debt/Cleanup #### Issue(s) * #3993 #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
19bf0fdf7e
commit
1f22b94fc1
@ -70,7 +70,7 @@ func (r reason) Category() path.CategoryType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r reason) SubtreePath() (path.Path, error) {
|
func (r reason) SubtreePath() (path.Path, error) {
|
||||||
p, err := path.ServicePrefix(
|
p, err := path.BuildPrefix(
|
||||||
r.Tenant(),
|
r.Tenant(),
|
||||||
r.ProtectedResource(),
|
r.ProtectedResource(),
|
||||||
r.Service(),
|
r.Service(),
|
||||||
|
|||||||
@ -2865,16 +2865,16 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSelectsMigrateSubt
|
|||||||
migratedUser = "user_migrate"
|
migratedUser = "user_migrate"
|
||||||
)
|
)
|
||||||
|
|
||||||
oldPrefixPathEmail, err := path.ServicePrefix(testTenant, testUser, path.ExchangeService, path.EmailCategory)
|
oldPrefixPathEmail, err := path.BuildPrefix(testTenant, testUser, path.ExchangeService, path.EmailCategory)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
newPrefixPathEmail, err := path.ServicePrefix(testTenant, migratedUser, path.ExchangeService, path.EmailCategory)
|
newPrefixPathEmail, err := path.BuildPrefix(testTenant, migratedUser, path.ExchangeService, path.EmailCategory)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
oldPrefixPathCont, err := path.ServicePrefix(testTenant, testUser, path.ExchangeService, path.ContactsCategory)
|
oldPrefixPathCont, err := path.BuildPrefix(testTenant, testUser, path.ExchangeService, path.ContactsCategory)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
newPrefixPathCont, err := path.ServicePrefix(testTenant, migratedUser, path.ExchangeService, path.ContactsCategory)
|
newPrefixPathCont, err := path.BuildPrefix(testTenant, migratedUser, path.ExchangeService, path.ContactsCategory)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@ -79,7 +79,7 @@ func BaseCollections(
|
|||||||
for cat := range categories {
|
for cat := range categories {
|
||||||
ictx := clues.Add(ctx, "base_service", service, "base_category", cat)
|
ictx := clues.Add(ctx, "base_service", service, "base_category", cat)
|
||||||
|
|
||||||
full, err := path.ServicePrefix(tenant, rOwner, service, cat)
|
full, err := path.BuildPrefix(tenant, rOwner, service, cat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Shouldn't happen.
|
// Shouldn't happen.
|
||||||
err = clues.Wrap(err, "making path").WithClues(ictx)
|
err = clues.Wrap(err, "making path").WithClues(ictx)
|
||||||
|
|||||||
@ -24,10 +24,10 @@ func (suite *CollectionsUnitSuite) TestNewPrefixCollection() {
|
|||||||
serv := path.OneDriveService
|
serv := path.OneDriveService
|
||||||
cat := path.FilesCategory
|
cat := path.FilesCategory
|
||||||
|
|
||||||
p1, err := path.ServicePrefix("t", "ro1", serv, cat)
|
p1, err := path.BuildPrefix("t", "ro1", serv, cat)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
p2, err := path.ServicePrefix("t", "ro2", serv, cat)
|
p2, err := path.BuildPrefix("t", "ro2", serv, cat)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
items, err := path.Build("t", "ro", serv, cat, true, "fld", "itm")
|
items, err := path.Build("t", "ro", serv, cat, true, "fld", "itm")
|
||||||
|
|||||||
@ -109,7 +109,7 @@ func migrationCollections(
|
|||||||
|
|
||||||
// unlike exchange, which enumerates all folders on every
|
// unlike exchange, which enumerates all folders on every
|
||||||
// backup, onedrive needs to force the owner PN -> ID migration
|
// backup, onedrive needs to force the owner PN -> ID migration
|
||||||
mc, err := path.ServicePrefix(
|
mc, err := path.BuildPrefix(
|
||||||
tenant,
|
tenant,
|
||||||
bpc.ProtectedResource.ID(),
|
bpc.ProtectedResource.ID(),
|
||||||
path.OneDriveService,
|
path.OneDriveService,
|
||||||
@ -118,7 +118,7 @@ func migrationCollections(
|
|||||||
return nil, clues.Wrap(err, "creating user id migration path")
|
return nil, clues.Wrap(err, "creating user id migration path")
|
||||||
}
|
}
|
||||||
|
|
||||||
mpc, err := path.ServicePrefix(
|
mpc, err := path.BuildPrefix(
|
||||||
tenant,
|
tenant,
|
||||||
bpc.ProtectedResource.Name(),
|
bpc.ProtectedResource.Name(),
|
||||||
path.OneDriveService,
|
path.OneDriveService,
|
||||||
|
|||||||
@ -42,7 +42,7 @@ func locationRef(
|
|||||||
|
|
||||||
func basicLocationPath(repoRef path.Path, locRef *path.Builder) (path.Path, error) {
|
func basicLocationPath(repoRef path.Path, locRef *path.Builder) (path.Path, error) {
|
||||||
if len(locRef.Elements()) == 0 {
|
if len(locRef.Elements()) == 0 {
|
||||||
res, err := path.ServicePrefix(
|
res, err := path.BuildPrefix(
|
||||||
repoRef.Tenant(),
|
repoRef.Tenant(),
|
||||||
repoRef.ResourceOwner(),
|
repoRef.ResourceOwner(),
|
||||||
repoRef.Service(),
|
repoRef.Service(),
|
||||||
|
|||||||
@ -347,7 +347,7 @@ func testExchangeContinuousBackups(suite *ExchangeBackupIntgSuite, toggles contr
|
|||||||
// },
|
// },
|
||||||
}
|
}
|
||||||
|
|
||||||
rrPfx, err := path.ServicePrefix(acct.ID(), uidn.ID(), service, path.EmailCategory)
|
rrPfx, err := path.BuildPrefix(acct.ID(), uidn.ID(), service, path.EmailCategory)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
// strip the category from the prefix; we primarily want the tenant and resource owner.
|
// strip the category from the prefix; we primarily want the tenant and resource owner.
|
||||||
|
|||||||
@ -213,7 +213,7 @@ func runDriveIncrementalTest(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
rrPfx, err := path.ServicePrefix(atid, roidn.ID(), service, category)
|
rrPfx, err := path.BuildPrefix(atid, roidn.ID(), service, category)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
// strip the category from the prefix; we primarily want the tenant and resource owner.
|
// strip the category from the prefix; we primarily want the tenant and resource owner.
|
||||||
|
|||||||
379
src/pkg/path/builder.go
Normal file
379
src/pkg/path/builder.go
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
package path
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
)
|
||||||
|
|
||||||
|
// interface compliance required for handling PII
|
||||||
|
var (
|
||||||
|
_ clues.Concealer = &Builder{}
|
||||||
|
_ fmt.Stringer = &Builder{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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-specific 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 Elements
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Unescaped elements can't fail validation.
|
||||||
|
//nolint:errcheck
|
||||||
|
res.appendElements(false, elements)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb *Builder) appendElements(escaped bool, elements []string) error {
|
||||||
|
for _, e := range elements {
|
||||||
|
if len(e) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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 nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitUnescapeAppend takes in an escaped string representing a directory
|
||||||
|
// path, splits the string, and appends it to the current builder.
|
||||||
|
func (pb Builder) SplitUnescapeAppend(s string) (*Builder, error) {
|
||||||
|
elems := Split(TrimTrailingSlash(s))
|
||||||
|
|
||||||
|
return pb.UnescapeAndAppend(elems...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb Builder) PopFront() *Builder {
|
||||||
|
if len(pb.elements) <= 1 {
|
||||||
|
return &Builder{}
|
||||||
|
}
|
||||||
|
|
||||||
|
elements := make([]string, len(pb.elements)-1)
|
||||||
|
copy(elements, pb.elements[1:])
|
||||||
|
|
||||||
|
return &Builder{
|
||||||
|
elements: elements,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dir removes the last element from the builder.
|
||||||
|
func (pb Builder) Dir() *Builder {
|
||||||
|
if len(pb.elements) <= 1 {
|
||||||
|
return &Builder{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Builder{
|
||||||
|
// Safe to use the same elements because Builders are immutable.
|
||||||
|
elements: pb.elements[:len(pb.elements)-1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeadElem returns the first element in the Builder.
|
||||||
|
func (pb Builder) HeadElem() string {
|
||||||
|
if len(pb.elements) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return pb.elements[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastElem returns the last element in the Builder.
|
||||||
|
func (pb Builder) LastElem() string {
|
||||||
|
if len(pb.elements) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return pb.elements[len(pb.elements)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateParent updates leading elements matching prev to be cur and returns
|
||||||
|
// true if it was updated. If prev is not a prefix of this Builder changes
|
||||||
|
// nothing and returns false. If either prev or cur is nil does nothing and
|
||||||
|
// returns false.
|
||||||
|
func (pb *Builder) UpdateParent(prev, cur *Builder) bool {
|
||||||
|
if prev == cur || prev == nil || cur == nil || len(prev.Elements()) > len(pb.Elements()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := true
|
||||||
|
|
||||||
|
for i, e := range prev.Elements() {
|
||||||
|
if pb.elements[i] != e {
|
||||||
|
parent = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parent {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
pb.elements = append(cur.Elements(), pb.elements[len(prev.Elements()):]...)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortRef produces a truncated hash of the builder that
|
||||||
|
// acts as a unique identifier.
|
||||||
|
func (pb Builder) ShortRef() string {
|
||||||
|
if len(pb.elements) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
data := bytes.Buffer{}
|
||||||
|
|
||||||
|
for _, element := range pb.elements {
|
||||||
|
data.WriteString(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := sha256.Sum256(data.Bytes())
|
||||||
|
|
||||||
|
// Some conversions to get the right number of characters in the output. This
|
||||||
|
// outputs hex, so we need to take the target number of characters and do the
|
||||||
|
// equivalent of (shortRefCharacters * 4) / 8. This is
|
||||||
|
// <number of bits represented> / <bits per byte> which gets us how many bytes
|
||||||
|
// to give to our format command.
|
||||||
|
numBytes := shortRefCharacters / 2
|
||||||
|
|
||||||
|
return fmt.Sprintf("%x", sum[:numBytes])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elements returns all the elements in the path. This is a temporary function
|
||||||
|
// and will likely be updated to handle encoded elements instead of clear-text
|
||||||
|
// elements in the future.
|
||||||
|
func (pb Builder) Elements() Elements {
|
||||||
|
return append(Elements{}, pb.elements...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// withPrefix creates a Builder prefixed with the parameter values, and
|
||||||
|
// concatenated with the current builder elements.
|
||||||
|
func (pb Builder) withPrefix(elements ...string) *Builder {
|
||||||
|
res := Builder{}.Append(elements...)
|
||||||
|
res.elements = append(res.elements, pb.elements...)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyPrefix ensures that the tenant and resourceOwner are valid
|
||||||
|
// values, and that the builder has some directory structure.
|
||||||
|
func (pb Builder) verifyPrefix(tenant, resourceOwner string) error {
|
||||||
|
if err := verifyInputValues(tenant, resourceOwner); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pb.elements) == 0 {
|
||||||
|
return clues.New("missing path beyond prefix")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Data Layer Path Transformers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (pb Builder) ToStreamStorePath(
|
||||||
|
tenant, purpose string,
|
||||||
|
service ServiceType,
|
||||||
|
isItem bool,
|
||||||
|
) (Path, error) {
|
||||||
|
if err := verifyInputValues(tenant, purpose); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isItem && len(pb.elements) == 0 {
|
||||||
|
return nil, clues.New("missing path beyond prefix")
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataService := UnknownService
|
||||||
|
|
||||||
|
switch service {
|
||||||
|
case ExchangeService:
|
||||||
|
metadataService = ExchangeMetadataService
|
||||||
|
case OneDriveService:
|
||||||
|
metadataService = OneDriveMetadataService
|
||||||
|
case SharePointService:
|
||||||
|
metadataService = SharePointMetadataService
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dataLayerResourcePath{
|
||||||
|
Builder: *pb.withPrefix(
|
||||||
|
tenant,
|
||||||
|
metadataService.String(),
|
||||||
|
purpose,
|
||||||
|
DetailsCategory.String()),
|
||||||
|
service: metadataService,
|
||||||
|
category: DetailsCategory,
|
||||||
|
hasItem: isItem,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb Builder) ToServiceCategoryMetadataPath(
|
||||||
|
tenant, user string,
|
||||||
|
service ServiceType,
|
||||||
|
category CategoryType,
|
||||||
|
isItem bool,
|
||||||
|
) (Path, error) {
|
||||||
|
if err := ValidateServiceAndCategory(service, category); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := verifyInputValues(tenant, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isItem && len(pb.elements) == 0 {
|
||||||
|
return nil, clues.New("missing path beyond prefix")
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataService := UnknownService
|
||||||
|
|
||||||
|
switch service {
|
||||||
|
case ExchangeService:
|
||||||
|
metadataService = ExchangeMetadataService
|
||||||
|
case OneDriveService:
|
||||||
|
metadataService = OneDriveMetadataService
|
||||||
|
case SharePointService:
|
||||||
|
metadataService = SharePointMetadataService
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dataLayerResourcePath{
|
||||||
|
Builder: *pb.withPrefix(
|
||||||
|
tenant,
|
||||||
|
metadataService.String(),
|
||||||
|
user,
|
||||||
|
category.String(),
|
||||||
|
),
|
||||||
|
service: metadataService,
|
||||||
|
category: category,
|
||||||
|
hasItem: isItem,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb Builder) ToDataLayerPath(
|
||||||
|
tenant, user string,
|
||||||
|
service ServiceType,
|
||||||
|
category CategoryType,
|
||||||
|
isItem bool,
|
||||||
|
) (Path, error) {
|
||||||
|
if err := ValidateServiceAndCategory(service, category); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pb.verifyPrefix(tenant, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dataLayerResourcePath{
|
||||||
|
Builder: *pb.withPrefix(
|
||||||
|
tenant,
|
||||||
|
service.String(),
|
||||||
|
user,
|
||||||
|
category.String()),
|
||||||
|
service: service,
|
||||||
|
category: category,
|
||||||
|
hasItem: isItem,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb Builder) ToDataLayerExchangePathForCategory(
|
||||||
|
tenant, user string,
|
||||||
|
category CategoryType,
|
||||||
|
isItem bool,
|
||||||
|
) (Path, error) {
|
||||||
|
return pb.ToDataLayerPath(tenant, user, ExchangeService, category, isItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb Builder) ToDataLayerOneDrivePath(
|
||||||
|
tenant, user string,
|
||||||
|
isItem bool,
|
||||||
|
) (Path, error) {
|
||||||
|
return pb.ToDataLayerPath(tenant, user, OneDriveService, FilesCategory, isItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb Builder) ToDataLayerSharePointPath(
|
||||||
|
tenant, site string,
|
||||||
|
category CategoryType,
|
||||||
|
isItem bool,
|
||||||
|
) (Path, error) {
|
||||||
|
return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Stringers and PII Concealer Compliance
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Conceal produces a concealed representation of the builder, suitable for
|
||||||
|
// logging, storing in errors, and other output.
|
||||||
|
func (pb Builder) Conceal() string {
|
||||||
|
return pb.elements.Conceal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format produces a concealed representation of the builder, even when
|
||||||
|
// used within a PrintF, suitable for logging, storing in errors,
|
||||||
|
// and other output.
|
||||||
|
func (pb Builder) Format(fs fmt.State, _ rune) {
|
||||||
|
fmt.Fprint(fs, pb.Conceal())
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string that contains all path elements joined together.
|
||||||
|
// Elements of the path that need escaping are escaped.
|
||||||
|
// The result is not concealed, and is not suitable for logging or structured
|
||||||
|
// errors.
|
||||||
|
func (pb Builder) String() string {
|
||||||
|
return pb.elements.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlainString returns an unescaped, unmodified string of the builder.
|
||||||
|
// The result is not concealed, and is not suitable for logging or structured
|
||||||
|
// errors.
|
||||||
|
func (pb Builder) PlainString() string {
|
||||||
|
return pb.elements.PlainString()
|
||||||
|
}
|
||||||
369
src/pkg/path/builder_test.go
Normal file
369
src/pkg/path/builder_test.go
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
package path
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuilderUnitSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &BuilderUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the clues hashing to mask for the span of this suite
|
||||||
|
func (suite *BuilderUnitSuite) SetupSuite() {
|
||||||
|
clues.SetHasher(clues.HashCfg{HashAlg: clues.Flatmask})
|
||||||
|
}
|
||||||
|
|
||||||
|
// revert clues hashing to plaintext for all other tests
|
||||||
|
func (suite *BuilderUnitSuite) TeardownSuite() {
|
||||||
|
clues.SetHasher(clues.NoHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestAppend() {
|
||||||
|
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
p := Builder{}.Append(test.input...)
|
||||||
|
assert.Equal(t, test.expectedString, p.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestAppendItem() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
p, err := Build("t", "ro", ExchangeService, EmailCategory, false, "foo", "bar")
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
pb := p.ToBuilder()
|
||||||
|
assert.Equal(t, pb.String(), p.String())
|
||||||
|
|
||||||
|
pb = pb.Append("qux")
|
||||||
|
|
||||||
|
p, err = p.AppendItem("qux")
|
||||||
|
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
assert.Equal(t, pb.String(), p.String())
|
||||||
|
|
||||||
|
_, err = p.AppendItem("fnords")
|
||||||
|
require.Error(t, err, clues.ToCore(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestUnescapeAndAppend() {
|
||||||
|
table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
p, err := Builder{}.UnescapeAndAppend(test.input...)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
assert.Equal(t, test.expectedString, p.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestEscapedFailure() {
|
||||||
|
target := "i_s"
|
||||||
|
|
||||||
|
for c := range charactersToEscape {
|
||||||
|
suite.Run(fmt.Sprintf("Unescaped-%c", c), func() {
|
||||||
|
tmp := strings.ReplaceAll(target, "_", string(c))
|
||||||
|
|
||||||
|
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
|
||||||
|
assert.Errorf(suite.T(), err, "path with unescaped %s did not error", string(c))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestBadEscapeSequenceErrors() {
|
||||||
|
target := `i\_s/a`
|
||||||
|
notEscapes := []rune{'a', 'b', '#', '%'}
|
||||||
|
|
||||||
|
for _, c := range notEscapes {
|
||||||
|
suite.Run(fmt.Sprintf("Escaped-%c", c), func() {
|
||||||
|
tmp := strings.ReplaceAll(target, "_", string(c))
|
||||||
|
|
||||||
|
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
|
||||||
|
assert.Errorf(
|
||||||
|
suite.T(),
|
||||||
|
err,
|
||||||
|
"path with bad escape sequence %c%c did not error",
|
||||||
|
escapeCharacter,
|
||||||
|
c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestTrailingEscapeChar() {
|
||||||
|
base := []string{"this", "is", "a", "path"}
|
||||||
|
|
||||||
|
for i := 0; i < len(base); i++ {
|
||||||
|
suite.Run(fmt.Sprintf("Element%v", i), func() {
|
||||||
|
path := make([]string, len(base))
|
||||||
|
copy(path, base)
|
||||||
|
path[i] = path[i] + string(escapeCharacter)
|
||||||
|
|
||||||
|
_, err := Builder{}.UnescapeAndAppend(path...)
|
||||||
|
assert.Error(
|
||||||
|
suite.T(),
|
||||||
|
err,
|
||||||
|
"path with trailing escape character did not error")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestElements() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
input []string
|
||||||
|
output []string
|
||||||
|
pathFunc func(elements []string) (*Builder, error)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "SimpleEscapedPath",
|
||||||
|
input: []string{"this", "is", "a", "path"},
|
||||||
|
output: []string{"this", "is", "a", "path"},
|
||||||
|
pathFunc: func(elements []string) (*Builder, error) {
|
||||||
|
return Builder{}.UnescapeAndAppend(elements...)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SimpleUnescapedPath",
|
||||||
|
input: []string{"this", "is", "a", "path"},
|
||||||
|
output: []string{"this", "is", "a", "path"},
|
||||||
|
pathFunc: func(elements []string) (*Builder, error) {
|
||||||
|
return Builder{}.Append(elements...), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EscapedPath",
|
||||||
|
input: []string{"this", `is\/`, "a", "path"},
|
||||||
|
output: []string{"this", "is/", "a", "path"},
|
||||||
|
pathFunc: func(elements []string) (*Builder, error) {
|
||||||
|
return Builder{}.UnescapeAndAppend(elements...)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
p, err := test.pathFunc(test.input)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
assert.Equal(t, Elements(test.output), p.Elements())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestPopFront() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
base *Builder
|
||||||
|
expectedString string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty",
|
||||||
|
base: &Builder{},
|
||||||
|
expectedString: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OneElement",
|
||||||
|
base: Builder{}.Append("something"),
|
||||||
|
expectedString: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TwoElements",
|
||||||
|
base: Builder{}.Append("something", "else"),
|
||||||
|
expectedString: "else",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
assert.Equal(t, test.expectedString, test.base.PopFront().String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestShortRef() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
inputElements []string
|
||||||
|
expectedLen int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "PopulatedPath",
|
||||||
|
inputElements: []string{"this", "is", "a", "path"},
|
||||||
|
expectedLen: shortRefCharacters,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyPath",
|
||||||
|
inputElements: nil,
|
||||||
|
expectedLen: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
pb := Builder{}.Append(test.inputElements...)
|
||||||
|
ref := pb.ShortRef()
|
||||||
|
assert.Len(suite.T(), ref, test.expectedLen)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestShortRefIsStable() {
|
||||||
|
t := suite.T()
|
||||||
|
pb := Builder{}.Append("this", "is", "a", "path")
|
||||||
|
prevRef := pb.ShortRef()
|
||||||
|
assert.Len(t, prevRef, shortRefCharacters)
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
ref := pb.ShortRef()
|
||||||
|
assert.Len(t, ref, shortRefCharacters)
|
||||||
|
assert.Equal(t, prevRef, ref, "ShortRef changed between calls")
|
||||||
|
|
||||||
|
prevRef = ref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestShortRefIsUnique() {
|
||||||
|
pb1 := Builder{}.Append("this", "is", "a", "path")
|
||||||
|
pb2 := pb1.Append("also")
|
||||||
|
|
||||||
|
require.NotEqual(suite.T(), pb1, pb2)
|
||||||
|
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestShortRefUniqueWithEscaping tests that two paths that output the same
|
||||||
|
// unescaped string but different escaped strings have different shortrefs. This
|
||||||
|
// situation can occur when one path has embedded path separators while the
|
||||||
|
// other does not but contains the same characters.
|
||||||
|
func (suite *BuilderUnitSuite) TestShortRefUniqueWithEscaping() {
|
||||||
|
pb1 := Builder{}.Append(`this`, `is`, `a`, `path`)
|
||||||
|
pb2 := Builder{}.Append(`this`, `is/a`, `path`)
|
||||||
|
|
||||||
|
require.NotEqual(suite.T(), pb1, pb2)
|
||||||
|
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestFolder() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
p func(t *testing.T) Path
|
||||||
|
escape bool
|
||||||
|
expectFolder string
|
||||||
|
expectSplit []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "clean path",
|
||||||
|
p: func(t *testing.T) Path {
|
||||||
|
p, err := Builder{}.
|
||||||
|
Append("a", "b", "c").
|
||||||
|
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
return p
|
||||||
|
},
|
||||||
|
expectFolder: "a/b/c",
|
||||||
|
expectSplit: []string{"a", "b", "c"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clean path escaped",
|
||||||
|
p: func(t *testing.T) Path {
|
||||||
|
p, err := Builder{}.
|
||||||
|
Append("a", "b", "c").
|
||||||
|
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
return p
|
||||||
|
},
|
||||||
|
escape: true,
|
||||||
|
expectFolder: "a/b/c",
|
||||||
|
expectSplit: []string{"a", "b", "c"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escapable path",
|
||||||
|
p: func(t *testing.T) Path {
|
||||||
|
p, err := Builder{}.
|
||||||
|
Append("a/", "b", "c").
|
||||||
|
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
return p
|
||||||
|
},
|
||||||
|
expectFolder: "a//b/c",
|
||||||
|
expectSplit: []string{"a", "b", "c"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escapable path escaped",
|
||||||
|
p: func(t *testing.T) Path {
|
||||||
|
p, err := Builder{}.
|
||||||
|
Append("a/", "b", "c").
|
||||||
|
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
return p
|
||||||
|
},
|
||||||
|
escape: true,
|
||||||
|
expectFolder: "a\\//b/c",
|
||||||
|
expectSplit: []string{"a\\/", "b", "c"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
p := test.p(t)
|
||||||
|
result := p.Folder(test.escape)
|
||||||
|
assert.Equal(t, test.expectFolder, result)
|
||||||
|
assert.Equal(t, test.expectSplit, Split(result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BuilderUnitSuite) TestPIIHandling() {
|
||||||
|
p, err := Build("t", "ro", ExchangeService, EventsCategory, true, "dir", "item")
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
p Path
|
||||||
|
expect string
|
||||||
|
expectPlain string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "standard path",
|
||||||
|
p: p,
|
||||||
|
expect: "***/exchange/***/events/***/***",
|
||||||
|
expectPlain: "t/exchange/ro/events/dir/item",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
assert.Equal(t, test.expect, test.p.Conceal(), "conceal")
|
||||||
|
assert.Equal(t, test.expectPlain, test.p.String(), "string")
|
||||||
|
assert.Equal(t, test.expect, fmt.Sprintf("%s", test.p), "fmt %%s")
|
||||||
|
assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.p), "fmt %%+v")
|
||||||
|
assert.Equal(t, test.expectPlain, test.p.PlainString(), "plain")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/pkg/path/category_type.go
Normal file
108
src/pkg/path/category_type.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package path
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrorUnknownCategory = clues.New("unknown category string")
|
||||||
|
|
||||||
|
// CategoryType denotes what category of data the path corresponds to. The order
|
||||||
|
// of the enums below can be changed, but the string representation of each enum
|
||||||
|
// must remain the same or migration code needs to be added to handle changes to
|
||||||
|
// the string format.
|
||||||
|
type CategoryType int
|
||||||
|
|
||||||
|
//go:generate stringer -type=CategoryType -linecomment
|
||||||
|
const (
|
||||||
|
UnknownCategory CategoryType = iota
|
||||||
|
EmailCategory // email
|
||||||
|
ContactsCategory // contacts
|
||||||
|
EventsCategory // events
|
||||||
|
FilesCategory // files
|
||||||
|
ListsCategory // lists
|
||||||
|
LibrariesCategory // libraries
|
||||||
|
PagesCategory // pages
|
||||||
|
DetailsCategory // details
|
||||||
|
)
|
||||||
|
|
||||||
|
func ToCategoryType(category string) CategoryType {
|
||||||
|
cat := strings.ToLower(category)
|
||||||
|
|
||||||
|
switch cat {
|
||||||
|
case strings.ToLower(EmailCategory.String()):
|
||||||
|
return EmailCategory
|
||||||
|
case strings.ToLower(ContactsCategory.String()):
|
||||||
|
return ContactsCategory
|
||||||
|
case strings.ToLower(EventsCategory.String()):
|
||||||
|
return EventsCategory
|
||||||
|
case strings.ToLower(FilesCategory.String()):
|
||||||
|
return FilesCategory
|
||||||
|
case strings.ToLower(LibrariesCategory.String()):
|
||||||
|
return LibrariesCategory
|
||||||
|
case strings.ToLower(ListsCategory.String()):
|
||||||
|
return ListsCategory
|
||||||
|
case strings.ToLower(PagesCategory.String()):
|
||||||
|
return PagesCategory
|
||||||
|
case strings.ToLower(DetailsCategory.String()):
|
||||||
|
return DetailsCategory
|
||||||
|
default:
|
||||||
|
return UnknownCategory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Service-Category pairings
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// serviceCategories is a mapping of all valid service/category pairs for
|
||||||
|
// non-metadata paths.
|
||||||
|
var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
|
||||||
|
ExchangeService: {
|
||||||
|
EmailCategory: {},
|
||||||
|
ContactsCategory: {},
|
||||||
|
EventsCategory: {},
|
||||||
|
},
|
||||||
|
OneDriveService: {
|
||||||
|
FilesCategory: {},
|
||||||
|
},
|
||||||
|
SharePointService: {
|
||||||
|
LibrariesCategory: {},
|
||||||
|
ListsCategory: {},
|
||||||
|
PagesCategory: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) {
|
||||||
|
service := toServiceType(s)
|
||||||
|
if service == UnknownService {
|
||||||
|
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("service", fmt.Sprintf("%q", s))
|
||||||
|
}
|
||||||
|
|
||||||
|
category := ToCategoryType(c)
|
||||||
|
if category == UnknownCategory {
|
||||||
|
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("category", fmt.Sprintf("%q", c))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateServiceAndCategory(service, category); err != nil {
|
||||||
|
return UnknownService, UnknownCategory, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service, category, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateServiceAndCategory(service ServiceType, category CategoryType) error {
|
||||||
|
cats, ok := serviceCategories[service]
|
||||||
|
if !ok {
|
||||||
|
return clues.New("unsupported service").With("service", fmt.Sprintf("%q", service))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := cats[category]; !ok {
|
||||||
|
return clues.New("unknown service/category combination").
|
||||||
|
With("service", fmt.Sprintf("%q", service), "category", fmt.Sprintf("%q", category))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package path
|
|||||||
|
|
||||||
import "github.com/alcionai/clues"
|
import "github.com/alcionai/clues"
|
||||||
|
|
||||||
|
// TODO: Move this into m365/collection/drive
|
||||||
// drivePath is used to represent path components
|
// drivePath is used to represent path components
|
||||||
// of an item within the drive i.e.
|
// of an item within the drive i.e.
|
||||||
// Given `drives/b!X_8Z2zuXpkKkXZsr7gThk9oJpuj0yXVGnK5_VjRRPK-q725SX_8ZQJgFDK8PlFxA/root:/Folder1/Folder2/file`
|
// Given `drives/b!X_8Z2zuXpkKkXZsr7gThk9oJpuj0yXVGnK5_VjRRPK-q725SX_8ZQJgFDK8PlFxA/root:/Folder1/Folder2/file`
|
||||||
|
|||||||
@ -51,8 +51,6 @@
|
|||||||
package path
|
package path
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -122,12 +120,6 @@ type Path interface {
|
|||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
}
|
}
|
||||||
|
|
||||||
// interface compliance required for handling PII
|
|
||||||
var (
|
|
||||||
_ clues.Concealer = &Builder{}
|
|
||||||
_ fmt.Stringer = &Builder{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// RestorePaths denotes the location to find an item in kopia and the path of
|
// RestorePaths denotes the location to find an item in kopia and the path of
|
||||||
// the collection to place the item in for restore.
|
// the collection to place the item in for restore.
|
||||||
type RestorePaths struct {
|
type RestorePaths struct {
|
||||||
@ -135,185 +127,26 @@ type RestorePaths struct {
|
|||||||
RestorePath Path
|
RestorePath Path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builder is a simple path representation that only tracks path elements. It
|
// ---------------------------------------------------------------------------
|
||||||
// can join, escape, and unescape elements. Higher-level packages are expected
|
// Exported Helpers
|
||||||
// to wrap this struct to build resource-specific contexts (e.x. an
|
// ---------------------------------------------------------------------------
|
||||||
// ExchangeMailPath).
|
|
||||||
// Resource-specific paths allow access to more information like segments in the
|
func Build(
|
||||||
// path. Builders that are turned into resource paths later on do not need to
|
tenant, resourceOwner string,
|
||||||
// manually add prefixes for items that normally appear in the data layer (ex.
|
service ServiceType,
|
||||||
// tenant ID, service, user ID, etc).
|
category CategoryType,
|
||||||
type Builder struct {
|
hasItem bool,
|
||||||
// Unescaped version of elements.
|
elements ...string,
|
||||||
elements Elements
|
) (Path, error) {
|
||||||
|
b := Builder{}.Append(elements...)
|
||||||
|
|
||||||
|
return b.ToDataLayerPath(
|
||||||
|
tenant, resourceOwner,
|
||||||
|
service, category,
|
||||||
|
hasItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append creates a copy of this Builder and adds the given elements them to the
|
func BuildPrefix(
|
||||||
// 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)
|
|
||||||
|
|
||||||
// Unescaped elements can't fail validation.
|
|
||||||
//nolint:errcheck
|
|
||||||
res.appendElements(false, elements)
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pb *Builder) appendElements(escaped bool, elements []string) error {
|
|
||||||
for _, e := range elements {
|
|
||||||
if len(e) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
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 nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SplitUnescapeAppend takes in an escaped string representing a directory
|
|
||||||
// path, splits the string, and appends it to the current builder.
|
|
||||||
func (pb Builder) SplitUnescapeAppend(s string) (*Builder, error) {
|
|
||||||
elems := Split(TrimTrailingSlash(s))
|
|
||||||
|
|
||||||
return pb.UnescapeAndAppend(elems...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pb Builder) PopFront() *Builder {
|
|
||||||
if len(pb.elements) <= 1 {
|
|
||||||
return &Builder{}
|
|
||||||
}
|
|
||||||
|
|
||||||
elements := make([]string, len(pb.elements)-1)
|
|
||||||
copy(elements, pb.elements[1:])
|
|
||||||
|
|
||||||
return &Builder{
|
|
||||||
elements: elements,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dir removes the last element from the builder.
|
|
||||||
func (pb Builder) Dir() *Builder {
|
|
||||||
if len(pb.elements) <= 1 {
|
|
||||||
return &Builder{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Builder{
|
|
||||||
// Safe to use the same elements because Builders are immutable.
|
|
||||||
elements: pb.elements[:len(pb.elements)-1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeadElem returns the first element in the Builder.
|
|
||||||
func (pb Builder) HeadElem() string {
|
|
||||||
if len(pb.elements) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return pb.elements[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// LastElem returns the last element in the Builder.
|
|
||||||
func (pb Builder) LastElem() string {
|
|
||||||
if len(pb.elements) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return pb.elements[len(pb.elements)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateParent updates leading elements matching prev to be cur and returns
|
|
||||||
// true if it was updated. If prev is not a prefix of this Builder changes
|
|
||||||
// nothing and returns false. If either prev or cur is nil does nothing and
|
|
||||||
// returns false.
|
|
||||||
func (pb *Builder) UpdateParent(prev, cur *Builder) bool {
|
|
||||||
if prev == cur || prev == nil || cur == nil || len(prev.Elements()) > len(pb.Elements()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
parent := true
|
|
||||||
|
|
||||||
for i, e := range prev.Elements() {
|
|
||||||
if pb.elements[i] != e {
|
|
||||||
parent = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !parent {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
pb.elements = append(cur.Elements(), pb.elements[len(prev.Elements()):]...)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShortRef produces a truncated hash of the builder that
|
|
||||||
// acts as a unique identifier.
|
|
||||||
func (pb Builder) ShortRef() string {
|
|
||||||
if len(pb.elements) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
data := bytes.Buffer{}
|
|
||||||
|
|
||||||
for _, element := range pb.elements {
|
|
||||||
data.WriteString(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
sum := sha256.Sum256(data.Bytes())
|
|
||||||
|
|
||||||
// Some conversions to get the right number of characters in the output. This
|
|
||||||
// outputs hex, so we need to take the target number of characters and do the
|
|
||||||
// equivalent of (shortRefCharacters * 4) / 8. This is
|
|
||||||
// <number of bits represented> / <bits per byte> which gets us how many bytes
|
|
||||||
// to give to our format command.
|
|
||||||
numBytes := shortRefCharacters / 2
|
|
||||||
|
|
||||||
return fmt.Sprintf("%x", sum[:numBytes])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Elements returns all the elements in the path. This is a temporary function
|
|
||||||
// and will likely be updated to handle encoded elements instead of clear-text
|
|
||||||
// elements in the future.
|
|
||||||
func (pb Builder) Elements() Elements {
|
|
||||||
return append(Elements{}, pb.elements...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServicePrefix(
|
|
||||||
tenant, resourceOwner string,
|
tenant, resourceOwner string,
|
||||||
s ServiceType,
|
s ServiceType,
|
||||||
c CategoryType,
|
c CategoryType,
|
||||||
@ -336,197 +169,6 @@ func ServicePrefix(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// withPrefix creates a Builder prefixed with the parameter values, and
|
|
||||||
// concatenated with the current builder elements.
|
|
||||||
func (pb Builder) withPrefix(elements ...string) *Builder {
|
|
||||||
res := Builder{}.Append(elements...)
|
|
||||||
res.elements = append(res.elements, pb.elements...)
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Data Layer Path Transformers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func (pb Builder) ToStreamStorePath(
|
|
||||||
tenant, purpose string,
|
|
||||||
service ServiceType,
|
|
||||||
isItem bool,
|
|
||||||
) (Path, error) {
|
|
||||||
if err := verifyInputValues(tenant, purpose); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if isItem && len(pb.elements) == 0 {
|
|
||||||
return nil, clues.New("missing path beyond prefix")
|
|
||||||
}
|
|
||||||
|
|
||||||
metadataService := UnknownService
|
|
||||||
|
|
||||||
switch service {
|
|
||||||
case ExchangeService:
|
|
||||||
metadataService = ExchangeMetadataService
|
|
||||||
case OneDriveService:
|
|
||||||
metadataService = OneDriveMetadataService
|
|
||||||
case SharePointService:
|
|
||||||
metadataService = SharePointMetadataService
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dataLayerResourcePath{
|
|
||||||
Builder: *pb.withPrefix(
|
|
||||||
tenant,
|
|
||||||
metadataService.String(),
|
|
||||||
purpose,
|
|
||||||
DetailsCategory.String()),
|
|
||||||
service: metadataService,
|
|
||||||
category: DetailsCategory,
|
|
||||||
hasItem: isItem,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pb Builder) ToServiceCategoryMetadataPath(
|
|
||||||
tenant, user string,
|
|
||||||
service ServiceType,
|
|
||||||
category CategoryType,
|
|
||||||
isItem bool,
|
|
||||||
) (Path, error) {
|
|
||||||
if err := ValidateServiceAndCategory(service, category); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := verifyInputValues(tenant, user); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if isItem && len(pb.elements) == 0 {
|
|
||||||
return nil, clues.New("missing path beyond prefix")
|
|
||||||
}
|
|
||||||
|
|
||||||
metadataService := UnknownService
|
|
||||||
|
|
||||||
switch service {
|
|
||||||
case ExchangeService:
|
|
||||||
metadataService = ExchangeMetadataService
|
|
||||||
case OneDriveService:
|
|
||||||
metadataService = OneDriveMetadataService
|
|
||||||
case SharePointService:
|
|
||||||
metadataService = SharePointMetadataService
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dataLayerResourcePath{
|
|
||||||
Builder: *pb.withPrefix(
|
|
||||||
tenant,
|
|
||||||
metadataService.String(),
|
|
||||||
user,
|
|
||||||
category.String(),
|
|
||||||
),
|
|
||||||
service: metadataService,
|
|
||||||
category: category,
|
|
||||||
hasItem: isItem,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pb Builder) ToDataLayerPath(
|
|
||||||
tenant, user string,
|
|
||||||
service ServiceType,
|
|
||||||
category CategoryType,
|
|
||||||
isItem bool,
|
|
||||||
) (Path, error) {
|
|
||||||
if err := ValidateServiceAndCategory(service, category); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pb.verifyPrefix(tenant, user); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dataLayerResourcePath{
|
|
||||||
Builder: *pb.withPrefix(
|
|
||||||
tenant,
|
|
||||||
service.String(),
|
|
||||||
user,
|
|
||||||
category.String()),
|
|
||||||
service: service,
|
|
||||||
category: category,
|
|
||||||
hasItem: isItem,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pb Builder) ToDataLayerExchangePathForCategory(
|
|
||||||
tenant, user string,
|
|
||||||
category CategoryType,
|
|
||||||
isItem bool,
|
|
||||||
) (Path, error) {
|
|
||||||
return pb.ToDataLayerPath(tenant, user, ExchangeService, category, isItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pb Builder) ToDataLayerOneDrivePath(
|
|
||||||
tenant, user string,
|
|
||||||
isItem bool,
|
|
||||||
) (Path, error) {
|
|
||||||
return pb.ToDataLayerPath(tenant, user, OneDriveService, FilesCategory, isItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pb Builder) ToDataLayerSharePointPath(
|
|
||||||
tenant, site string,
|
|
||||||
category CategoryType,
|
|
||||||
isItem bool,
|
|
||||||
) (Path, error) {
|
|
||||||
return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Stringers and PII Concealer Compliance
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Conceal produces a concealed representation of the builder, suitable for
|
|
||||||
// logging, storing in errors, and other output.
|
|
||||||
func (pb Builder) Conceal() string {
|
|
||||||
return pb.elements.Conceal()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format produces a concealed representation of the builder, even when
|
|
||||||
// used within a PrintF, suitable for logging, storing in errors,
|
|
||||||
// and other output.
|
|
||||||
func (pb Builder) Format(fs fmt.State, _ rune) {
|
|
||||||
fmt.Fprint(fs, pb.Conceal())
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string that contains all path elements joined together.
|
|
||||||
// Elements of the path that need escaping are escaped.
|
|
||||||
// The result is not concealed, and is not suitable for logging or structured
|
|
||||||
// errors.
|
|
||||||
func (pb Builder) String() string {
|
|
||||||
return pb.elements.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlainString returns an unescaped, unmodified string of the builder.
|
|
||||||
// The result is not concealed, and is not suitable for logging or structured
|
|
||||||
// errors.
|
|
||||||
func (pb Builder) PlainString() string {
|
|
||||||
return pb.elements.PlainString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Exported Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Build(
|
|
||||||
tenant, resourceOwner string,
|
|
||||||
service ServiceType,
|
|
||||||
category CategoryType,
|
|
||||||
hasItem bool,
|
|
||||||
elements ...string,
|
|
||||||
) (Path, error) {
|
|
||||||
b := Builder{}.Append(elements...)
|
|
||||||
|
|
||||||
return b.ToDataLayerPath(
|
|
||||||
tenant, resourceOwner,
|
|
||||||
service, category,
|
|
||||||
hasItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDataLayerPath parses the escaped path p, validates the elements in p
|
// FromDataLayerPath parses the escaped path p, validates the elements in p
|
||||||
// match a resource-specific path format, and returns a Path struct for that
|
// match a resource-specific path format, and returns a Path struct for that
|
||||||
// resource-specific type. If p does not match any resource-specific paths or
|
// resource-specific type. If p does not match any resource-specific paths or
|
||||||
@ -762,17 +404,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyPrefix ensures that the tenant and resourceOwner are valid
|
|
||||||
// values, and that the builder has some directory structure.
|
|
||||||
func (pb Builder) verifyPrefix(tenant, resourceOwner string) error {
|
|
||||||
if err := verifyInputValues(tenant, resourceOwner); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pb.elements) == 0 {
|
|
||||||
return clues.New("missing path beyond prefix")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package path
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
@ -233,239 +232,7 @@ func (suite *PathUnitSuite) TeardownSuite() {
|
|||||||
clues.SetHasher(clues.NoHash())
|
clues.SetHasher(clues.NoHash())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestAppend() {
|
func (suite *PathUnitSuite) TestFromDataLayerPathErrors() {
|
||||||
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
|
|
||||||
for _, test := range table {
|
|
||||||
suite.Run(test.name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
p := Builder{}.Append(test.input...)
|
|
||||||
assert.Equal(t, test.expectedString, p.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestAppendItem() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
p, err := Build("t", "ro", ExchangeService, EmailCategory, false, "foo", "bar")
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
pb := p.ToBuilder()
|
|
||||||
assert.Equal(t, pb.String(), p.String())
|
|
||||||
|
|
||||||
pb = pb.Append("qux")
|
|
||||||
|
|
||||||
p, err = p.AppendItem("qux")
|
|
||||||
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
assert.Equal(t, pb.String(), p.String())
|
|
||||||
|
|
||||||
_, err = p.AppendItem("fnords")
|
|
||||||
require.Error(t, err, clues.ToCore(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestUnescapeAndAppend() {
|
|
||||||
table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
|
|
||||||
for _, test := range table {
|
|
||||||
suite.Run(test.name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
p, err := Builder{}.UnescapeAndAppend(test.input...)
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
assert.Equal(t, test.expectedString, p.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestEscapedFailure() {
|
|
||||||
target := "i_s"
|
|
||||||
|
|
||||||
for c := range charactersToEscape {
|
|
||||||
suite.Run(fmt.Sprintf("Unescaped-%c", c), func() {
|
|
||||||
tmp := strings.ReplaceAll(target, "_", string(c))
|
|
||||||
|
|
||||||
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
|
|
||||||
assert.Errorf(suite.T(), err, "path with unescaped %s did not error", string(c))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestBadEscapeSequenceErrors() {
|
|
||||||
target := `i\_s/a`
|
|
||||||
notEscapes := []rune{'a', 'b', '#', '%'}
|
|
||||||
|
|
||||||
for _, c := range notEscapes {
|
|
||||||
suite.Run(fmt.Sprintf("Escaped-%c", c), func() {
|
|
||||||
tmp := strings.ReplaceAll(target, "_", string(c))
|
|
||||||
|
|
||||||
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
|
|
||||||
assert.Errorf(
|
|
||||||
suite.T(),
|
|
||||||
err,
|
|
||||||
"path with bad escape sequence %c%c did not error",
|
|
||||||
escapeCharacter,
|
|
||||||
c)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestTrailingEscapeChar() {
|
|
||||||
base := []string{"this", "is", "a", "path"}
|
|
||||||
|
|
||||||
for i := 0; i < len(base); i++ {
|
|
||||||
suite.Run(fmt.Sprintf("Element%v", i), func() {
|
|
||||||
path := make([]string, len(base))
|
|
||||||
copy(path, base)
|
|
||||||
path[i] = path[i] + string(escapeCharacter)
|
|
||||||
|
|
||||||
_, err := Builder{}.UnescapeAndAppend(path...)
|
|
||||||
assert.Error(
|
|
||||||
suite.T(),
|
|
||||||
err,
|
|
||||||
"path with trailing escape character did not error")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestElements() {
|
|
||||||
table := []struct {
|
|
||||||
name string
|
|
||||||
input []string
|
|
||||||
output []string
|
|
||||||
pathFunc func(elements []string) (*Builder, error)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "SimpleEscapedPath",
|
|
||||||
input: []string{"this", "is", "a", "path"},
|
|
||||||
output: []string{"this", "is", "a", "path"},
|
|
||||||
pathFunc: func(elements []string) (*Builder, error) {
|
|
||||||
return Builder{}.UnescapeAndAppend(elements...)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "SimpleUnescapedPath",
|
|
||||||
input: []string{"this", "is", "a", "path"},
|
|
||||||
output: []string{"this", "is", "a", "path"},
|
|
||||||
pathFunc: func(elements []string) (*Builder, error) {
|
|
||||||
return Builder{}.Append(elements...), nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "EscapedPath",
|
|
||||||
input: []string{"this", `is\/`, "a", "path"},
|
|
||||||
output: []string{"this", "is/", "a", "path"},
|
|
||||||
pathFunc: func(elements []string) (*Builder, error) {
|
|
||||||
return Builder{}.UnescapeAndAppend(elements...)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range table {
|
|
||||||
suite.Run(test.name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
p, err := test.pathFunc(test.input)
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
assert.Equal(t, Elements(test.output), p.Elements())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestPopFront() {
|
|
||||||
table := []struct {
|
|
||||||
name string
|
|
||||||
base *Builder
|
|
||||||
expectedString string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Empty",
|
|
||||||
base: &Builder{},
|
|
||||||
expectedString: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "OneElement",
|
|
||||||
base: Builder{}.Append("something"),
|
|
||||||
expectedString: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "TwoElements",
|
|
||||||
base: Builder{}.Append("something", "else"),
|
|
||||||
expectedString: "else",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range table {
|
|
||||||
suite.Run(test.name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
assert.Equal(t, test.expectedString, test.base.PopFront().String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestShortRef() {
|
|
||||||
table := []struct {
|
|
||||||
name string
|
|
||||||
inputElements []string
|
|
||||||
expectedLen int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "PopulatedPath",
|
|
||||||
inputElements: []string{"this", "is", "a", "path"},
|
|
||||||
expectedLen: shortRefCharacters,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "EmptyPath",
|
|
||||||
inputElements: nil,
|
|
||||||
expectedLen: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range table {
|
|
||||||
suite.Run(test.name, func() {
|
|
||||||
pb := Builder{}.Append(test.inputElements...)
|
|
||||||
ref := pb.ShortRef()
|
|
||||||
assert.Len(suite.T(), ref, test.expectedLen)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestShortRefIsStable() {
|
|
||||||
t := suite.T()
|
|
||||||
pb := Builder{}.Append("this", "is", "a", "path")
|
|
||||||
prevRef := pb.ShortRef()
|
|
||||||
assert.Len(t, prevRef, shortRefCharacters)
|
|
||||||
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
ref := pb.ShortRef()
|
|
||||||
assert.Len(t, ref, shortRefCharacters)
|
|
||||||
assert.Equal(t, prevRef, ref, "ShortRef changed between calls")
|
|
||||||
|
|
||||||
prevRef = ref
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestShortRefIsUnique() {
|
|
||||||
pb1 := Builder{}.Append("this", "is", "a", "path")
|
|
||||||
pb2 := pb1.Append("also")
|
|
||||||
|
|
||||||
require.NotEqual(suite.T(), pb1, pb2)
|
|
||||||
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestShortRefUniqueWithEscaping tests that two paths that output the same
|
|
||||||
// unescaped string but different escaped strings have different shortrefs. This
|
|
||||||
// situation can occur when one path has embedded path separators while the
|
|
||||||
// other does not but contains the same characters.
|
|
||||||
func (suite *PathUnitSuite) TestShortRefUniqueWithEscaping() {
|
|
||||||
pb1 := Builder{}.Append(`this`, `is`, `a`, `path`)
|
|
||||||
pb2 := Builder{}.Append(`this`, `is/a`, `path`)
|
|
||||||
|
|
||||||
require.NotEqual(suite.T(), pb1, pb2)
|
|
||||||
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestFromStringErrors() {
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
escapedPath string
|
escapedPath string
|
||||||
@ -521,82 +288,7 @@ func (suite *PathUnitSuite) TestFromStringErrors() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestFolder() {
|
func (suite *PathUnitSuite) TestFromDataLayerPath() {
|
||||||
table := []struct {
|
|
||||||
name string
|
|
||||||
p func(t *testing.T) Path
|
|
||||||
escape bool
|
|
||||||
expectFolder string
|
|
||||||
expectSplit []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "clean path",
|
|
||||||
p: func(t *testing.T) Path {
|
|
||||||
p, err := Builder{}.
|
|
||||||
Append("a", "b", "c").
|
|
||||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
return p
|
|
||||||
},
|
|
||||||
expectFolder: "a/b/c",
|
|
||||||
expectSplit: []string{"a", "b", "c"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "clean path escaped",
|
|
||||||
p: func(t *testing.T) Path {
|
|
||||||
p, err := Builder{}.
|
|
||||||
Append("a", "b", "c").
|
|
||||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
return p
|
|
||||||
},
|
|
||||||
escape: true,
|
|
||||||
expectFolder: "a/b/c",
|
|
||||||
expectSplit: []string{"a", "b", "c"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "escapable path",
|
|
||||||
p: func(t *testing.T) Path {
|
|
||||||
p, err := Builder{}.
|
|
||||||
Append("a/", "b", "c").
|
|
||||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
return p
|
|
||||||
},
|
|
||||||
expectFolder: "a//b/c",
|
|
||||||
expectSplit: []string{"a", "b", "c"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "escapable path escaped",
|
|
||||||
p: func(t *testing.T) Path {
|
|
||||||
p, err := Builder{}.
|
|
||||||
Append("a/", "b", "c").
|
|
||||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
return p
|
|
||||||
},
|
|
||||||
escape: true,
|
|
||||||
expectFolder: "a\\//b/c",
|
|
||||||
expectSplit: []string{"a\\/", "b", "c"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range table {
|
|
||||||
suite.Run(test.name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
p := test.p(t)
|
|
||||||
result := p.Folder(test.escape)
|
|
||||||
assert.Equal(t, test.expectFolder, result)
|
|
||||||
assert.Equal(t, test.expectSplit, Split(result))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestFromString() {
|
|
||||||
const (
|
const (
|
||||||
testTenant = "tenant"
|
testTenant = "tenant"
|
||||||
testUser = "user"
|
testUser = "user"
|
||||||
@ -740,37 +432,7 @@ func (suite *PathUnitSuite) TestFromString() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestPath_piiHandling() {
|
func (suite *PathUnitSuite) TestBuildPrefix() {
|
||||||
p, err := Build("t", "ro", ExchangeService, EventsCategory, true, "dir", "item")
|
|
||||||
require.NoError(suite.T(), err)
|
|
||||||
|
|
||||||
table := []struct {
|
|
||||||
name string
|
|
||||||
p Path
|
|
||||||
expect string
|
|
||||||
expectPlain string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "standard path",
|
|
||||||
p: p,
|
|
||||||
expect: "***/exchange/***/events/***/***",
|
|
||||||
expectPlain: "t/exchange/ro/events/dir/item",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range table {
|
|
||||||
suite.Run(test.name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
assert.Equal(t, test.expect, test.p.Conceal(), "conceal")
|
|
||||||
assert.Equal(t, test.expectPlain, test.p.String(), "string")
|
|
||||||
assert.Equal(t, test.expect, fmt.Sprintf("%s", test.p), "fmt %%s")
|
|
||||||
assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.p), "fmt %%+v")
|
|
||||||
assert.Equal(t, test.expectPlain, test.p.PlainString(), "plain")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestToServicePrefix() {
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
service ServiceType
|
service ServiceType
|
||||||
@ -818,7 +480,7 @@ func (suite *PathUnitSuite) TestToServicePrefix() {
|
|||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
|
||||||
r, err := ServicePrefix(test.tenant, test.owner, test.service, test.category)
|
r, err := BuildPrefix(test.tenant, test.owner, test.service, test.category)
|
||||||
test.expectErr(t, err, clues.ToCore(err))
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
if r == nil {
|
if r == nil {
|
||||||
|
|||||||
@ -1,154 +1,9 @@
|
|||||||
package path
|
package path
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrorUnknownService = clues.New("unknown service string")
|
|
||||||
|
|
||||||
// ServiceType denotes what service the path corresponds to. Metadata services
|
|
||||||
// are also included though they are only used for paths that house metadata for
|
|
||||||
// Corso backups.
|
|
||||||
//
|
|
||||||
// Metadata services are not considered valid service types for resource paths
|
|
||||||
// though they can be used for metadata paths.
|
|
||||||
//
|
|
||||||
// The order of the enums below can be changed, but the string representation of
|
|
||||||
// each enum must remain the same or migration code needs to be added to handle
|
|
||||||
// changes to the string format.
|
|
||||||
type ServiceType int
|
|
||||||
|
|
||||||
//go:generate stringer -type=ServiceType -linecomment
|
|
||||||
const (
|
|
||||||
UnknownService ServiceType = iota
|
|
||||||
ExchangeService // exchange
|
|
||||||
OneDriveService // onedrive
|
|
||||||
SharePointService // sharepoint
|
|
||||||
ExchangeMetadataService // exchangeMetadata
|
|
||||||
OneDriveMetadataService // onedriveMetadata
|
|
||||||
SharePointMetadataService // sharepointMetadata
|
|
||||||
)
|
|
||||||
|
|
||||||
func toServiceType(service string) ServiceType {
|
|
||||||
s := strings.ToLower(service)
|
|
||||||
|
|
||||||
switch s {
|
|
||||||
case strings.ToLower(ExchangeService.String()):
|
|
||||||
return ExchangeService
|
|
||||||
case strings.ToLower(OneDriveService.String()):
|
|
||||||
return OneDriveService
|
|
||||||
case strings.ToLower(SharePointService.String()):
|
|
||||||
return SharePointService
|
|
||||||
case strings.ToLower(ExchangeMetadataService.String()):
|
|
||||||
return ExchangeMetadataService
|
|
||||||
case strings.ToLower(OneDriveMetadataService.String()):
|
|
||||||
return OneDriveMetadataService
|
|
||||||
case strings.ToLower(SharePointMetadataService.String()):
|
|
||||||
return SharePointMetadataService
|
|
||||||
default:
|
|
||||||
return UnknownService
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrorUnknownCategory = clues.New("unknown category string")
|
|
||||||
|
|
||||||
// CategoryType denotes what category of data the path corresponds to. The order
|
|
||||||
// of the enums below can be changed, but the string representation of each enum
|
|
||||||
// must remain the same or migration code needs to be added to handle changes to
|
|
||||||
// the string format.
|
|
||||||
type CategoryType int
|
|
||||||
|
|
||||||
//go:generate stringer -type=CategoryType -linecomment
|
|
||||||
const (
|
|
||||||
UnknownCategory CategoryType = iota
|
|
||||||
EmailCategory // email
|
|
||||||
ContactsCategory // contacts
|
|
||||||
EventsCategory // events
|
|
||||||
FilesCategory // files
|
|
||||||
ListsCategory // lists
|
|
||||||
LibrariesCategory // libraries
|
|
||||||
PagesCategory // pages
|
|
||||||
DetailsCategory // details
|
|
||||||
)
|
|
||||||
|
|
||||||
func ToCategoryType(category string) CategoryType {
|
|
||||||
cat := strings.ToLower(category)
|
|
||||||
|
|
||||||
switch cat {
|
|
||||||
case strings.ToLower(EmailCategory.String()):
|
|
||||||
return EmailCategory
|
|
||||||
case strings.ToLower(ContactsCategory.String()):
|
|
||||||
return ContactsCategory
|
|
||||||
case strings.ToLower(EventsCategory.String()):
|
|
||||||
return EventsCategory
|
|
||||||
case strings.ToLower(FilesCategory.String()):
|
|
||||||
return FilesCategory
|
|
||||||
case strings.ToLower(LibrariesCategory.String()):
|
|
||||||
return LibrariesCategory
|
|
||||||
case strings.ToLower(ListsCategory.String()):
|
|
||||||
return ListsCategory
|
|
||||||
case strings.ToLower(PagesCategory.String()):
|
|
||||||
return PagesCategory
|
|
||||||
case strings.ToLower(DetailsCategory.String()):
|
|
||||||
return DetailsCategory
|
|
||||||
default:
|
|
||||||
return UnknownCategory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serviceCategories is a mapping of all valid service/category pairs for
|
|
||||||
// non-metadata paths.
|
|
||||||
var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
|
|
||||||
ExchangeService: {
|
|
||||||
EmailCategory: {},
|
|
||||||
ContactsCategory: {},
|
|
||||||
EventsCategory: {},
|
|
||||||
},
|
|
||||||
OneDriveService: {
|
|
||||||
FilesCategory: {},
|
|
||||||
},
|
|
||||||
SharePointService: {
|
|
||||||
LibrariesCategory: {},
|
|
||||||
ListsCategory: {},
|
|
||||||
PagesCategory: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) {
|
|
||||||
service := toServiceType(s)
|
|
||||||
if service == UnknownService {
|
|
||||||
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("service", fmt.Sprintf("%q", s))
|
|
||||||
}
|
|
||||||
|
|
||||||
category := ToCategoryType(c)
|
|
||||||
if category == UnknownCategory {
|
|
||||||
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("category", fmt.Sprintf("%q", c))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ValidateServiceAndCategory(service, category); err != nil {
|
|
||||||
return UnknownService, UnknownCategory, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return service, category, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateServiceAndCategory(service ServiceType, category CategoryType) error {
|
|
||||||
cats, ok := serviceCategories[service]
|
|
||||||
if !ok {
|
|
||||||
return clues.New("unsupported service").With("service", fmt.Sprintf("%q", service))
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := cats[category]; !ok {
|
|
||||||
return clues.New("unknown service/category combination").
|
|
||||||
With("service", fmt.Sprintf("%q", service), "category", fmt.Sprintf("%q", category))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dataLayerResourcePath allows callers to extract information from a
|
// dataLayerResourcePath allows callers to extract information from a
|
||||||
// resource-specific path. This struct is unexported so that callers are
|
// resource-specific path. This struct is unexported so that callers are
|
||||||
// forced to use the pre-defined constructors, making it impossible to create a
|
// forced to use the pre-defined constructors, making it impossible to create a
|
||||||
|
|||||||
53
src/pkg/path/service_type.go
Normal file
53
src/pkg/path/service_type.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package path
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrorUnknownService = clues.New("unknown service string")
|
||||||
|
|
||||||
|
// ServiceType denotes what service the path corresponds to. Metadata services
|
||||||
|
// are also included though they are only used for paths that house metadata for
|
||||||
|
// Corso backups.
|
||||||
|
//
|
||||||
|
// Metadata services are not considered valid service types for resource paths
|
||||||
|
// though they can be used for metadata paths.
|
||||||
|
//
|
||||||
|
// The order of the enums below can be changed, but the string representation of
|
||||||
|
// each enum must remain the same or migration code needs to be added to handle
|
||||||
|
// changes to the string format.
|
||||||
|
type ServiceType int
|
||||||
|
|
||||||
|
//go:generate stringer -type=ServiceType -linecomment
|
||||||
|
const (
|
||||||
|
UnknownService ServiceType = iota
|
||||||
|
ExchangeService // exchange
|
||||||
|
OneDriveService // onedrive
|
||||||
|
SharePointService // sharepoint
|
||||||
|
ExchangeMetadataService // exchangeMetadata
|
||||||
|
OneDriveMetadataService // onedriveMetadata
|
||||||
|
SharePointMetadataService // sharepointMetadata
|
||||||
|
)
|
||||||
|
|
||||||
|
func toServiceType(service string) ServiceType {
|
||||||
|
s := strings.ToLower(service)
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case strings.ToLower(ExchangeService.String()):
|
||||||
|
return ExchangeService
|
||||||
|
case strings.ToLower(OneDriveService.String()):
|
||||||
|
return OneDriveService
|
||||||
|
case strings.ToLower(SharePointService.String()):
|
||||||
|
return SharePointService
|
||||||
|
case strings.ToLower(ExchangeMetadataService.String()):
|
||||||
|
return ExchangeMetadataService
|
||||||
|
case strings.ToLower(OneDriveMetadataService.String()):
|
||||||
|
return OneDriveMetadataService
|
||||||
|
case strings.ToLower(SharePointMetadataService.String()):
|
||||||
|
return SharePointMetadataService
|
||||||
|
default:
|
||||||
|
return UnknownService
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -38,7 +38,7 @@ func (br backupReason) Category() path.CategoryType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (br backupReason) SubtreePath() (path.Path, error) {
|
func (br backupReason) SubtreePath() (path.Path, error) {
|
||||||
return path.ServicePrefix(
|
return path.BuildPrefix(
|
||||||
br.tenant,
|
br.tenant,
|
||||||
br.resource,
|
br.resource,
|
||||||
br.service,
|
br.service,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user