179 lines
4.9 KiB
Go
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))
|
|
}
|