ashmrtn ea3c9c035e
Implement and test one path constructor and join (#398)
Implement and test one path constructor and join
2022-07-25 20:31:09 +00:00

179 lines
4.9 KiB
Go

// Package path provides a set of functions for wrangling paths from the outside
// world into paths that corso can understand. Paths use the standard Unix path
// separator character '/'. If for some reason an individual element in a raw
// path contains the '/' character, it should be escaped with '\'. If the path
// contains '\' it should be escaped by turning it into '\\'.
//
// Paths can be split into elements by splitting on '/' if the '/' is not
// escaped. Additionally, corso may operate on segments in a path. Segments are
// made up of one or more path elements.
//
// Examples of paths splitting by elements and canonicalization with escaping:
// 1.
// input path: `this/is/a/path`
// elements of path: `this`, `is`, `a`, `path`
// 2.
// input path: `this/is\/a/path`
// elements of path: `this`, `is/a`, `path`
// 3.
// input path: `this/is\\/a/path`
// elements of path: `this`, `is\`, `a`, `path`
// 4.
// input path: `this/is\\\/a/path`
// elements of path: `this`, `is\/a`, `path`
// 5.
// input path: `this/is//a/path`
// elements of path: `this`, `is`, `a`, `path`
// 6.
// input path: `this/is\//a/path`
// elements of path: `this`, `is/`, `a`, `path`
// 7.
// input path: `this/is/a/path/`
// elements of path: `this`, `is`, `a`, `path`
// 8.
// input path: `this/is/a/path\/`
// elements of path: `this`, `is`, `a`, `path/`
package path
import (
"strings"
"github.com/pkg/errors"
)
const (
escapeCharacter = '\\'
pathSeparator = '/'
)
var (
charactersToEscape = map[rune]struct{}{
pathSeparator: {},
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.
// For now, adding generic functions to pull information from segments.
// Resources that don't have the requested information should return an empty
// string.
type Path interface {
String() string
Tenant() string
User() string
Folder() string
Item() string
}
type Base struct {
// Escaped path 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 is escaped.
func newPath(segments [][]string) Base {
if len(segments) == 0 {
return Base{}
}
res := Base{segmentIdx: make([]int, 0, len(segments))}
idx := 0
for _, s := range segments {
res.segmentIdx = append(res.segmentIdx, idx)
for _, e := range s {
if len(e) == 0 {
continue
}
res.elements = append(res.elements, escapeElement(e))
idx++
}
}
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.
func newPathFromEscapedSegments(segments []string) (Base, error) {
return Base{}, errors.New("not implemented")
}
// 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.
func (p Base) unescapedSegmentElements(n int) []string {
return nil
}
// 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
}
func escapeElement(element string) string {
escapeIdx := make([]int, 0)
for i, c := range element {
if _, ok := charactersToEscape[c]; ok {
escapeIdx = append(escapeIdx, i)
}
}
if len(escapeIdx) == 0 {
return element
}
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
}
// Add the end of the element after the last escape character.
b.WriteString(element[startIdx:])
return b.String()
}
// join returns a string containing the given elements joined by the path
// separator '/'.
func join(elements []string) string {
// Have to use strings because path package does not handle escaped '/' and
// '\' according to the escaping rules.
return strings.Join(elements, string(pathSeparator))
}