| // 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. |
| |
| //go:build windows |
| // +build windows |
| |
| package normalpath |
| |
| import ( |
| "os" |
| "path/filepath" |
| "strings" |
| ) |
| |
| // NormalizeAndValidate normalizes and validates the given path. |
| // |
| // This calls Normalize on the path. |
| // Returns Error if the path is not relative or jumps context. |
| // This can be used to validate that paths are valid to use with Buckets. |
| // The error message is safe to pass to users. |
| func NormalizeAndValidate(path string) (string, error) { |
| normalizedPath := Normalize(path) |
| if filepath.IsAbs(normalizedPath) || (len(normalizedPath) > 0 && normalizedPath[0] == '/') { |
| // the stdlib implementation of `IsAbs` assumes that a volume name is required for a path to |
| // be absolute, however Windows treats a `/` (normalized) rooted path as absolute _within_ the current volume. |
| // In the context of validating that a path is _not_ relative, we need to reject a path that begins |
| // with `/`. |
| return "", NewError(path, errNotRelative) |
| } |
| // https://github.com/ProtobufMan/bufman-cli/issues/51 |
| if strings.HasPrefix(normalizedPath, normalizedRelPathJumpContextPrefix) { |
| return "", NewError(path, errOutsideContextDir) |
| } |
| return normalizedPath, nil |
| } |
| |
| // EqualsOrContainsPath returns true if the value is equal to or contains the path. |
| // path is compared at each directory level to value for equivalency under simple unicode |
| // codepoint folding. This means it is context and locale independent. This matching |
| // will not support the few rare cases, primarily in Turkish and Lithuanian, noted |
| // in the caseless matching section of Unicode 13.0 https://www.unicode.org/versions/Unicode13.0.0/ch05.pdf#page=47. |
| // |
| // 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. |
| func EqualsOrContainsPath(value string, path string, pathType PathType) bool { |
| curPath := path |
| var lastSeen string |
| for { |
| if strings.EqualFold(value, curPath) { |
| return true |
| } |
| curPath = Dir(curPath) |
| if lastSeen == curPath { |
| break |
| } |
| lastSeen = curPath |
| } |
| return false |
| } |
| |
| // MapHasEqualOrContainingPath returns true if the path matches any file or directory in the map. |
| // |
| // 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 false. |
| func MapHasEqualOrContainingPath(m map[string]struct{}, path string, pathType PathType) bool { |
| if len(m) == 0 { |
| return false |
| } |
| |
| for value := range m { |
| if EqualsOrContainsPath(value, path, pathType) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| // MapAllEqualOrContainingPathMap returns the paths in m that are equal to, or contain |
| // path, in a new map. |
| // |
| // 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 MapAllEqualOrContainingPathMap(m map[string]struct{}, path string, pathType PathType) map[string]struct{} { |
| if len(m) == 0 { |
| return nil |
| } |
| |
| n := make(map[string]struct{}) |
| |
| for potentialMatch := range m { |
| if EqualsOrContainsPath(potentialMatch, path, pathType) { |
| n[potentialMatch] = struct{}{} |
| } |
| } |
| return n |
| } |
| |
| // Components splits the path into its components. |
| // |
| // This calls filepath.Split repeatedly. |
| // |
| // The path is expected to be normalized. |
| func Components(path string) []string { |
| var components []string |
| |
| if len(path) < 1 { |
| return []string{"."} |
| } |
| |
| dir := Unnormalize(path) |
| |
| volumeComponent := filepath.VolumeName(dir) |
| if len(volumeComponent) > 0 { |
| // On Windows the volume of an absolute path could be one of the following 3 forms |
| // c.f. https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#fully-qualified-vs-relative-paths |
| // * A disk designator: `C:\` |
| // * A UNC Path: `\\servername\share\` |
| // c.f. https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dfsc/149a3039-98ce-491a-9268-2f5ddef08192 |
| // * A "current volume absolute path" `\` |
| // This refers to the root of the current volume |
| // |
| // We do not support paths with string parsing disabled such as |
| // `\\?\path` |
| // |
| // If we did extract a volume name, we need to add a path separator to turn it into |
| // a path component. Volume Names without path separators have an implied "current directory" |
| // when performing a join operation, or using them as a path directly, which is not the |
| // intention of `Split` so we ensure they always mean "the root of this volume". |
| volumeComponent = volumeComponent + stringOSPathSeparator |
| } |
| if len(volumeComponent) < 1 && dir[0] == os.PathSeparator { |
| // If we didn't extract a volume name then the path is either |
| // absolute and starts with an os.PathSeparator (it must be exactly 1 |
| // otherwise its a UNC path and we would have found a volume above) or it is relative. |
| // If it is absolute, we set the expected volume component to os.PathSeparator. |
| // otherwise we leave it as an empty string. |
| volumeComponent = stringOSPathSeparator |
| } |
| for { |
| var file string |
| dir, file = filepath.Split(dir) |
| // puts in reverse |
| components = append(components, file) |
| |
| if dir == volumeComponent { |
| if volumeComponent != "" { |
| components = append(components, dir) |
| } |
| break |
| } |
| |
| dir = strings.TrimSuffix(dir, stringOSPathSeparator) |
| } |
| |
| // https://github.com/golang/go/wiki/SliceTricks#reversing |
| for i := len(components)/2 - 1; i >= 0; i-- { |
| opp := len(components) - 1 - i |
| components[i], components[opp] = components[opp], components[i] |
| } |
| for i, component := range components { |
| components[i] = Normalize(component) |
| } |
| |
| return components |
| } |