blob: 45c89d28d6899d7d9e0c2f572ebd53a213737457 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package normalpath provides functions similar to filepath.
//
// A normalized path is a cleaned and to-slash'ed path.
// A validated path validates that a path is relative and does not jump context.
package normalpath
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
)
import (
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/stringutil"
)
const (
// Relative is the PathType for normalized and validated paths.
Relative PathType = 1
// Absolute is the PathType for normalized and absolute paths.
Absolute PathType = 2
stringOSPathSeparator = string(os.PathSeparator)
// This has to be with "/" instead of os.PathSeparator as we use this on normalized paths
normalizedRelPathJumpContextPrefix = "../"
)
var (
// errNotRelative is the error returned if the path is not relative.
errNotRelative = errors.New("expected to be relative")
// errOutsideContextDir is the error returned if the path is outside the context directory.
errOutsideContextDir = errors.New("is outside the context directory")
)
// PathType is a terminate type for path comparisons.
type PathType int
// Separator gets the string value of the separator.
//
// TODO: rename to Terminator if we keep this
// TODO: we should probably refactor so we never need to use absolute paths at all
// this could be accomplished if we could for ExternalPathToRelPath on buckets
func (t PathType) Separator() string {
switch t {
case Relative:
return "."
case Absolute:
return "/"
default:
return ""
}
}
// Error is a path error.
type Error struct {
Path string
Err error
}
// NewError returns a new Error.
func NewError(path string, err error) *Error {
return &Error{
Path: path,
Err: err,
}
}
// Error implements error.
func (e *Error) Error() string {
errString := ""
if e.Err != nil {
errString = e.Err.Error()
}
if errString == "" {
errString = "error"
}
return e.Path + ": " + errString
}
// Unwrap implements errors.Unwrap for Error.
func (e *Error) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
// Normalize normalizes the given path.
//
// This calls filepath.Clean and filepath.ToSlash on the path.
// If the path is "" or ".", this returns ".".
func Normalize(path string) string {
return filepath.ToSlash(filepath.Clean(path))
}
// NormalizeAndAbsolute normalizes the path and makes it absolute.
func NormalizeAndAbsolute(path string) (string, error) {
absPath, err := filepath.Abs(Unnormalize(path))
if err != nil {
return "", err
}
return Normalize(absPath), nil
}
// NormalizeAndTransformForPathType calls NormalizeAndValidate for relative
// paths, and NormalizeAndAbsolute for absolute paths.
func NormalizeAndTransformForPathType(path string, pathType PathType) (string, error) {
switch pathType {
case Relative:
return NormalizeAndValidate(path)
case Absolute:
return NormalizeAndAbsolute(path)
default:
return "", fmt.Errorf("unknown PathType: %v", pathType)
}
}
// Unnormalize unnormalizes the given path.
//
// This calls filepath.FromSlash on the path.
// If the path is "", this returns "".
func Unnormalize(path string) string {
return filepath.FromSlash(path)
}
// Base is equivalent to filepath.Base.
//
// Normalizes before returning.
func Base(path string) string {
return Normalize(filepath.Base(Unnormalize(path)))
}
// Dir is equivalent to filepath.Dir.
//
// Normalizes before returning.
func Dir(path string) string {
return Normalize(filepath.Dir(Unnormalize(path)))
}
// Ext is equivalent to filepath.Ext.
//
// Can return empty string.
func Ext(path string) string {
return filepath.Ext(Unnormalize(path))
}
// Join is equivalent to filepath.Join.
//
// Empty strings are ignored,
// Can return empty string.
//
// Normalizes before returning otherwise.
func Join(paths ...string) string {
unnormalized := make([]string, len(paths))
for i, path := range paths {
unnormalized[i] = Unnormalize(path)
}
value := filepath.Join(unnormalized...)
if value == "" {
return ""
}
return Normalize(value)
}
// Rel is equivalent to filepath.Rel.
//
// Can return empty string, especially on error.
//
// Normalizes before returning otherwise.
func Rel(basepath string, targpath string) (string, error) {
path, err := filepath.Rel(Unnormalize(basepath), Unnormalize(targpath))
if path == "" {
return "", err
}
return Normalize(path), err
}
// ByDir maps the paths into a map from directory via Dir to the original paths.
//
// The paths for each value slice will be sorted.
//
// The path is expected to be normalized.
func ByDir(paths ...string) map[string][]string {
m := make(map[string][]string)
for _, path := range paths {
path = Normalize(path)
dir := filepath.Dir(path)
m[dir] = append(m[dir], path)
}
for _, dirPaths := range m {
sort.Strings(dirPaths)
}
return m
}
// ChunkByDir splits paths into chunks of around the suggestedChunkSize while keeping
// directory affinity, i.e. paths in the same directory will be in the same chunk.
//
// Paths are expected to be normalized.
// If paths is nil or empty, returns empty.
// If suggestedChunkSize is <=0, returns [][]string{paths}.
func ChunkByDir(paths []string, suggestedChunkSize int) [][]string {
var chunks [][]string
if len(paths) == 0 {
return chunks
}
if suggestedChunkSize <= 0 {
return [][]string{paths}
}
dirToPaths := ByDir(paths...)
// Get the keys and sort them to keep the output of this stable
dirs := make([]string, 0, len(dirToPaths))
for dir := range dirToPaths {
dirs = append(dirs, dir)
}
sort.Strings(dirs)
// Create a slice of the per-directory path slices
pathsByDir := make([][]string, 0, len(dirToPaths))
for _, dir := range dirs {
pathsByDir = append(pathsByDir, dirToPaths[dir])
}
// Sort the per-directory path slices by length, from greatest to smallest
// We do greatest to smallest because we pop the greatest off first, and
// This will approximate keeping things in alphabetical order given that
// pathsByDir is in alphabetical order of directories.
sort.SliceStable(
pathsByDir,
func(i int, j int) bool {
return len(pathsByDir[i]) > len(pathsByDir[j])
},
)
for len(pathsByDir) > 0 {
// Take the largest set of paths and remove from pathsByDir
chunk := pathsByDir[0]
pathsByDir = pathsByDir[1:]
// While our current chunk is less than the suggestedChunkSize, take the smallest
// set of paths and append them to the chunk, and then remove them from pathsByDir
//
// This will mean that sometimes we have chunks larger than suggestedChunkSize, but
// this is best effort. We could likely use more-complicated algorithms here that
// smartly combine per-directory path sets that minimize the distance from
// suggestedChunkSize, as there are adversary cases to this algorithm, but we
// are not diving into that at the moment unless there is a need.
for len(chunk) < suggestedChunkSize && len(pathsByDir) > 0 {
chunk = append(chunk, pathsByDir[len(pathsByDir)-1]...)
pathsByDir = pathsByDir[:len(pathsByDir)-1]
}
// Append the chunk to the list of chunks
chunks = append(chunks, chunk)
}
return chunks
}
// ContainsPath returns true if the dirPath contains the path.
//
// The path and value are expected to be normalized and validated if Relative is used.
// The path and value are expected to be normalized and absolute if Absolute is used.
//
// For a given dirPath:
//
// - If path == PathType, dirPath does not contain the path.
// - If dirPath == PathType, the dirPath contains the path.
// - If dirPath is a directory that contains path, this returns true.
func ContainsPath(dirPath string, path string, pathType PathType) bool {
if dirPath == path {
return false
}
return EqualsOrContainsPath(dirPath, Dir(path), pathType)
}
// MapAllEqualOrContainingPaths returns the matching paths in the map in a sorted slice.
//
// The path and keys in m are expected to be normalized and validated if Relative is used.
// The path and keys in m are expected to be normalized and absolute if Absolute is used.
//
// If the map is empty, returns nil.
func MapAllEqualOrContainingPaths(m map[string]struct{}, path string, pathType PathType) []string {
if len(m) == 0 {
return nil
}
return stringutil.MapToSortedSlice(MapAllEqualOrContainingPathMap(m, path, pathType))
}
// StripComponents strips the specified number of components.
//
// Path expected to be normalized.
// Returns false if the path does not have more than the specified number of components.
func StripComponents(path string, countUint32 uint32) (string, bool) {
count := int(countUint32)
if count == 0 {
return path, true
}
components := Components(path)
if len(components) <= count {
return "", false
}
return Join(components[count:]...), true
}
// ValidatePathComponent validates that the string is a valid
// component of a path, e.g. it can be Joined and form a valid path.
func ValidatePathComponent(component string) error {
if component == "" {
return errors.New("path component must not be empty")
}
if strings.ContainsRune(component, '/') {
return errors.New(`path component must not contain "/" `)
}
if strings.Contains(component, "..") {
return errors.New(`path component must not contain ".."`)
}
if url.PathEscape(component) != component {
return fmt.Errorf(
"path component must match its URL escaped version: %q did not match %q",
component,
url.PathEscape(component),
)
}
return nil
}
// ValidatePathComponents validates that all the strings are valid
// components of a path, e.g. they can be Joined and form a valid path.
func ValidatePathComponents(components ...string) error {
for _, component := range components {
if err := ValidatePathComponent(component); err != nil {
return err
}
}
return nil
}
// ValidatePathsNormalizedValidatedUnique validates the file or diretory paths are normalized
// and validated, and not duplicated.
func ValidatePathsNormalizedValidatedUnique(paths []string) error {
pathMap := make(map[string]struct{}, len(paths))
for _, path := range paths {
if path == "" {
return errors.New("path is empty")
}
normalized, err := NormalizeAndValidate(path)
if err != nil {
return fmt.Errorf("path had normalization error: %w", err)
}
if path != normalized {
return fmt.Errorf("path %s was not normalized to %s", path, normalized)
}
if _, ok := pathMap[path]; ok {
return fmt.Errorf("duplicate path: %s", path)
}
pathMap[path] = struct{}{}
}
return nil
}