diff --git a/src/pkg/path/path.go b/src/pkg/path/path.go index 5093bcb79..71b627705 100644 --- a/src/pkg/path/path.go +++ b/src/pkg/path/path.go @@ -143,28 +143,6 @@ type Builder struct { elements Elements } -// 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...) -} - // 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 { @@ -206,6 +184,28 @@ func (pb *Builder) appendElements(escaped bool, elements []string) error { 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{} @@ -219,6 +219,7 @@ func (pb Builder) PopFront() *Builder { } } +// Dir removes the last element from the builder. func (pb Builder) Dir() *Builder { if len(pb.elements) <= 1 { return &Builder{} @@ -230,6 +231,7 @@ func (pb Builder) Dir() *Builder { } } +// LastElem returns the last element in the Builder. func (pb Builder) LastElem() string { if len(pb.elements) == 0 { return "" @@ -238,6 +240,8 @@ func (pb Builder) LastElem() string { return pb.elements[len(pb.elements)-1] } +// 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 "" @@ -268,18 +272,8 @@ func (pb Builder) Elements() Elements { return append(Elements{}, pb.elements...) } -func verifyInputValues(tenant, resourceOwner string) error { - if len(tenant) == 0 { - return clues.Stack(errMissingSegment, clues.New("tenant")) - } - - if len(resourceOwner) == 0 { - return clues.Stack(errMissingSegment, clues.New("resourceOwner")) - } - - return nil -} - +// 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 @@ -292,6 +286,8 @@ func (pb Builder) verifyPrefix(tenant, resourceOwner string) error { return 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...) @@ -299,6 +295,10 @@ func (pb Builder) withPrefix(elements ...string) *Builder { return res } +// --------------------------------------------------------------------------- +// Data Layer Path Transformers +// --------------------------------------------------------------------------- + func (pb Builder) ToStreamStorePath( tenant, purpose string, service ServiceType, @@ -377,21 +377,6 @@ func (pb Builder) ToServiceCategoryMetadataPath( }, nil } -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) -} - func (pb Builder) ToDataLayerPath( tenant, user string, service ServiceType, @@ -442,6 +427,57 @@ func (pb Builder) ToDataLayerSharePointPath( 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 // 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 @@ -479,6 +515,102 @@ func FromDataLayerPath(p string, isItem bool) (Path, error) { }, nil } +// TrimTrailingSlash takes an escaped path element and returns an escaped path +// element with the trailing path separator character(s) removed if they were not +// escaped. If there were no trailing path separator character(s) or the separator(s) +// were escaped the input is returned unchanged. +func TrimTrailingSlash(element string) string { + for len(element) > 0 && element[len(element)-1] == PathSeparator { + lastIdx := len(element) - 1 + numSlashes := 0 + + for i := lastIdx - 1; i >= 0; i-- { + if element[i] != escapeCharacter { + break + } + + numSlashes++ + } + + if numSlashes%2 != 0 { + break + } + + element = element[:lastIdx] + } + + return element +} + +// split takes an escaped string and returns a slice of path elements. The +// string is split on the path separator according to the escaping rules. The +// provided string must not contain an unescaped trailing path separator. +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 { + prevWasSeparator = false + numEscapes++ + + 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. + res = append(res, segment[startIdx:]) + + return res +} + +// --------------------------------------------------------------------------- +// Unexported Helpers +// --------------------------------------------------------------------------- + +func verifyInputValues(tenant, resourceOwner string) error { + if len(tenant) == 0 { + return clues.Stack(errMissingSegment, clues.New("tenant")) + } + + if len(resourceOwner) == 0 { + return clues.Stack(errMissingSegment, clues.New("resourceOwner")) + } + + return nil +} + // 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. @@ -574,33 +706,6 @@ func validateEscapedElement(element string) error { return nil } -// TrimTrailingSlash takes an escaped path element and returns an escaped path -// element with the trailing path separator character(s) removed if they were not -// escaped. If there were no trailing path separator character(s) or the separator(s) -// were escaped the input is returned unchanged. -func TrimTrailingSlash(element string) string { - for len(element) > 0 && element[len(element)-1] == PathSeparator { - lastIdx := len(element) - 1 - numSlashes := 0 - - for i := lastIdx - 1; i >= 0; i-- { - if element[i] != escapeCharacter { - break - } - - numSlashes++ - } - - if numSlashes%2 != 0 { - break - } - - element = element[:lastIdx] - } - - return element -} - // join returns a string containing the given elements joined by the path // separator '/'. func join(elements []string) string { @@ -608,88 +713,3 @@ func join(elements []string) string { // '\' according to the escaping rules. return strings.Join(elements, string(PathSeparator)) } - -// split takes an escaped string and returns a slice of path elements. The -// string is split on the path separator according to the escaping rules. The -// provided string must not contain an unescaped trailing path separator. -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 { - prevWasSeparator = false - numEscapes++ - - 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. - res = append(res, segment[startIdx:]) - - return res -} - -// --------------------------------------------------------------------------- -// 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() -} diff --git a/src/pkg/path/resource_path.go b/src/pkg/path/resource_path.go index a352b34ab..bcf0cfaa8 100644 --- a/src/pkg/path/resource_path.go +++ b/src/pkg/path/resource_path.go @@ -237,6 +237,8 @@ func (rp dataLayerResourcePath) Item() string { return "" } +// Dir removes the last element from the path. If this would remove a +// value that is part of the standard prefix structure, an error is returned. func (rp dataLayerResourcePath) Dir() (Path, error) { if len(rp.elements) <= 4 { return nil, clues.New("unable to shorten path").With("path", rp)