Files
mgmt/util/safepath/safepath.go
2024-03-05 01:05:50 -05:00

1073 lines
32 KiB
Go

// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
// This particular file is dual-licensed. It's available under the GNU LGPL-3.0+
// and the GNU GPL-3.0+ so that it can be used by other projects easily. If it's
// popular we can consider spinning it out into its own separate git repository.
// SPDX-License-Identifier: GPL-3.0+ OR LGPL-3.0+
// Package safepath implements some types and methods for dealing with POSIX
// file paths safely. When golang programmers use strings to express paths, it
// can sometimes be confusing whether a particular string represents either a
// file or a directory, and whether it is absolute or relative. This package
// provides a type for each of these, and safe methods to manipulate them all.
// The convention is that directories must end with a slash, and absolute paths
// must start with one. There are no generic "path" types for now, you must be
// more specific when using this library. If you can't discern exactly what you
// are, then use a string. It is your responsibility to build the type correctly
// and to call Validate on them to ensure you've done it correctly. If you
// don't, then you could cause a panic.
//
// As a reminder, this library knows nothing about the special path characters
// like period (as in ./) and it only knows about two periods (..) in so far as
// it uses the stdlib Clean method when pulling in new paths.
//
// The ParseInto* family of functions will sometimes add or remove a trailing
// slash to ensure you get a directory or file. It is recommended that you make
// sure to verify your desired type is what you expect before calling this.
package safepath
// NOTE: I started the design of this library by thinking about what types I
// wanted. Absolute and relative files and directories. I don't think there's a
// need to handle symlinks at the moment. This means I'll need four types. Next
// I need to work out what possible operations are valid. My sketch of that is:
//
// join(absdir, relfile) => absfile
// join(absdir, reldir) => absdir
// join(reldir, relfile) => relfile
// join(reldir, reldir) => reldir
// join(absdir, absdir) => error
// join(absdir, absfile) => error
// join(absfile, absfile) => error
// join(relfile, relfile) => error
//
// This isn't Haskell, so I'll either need four separate functions or one join
// function that takes interfaces. The latter would be a big unsafe mess. As it
// turns out, there is exactly one join operation that produces each of
// the four types. So instead of naming each one like `JoinAbsDirRelFile` and so
// on, I decided to name them based on their return type.
//
// Be consistent! Order: file, dir, abs, rel, absfile, absdir, relfile, reldir.
//
// This could probably get spun off into it's own standalone library.
import (
"fmt"
stdlibPath "path"
"strings"
)
// Path represents any absolute or relative file or directory.
type Path interface {
fmt.Stringer
Path() string
IsDir() bool
IsAbs() bool
isPath() // private to prevent others from implementing this (ok?)
}
// File represents either an absolute or relative file. Directories are not
// included.
type File interface {
fmt.Stringer
Path() string
//IsDir() bool // TODO: add this to allow a File to become a Path?
IsAbs() bool
isFile() // only the files have this
}
// Dir represents either an absolute or relative directory. Files are not
// included.
type Dir interface {
fmt.Stringer
Path() string
//IsDir() bool // TODO: add this to allow a Dir to become a Path?
IsAbs() bool
isDir() // only the dirs have this
}
// Abs represents an absolute file or directory. Relative paths are not
// included.
type Abs interface {
fmt.Stringer
Path() string
IsDir() bool
//IsAbs() bool // TODO: add this to allow an Abs to become a Path?
isAbs() // only the abs have this
}
// Rel represents a relative file or directory. Absolute paths are not included.
type Rel interface {
fmt.Stringer
Path() string
IsDir() bool
//IsAbs() bool // TODO: add this to allow a Rel to become a Path?
isRel() // only the rel have this
}
// AbsFile represents an absolute file path.
type AbsFile struct {
path string
}
func (obj AbsFile) isAbs() {}
func (obj AbsFile) isFile() {}
func (obj AbsFile) isPath() {}
// String returns the canonical "friendly" representation of this path. If it is
// a directory, then it will end with a slash.
func (obj AbsFile) String() string { return obj.path }
// Path returns the cleaned version of this path. It is what you expect after
// running the golang path cleaner on the internal representation.
func (obj AbsFile) Path() string { return stdlibPath.Clean(obj.path) }
// IsDir returns false for this struct.
func (obj AbsFile) IsDir() bool { return false }
// IsAbs returns true for this struct.
func (obj AbsFile) IsAbs() bool { return true }
// Validate returns an error if the path was not specified correctly.
func (obj AbsFile) Validate() error {
if !strings.HasPrefix(obj.path, "/") {
return fmt.Errorf("file is not absolute")
}
if strings.HasSuffix(obj.path, "/") {
return fmt.Errorf("path is not a file")
}
return nil
}
// PanicValidate panics if the path was not specified correctly.
func (obj AbsFile) PanicValidate() {
if err := obj.Validate(); err != nil {
panic(err.Error())
}
}
// Cmp compares two AbsFile's and returns nil if they have the same path.
func (obj AbsFile) Cmp(absFile AbsFile) error {
if obj.path != absFile.path {
return fmt.Errorf("files differ")
}
return nil
}
// Base returns the last component of the AbsFile, in this case, the filename.
func (obj AbsFile) Base() RelFile {
obj.PanicValidate()
ix := strings.LastIndex(obj.path, "/")
return RelFile{
path: obj.path[ix+1:],
}
}
// Dir returns the head component of the AbsFile, in this case, the directory.
func (obj AbsFile) Dir() AbsDir {
obj.PanicValidate()
ix := strings.LastIndex(obj.path, "/")
if ix == 0 {
return AbsDir{
path: "/",
}
}
return AbsDir{
path: obj.path[0:ix],
}
}
// HasDir returns true if the input relative dir is present in the path.
func (obj AbsFile) HasDir(relDir RelDir) bool {
obj.PanicValidate()
relDir.PanicValidate()
//if obj.path == "/" {
// return false
//}
// TODO: test with ""
i := strings.Index(obj.path, relDir.path)
if i == -1 {
return false // not found
}
if i == 0 {
// not possible unless relDir is /
//return false // found the root dir
panic("relDir was root which isn't relative")
}
// We want to make sure we land on a split char, or we didn't match it.
// We don't need to check the last char, because we know it's a /
return obj.path[i-1] == '/' // check if the char before is a /
}
// HasExt checks if the file ends with the given extension. It checks for an
// exact string match. You might prefer using HasExtInsensitive instead. As a
// special case, if you pass in an empty string as the extension to match, this
// will return false.
// TODO: add tests
func (obj AbsFile) HasExt(ext string) bool {
obj.PanicValidate()
if ext == "" { // special case, not consistent with strings.HasSuffix
return false
}
if !strings.HasSuffix(obj.path, ext) {
return false
}
return true
}
// HasExtInsensitive checks if the file ends with the given extension. It checks
// with a fancy case-insensitive match. As a special case, if you pass in an
// empty string as the extension to match, this will return false.
func (obj AbsFile) HasExtInsensitive(ext string) bool {
obj.PanicValidate()
return hasExtInsensitive(obj.path, ext)
}
// ParseIntoAbsFile takes an input path and ensures it's an AbsFile. It doesn't
// do anything particularly magical. It then runs Validate to ensure the path
// was valid overall. It also runs the stdlib path Clean function on it. Please
// note, that passing in the root slash / will cause this to fail.
func ParseIntoAbsFile(path string) (AbsFile, error) {
if path == "" {
return AbsFile{}, fmt.Errorf("path is empty")
}
path = stdlibPath.Clean(path)
absFile := AbsFile{path: path}
return absFile, absFile.Validate()
}
// UnsafeParseIntoAbsFile performs exactly as ParseIntoAbsFile does, but it
// panics if the latter would have returned an error.
func UnsafeParseIntoAbsFile(path string) AbsFile {
absFile, err := ParseIntoAbsFile(path)
if err != nil {
panic(err.Error())
}
return absFile
}
// AbsDir represents an absolute dir path.
type AbsDir struct {
path string
}
func (obj AbsDir) isAbs() {}
func (obj AbsDir) isDir() {}
func (obj AbsDir) isPath() {}
// String returns the canonical "friendly" representation of this path. If it is
// a directory, then it will end with a slash.
func (obj AbsDir) String() string { return obj.path }
// Path returns the cleaned version of this path. It is what you expect after
// running the golang path cleaner on the internal representation.
func (obj AbsDir) Path() string { return stdlibPath.Clean(obj.path) }
// IsDir returns true for this struct.
func (obj AbsDir) IsDir() bool { return true }
// IsAbs returns true for this struct.
func (obj AbsDir) IsAbs() bool { return true }
// Validate returns an error if the path was not specified correctly.
func (obj AbsDir) Validate() error {
if !strings.HasPrefix(obj.path, "/") {
return fmt.Errorf("dir is not absolute")
}
if !strings.HasSuffix(obj.path, "/") {
return fmt.Errorf("path is not a dir")
}
return nil
}
// PanicValidate panics if the path was not specified correctly.
func (obj AbsDir) PanicValidate() {
if err := obj.Validate(); err != nil {
panic(err.Error())
}
}
// Cmp compares two AbsDir's and returns nil if they have the same path.
func (obj AbsDir) Cmp(absDir AbsDir) error {
if obj.path != absDir.path {
return fmt.Errorf("dirs differ")
}
return nil
}
// HasDir returns true if the input relative dir is present in the path.
func (obj AbsDir) HasDir(relDir RelDir) bool {
obj.PanicValidate()
relDir.PanicValidate()
if obj.path == "/" {
return false
}
// TODO: test with ""
i := strings.Index(obj.path, relDir.path)
if i == -1 {
return false // not found
}
if i == 0 {
// not possible unless relDir is /
//return false // found the root dir
panic("relDir was root which isn't relative")
}
// We want to make sure we land on a split char, or we didn't match it.
// We don't need to check the last char, because we know it's a /
return obj.path[i-1] == '/' // check if the char before is a /
}
// HasDirOne returns true if the input relative dir is present in the path. It
// only works with a single dir as relDir, so it won't work if relDir is `a/b/`.
func (obj AbsDir) HasDirOne(relDir RelDir) bool {
obj.PanicValidate()
relDir.PanicValidate()
if obj.path == "/" {
return false
}
// TODO: test with ""
sa := strings.Split(obj.path, "/")
for i := 1; i < len(sa)-1; i++ {
p := sa[i] + "/"
if p == relDir.path {
return true
}
}
return false
}
// ParseIntoAbsDir takes an input path and ensures it's an AbsDir, by adding a
// trailing slash if it's missing. It then runs Validate to ensure the path was
// valid overall. It also runs the stdlib path Clean function on it.
func ParseIntoAbsDir(path string) (AbsDir, error) {
if path == "" {
return AbsDir{}, fmt.Errorf("path is empty")
}
path = stdlibPath.Clean(path)
// NOTE: after clean we won't have a trailing slash I think ;)
if !strings.HasSuffix(path, "/") { // add trailing slash if missing
path += "/"
}
absDir := AbsDir{path: path}
return absDir, absDir.Validate()
}
// UnsafeParseIntoAbsDir performs exactly as ParseIntoAbsDir does, but it panics
// if the latter would have returned an error.
func UnsafeParseIntoAbsDir(path string) AbsDir {
absDir, err := ParseIntoAbsDir(path)
if err != nil {
panic(err.Error())
}
return absDir
}
// RelFile represents a relative file path.
type RelFile struct {
path string
}
func (obj RelFile) isRel() {}
func (obj RelFile) isFile() {}
func (obj RelFile) isPath() {}
// String returns the canonical "friendly" representation of this path. If it is
// a directory, then it will end with a slash.
func (obj RelFile) String() string { return obj.path }
// Path returns the cleaned version of this path. It is what you expect after
// running the golang path cleaner on the internal representation.
func (obj RelFile) Path() string { return stdlibPath.Clean(obj.path) }
// IsDir returns false for this struct.
func (obj RelFile) IsDir() bool { return false }
// IsAbs returns false for this struct.
func (obj RelFile) IsAbs() bool { return false }
// Validate returns an error if the path was not specified correctly.
func (obj RelFile) Validate() error {
if strings.HasPrefix(obj.path, "/") {
return fmt.Errorf("file is not relative")
}
if strings.HasSuffix(obj.path, "/") {
return fmt.Errorf("path is not a file")
}
if obj.path == "" {
return fmt.Errorf("path is empty")
}
return nil
}
// PanicValidate panics if the path was not specified correctly.
func (obj RelFile) PanicValidate() {
if err := obj.Validate(); err != nil {
panic(err.Error())
}
}
// Cmp compares two RelFile's and returns nil if they have the same path.
func (obj RelFile) Cmp(relfile RelFile) error {
if obj.path != relfile.path {
return fmt.Errorf("files differ")
}
return nil
}
// HasDir returns true if the input relative dir is present in the path.
func (obj RelFile) HasDir(relDir RelDir) bool {
obj.PanicValidate()
relDir.PanicValidate()
//if obj.path == "/" {
// return false
//}
// TODO: test with ""
i := strings.Index(obj.path, relDir.path)
if i == -1 {
return false // not found
}
if i == 0 {
return true // found at the beginning
}
// We want to make sure we land on a split char, or we didn't match it.
// We don't need to check the last char, because we know it's a /
return obj.path[i-1] == '/' // check if the char before is a /
}
// HasExt checks if the file ends with the given extension. It checks for an
// exact string match. You might prefer using HasExtInsensitive instead. As a
// special case, if you pass in an empty string as the extension to match, this
// will return false.
// TODO: add tests
func (obj RelFile) HasExt(ext string) bool {
obj.PanicValidate()
if ext == "" { // special case, not consistent with strings.HasSuffix
return false
}
if !strings.HasSuffix(obj.path, ext) {
return false
}
return true
}
// HasExtInsensitive checks if the file ends with the given extension. It checks
// with a fancy case-insensitive match. As a special case, if you pass in an
// empty string as the extension to match, this will return false.
func (obj RelFile) HasExtInsensitive(ext string) bool {
obj.PanicValidate()
return hasExtInsensitive(obj.path, ext)
}
// ParseIntoRelFile takes an input path and ensures it's an RelFile. It doesn't
// do anything particularly magical. It then runs Validate to ensure the path
// was valid overall. It also runs the stdlib path Clean function on it.
func ParseIntoRelFile(path string) (RelFile, error) {
if path == "" {
return RelFile{}, fmt.Errorf("path is empty")
}
path = stdlibPath.Clean(path)
relFile := RelFile{path: path}
return relFile, relFile.Validate()
}
// UnsafeParseIntoRelFile performs exactly as ParseIntoRelFile does, but it
// panics if the latter would have returned an error.
func UnsafeParseIntoRelFile(path string) RelFile {
relFile, err := ParseIntoRelFile(path)
if err != nil {
panic(err.Error())
}
return relFile
}
// RelDir represents a relative dir path.
type RelDir struct {
path string
}
func (obj RelDir) isRel() {}
func (obj RelDir) isDir() {}
func (obj RelDir) isPath() {}
// String returns the canonical "friendly" representation of this path. If it is
// a directory, then it will end with a slash.
func (obj RelDir) String() string { return obj.path }
// Path returns the cleaned version of this path. It is what you expect after
// running the golang path cleaner on the internal representation.
func (obj RelDir) Path() string { return stdlibPath.Clean(obj.path) }
// IsDir returns true for this struct.
func (obj RelDir) IsDir() bool { return true }
// IsAbs returns false for this struct.
func (obj RelDir) IsAbs() bool { return false }
// Validate returns an error if the path was not specified correctly.
func (obj RelDir) Validate() error {
if strings.HasPrefix(obj.path, "/") {
return fmt.Errorf("dir is not relative")
}
if !strings.HasSuffix(obj.path, "/") {
return fmt.Errorf("path is not a dir")
}
return nil
}
// PanicValidate panics if the path was not specified correctly.
func (obj RelDir) PanicValidate() {
if err := obj.Validate(); err != nil {
panic(err.Error())
}
}
// Cmp compares two RelDir's and returns nil if they have the same path.
func (obj RelDir) Cmp(relDir RelDir) error {
if obj.path != relDir.path {
return fmt.Errorf("dirs differ")
}
return nil
}
// HasDir returns true if the input relative dir is present in the path.
func (obj RelDir) HasDir(relDir RelDir) bool {
obj.PanicValidate()
relDir.PanicValidate()
//if obj.path == "/" {
// return false
//}
// TODO: test with ""
i := strings.Index(obj.path, relDir.path)
if i == -1 {
return false // not found
}
if i == 0 {
return true // found at the beginning
}
// We want to make sure we land on a split char, or we didn't match it.
// We don't need to check the last char, because we know it's a /
return obj.path[i-1] == '/' // check if the char before is a /
}
// HasDirOne returns true if the input relative dir is present in the path. It
// only works with a single dir as relDir, so it won't work if relDir is `a/b/`.
func (obj RelDir) HasDirOne(relDir RelDir) bool {
obj.PanicValidate()
relDir.PanicValidate()
// TODO: test with "" and "/"
sa := strings.Split(obj.path, "/")
for i := 1; i < len(sa)-1; i++ {
p := sa[i] + "/"
if p == relDir.path {
return true
}
}
return false
}
// ParseIntoRelDir takes an input path and ensures it's an RelDir, by adding a
// trailing slash if it's missing. It then runs Validate to ensure the path was
// valid overall. It also runs the stdlib path Clean function on it.
func ParseIntoRelDir(path string) (RelDir, error) {
if path == "" {
return RelDir{}, fmt.Errorf("path is empty")
}
path = stdlibPath.Clean(path)
// NOTE: after clean we won't have a trailing slash I think ;)
if !strings.HasSuffix(path, "/") { // add trailing slash if missing
path += "/"
}
relDir := RelDir{path: path}
return relDir, relDir.Validate()
}
// UnsafeParseIntoRelDir performs exactly as ParseIntoRelDir does, but it panics
// if the latter would have returned an error.
func UnsafeParseIntoRelDir(path string) RelDir {
relDir, err := ParseIntoRelDir(path)
if err != nil {
panic(err.Error())
}
return relDir
}
// AbsPath represents an absolute file or dir path.
type AbsPath struct {
path string
}
func (obj AbsPath) isAbs() {}
func (obj AbsPath) isPath() {}
// String returns the canonical "friendly" representation of this path. If it is
// a directory, then it will end with a slash.
func (obj AbsPath) String() string { return obj.path }
// Path returns the cleaned version of this path. It is what you expect after
// running the golang path cleaner on the internal representation.
func (obj AbsPath) Path() string { return stdlibPath.Clean(obj.path) }
// IsDir returns true if this is a dir, and false if it's not based on the path
// stored within and the parsing criteria in the IsDir helper function.
func (obj AbsPath) IsDir() bool { return IsDir(obj.path) }
// IsAbs returns true for this struct.
func (obj AbsPath) IsAbs() bool { return true }
// Validate returns an error if the path was not specified correctly.
func (obj AbsPath) Validate() error {
if !strings.HasPrefix(obj.path, "/") {
return fmt.Errorf("path is not absolute")
}
return nil
}
// PanicValidate panics if the path was not specified correctly.
func (obj AbsPath) PanicValidate() {
if err := obj.Validate(); err != nil {
panic(err.Error())
}
}
// Cmp compares two AbsPath's and returns nil if they have the same path.
func (obj AbsPath) Cmp(absPath AbsPath) error {
if obj.path != absPath.path {
return fmt.Errorf("paths differ")
}
return nil
}
// Dir returns the head component of the AbsPath, in this case, the directory.
func (obj AbsPath) Dir() AbsDir {
obj.PanicValidate()
ix := strings.LastIndex(obj.path, "/")
if ix == 0 {
return AbsDir{
path: "/",
}
}
return AbsDir{
path: obj.path[0:ix],
}
}
// HasDir returns true if the input relative dir is present in the path.
// TODO: write tests for this and ensure it doesn't have a bug
func (obj AbsPath) HasDir(relDir RelDir) bool {
obj.PanicValidate()
relDir.PanicValidate()
//if obj.path == "/" {
// return false
//}
// TODO: test with ""
i := strings.Index(obj.path, relDir.path)
if i == -1 {
return false // not found
}
if i == 0 {
// not possible unless relDir is /
//return false // found the root dir
panic("relDir was root which isn't relative")
}
// We want to make sure we land on a split char, or we didn't match it.
// We don't need to check the last char, because we know it's a /
return obj.path[i-1] == '/' // check if the char before is a /
}
// ParseIntoAbsPath takes an input path and ensures it's an AbsPath. It doesn't
// do anything particularly magical. It then runs Validate to ensure the path
// was valid overall. It also runs the stdlib path Clean function on it. Please
// note, that passing in the root slash / will cause this to fail.
func ParseIntoAbsPath(path string) (AbsPath, error) {
if path == "" {
return AbsPath{}, fmt.Errorf("path is empty")
}
path = stdlibPath.Clean(path)
absPath := AbsPath{path: path}
return absPath, absPath.Validate()
}
// UnsafeParseIntoAbsPath performs exactly as ParseIntoAbsPath does, but it
// panics if the latter would have returned an error.
func UnsafeParseIntoAbsPath(path string) AbsPath {
absPath, err := ParseIntoAbsPath(path)
if err != nil {
panic(err.Error())
}
return absPath
}
// RelPath represents a relative file or dir path.
type RelPath struct {
path string
}
func (obj RelPath) isRel() {}
func (obj RelPath) isPath() {}
// String returns the canonical "friendly" representation of this path. If it is
// a directory, then it will end with a slash.
func (obj RelPath) String() string { return obj.path }
// Path returns the cleaned version of this path. It is what you expect after
// running the golang path cleaner on the internal representation.
func (obj RelPath) Path() string { return stdlibPath.Clean(obj.path) }
// IsDir returns true if this is a dir, and false if it's not based on the path
// stored within and the parsing criteria in the IsDir helper function.
func (obj RelPath) IsDir() bool { return IsDir(obj.path) }
// IsAbs returns false for this struct.
func (obj RelPath) IsAbs() bool { return false }
// Validate returns an error if the path was not specified correctly.
func (obj RelPath) Validate() error {
if strings.HasPrefix(obj.path, "/") {
return fmt.Errorf("path is not relative")
}
if obj.path == "" {
return fmt.Errorf("path is empty")
}
return nil
}
// PanicValidate panics if the path was not specified correctly.
func (obj RelPath) PanicValidate() {
if err := obj.Validate(); err != nil {
panic(err.Error())
}
}
// Cmp compares two RelPath's and returns nil if they have the same path.
func (obj RelPath) Cmp(relpath RelPath) error {
if obj.path != relpath.path {
return fmt.Errorf("paths differ")
}
return nil
}
// HasDir returns true if the input relative dir is present in the path.
// TODO: write tests for this and ensure it doesn't have a bug
func (obj RelPath) HasDir(relDir RelDir) bool {
obj.PanicValidate()
relDir.PanicValidate()
//if obj.path == "/" {
// return false
//}
// TODO: test with ""
i := strings.Index(obj.path, relDir.path)
if i == -1 {
return false // not found
}
if i == 0 {
return true // found at the beginning
}
// We want to make sure we land on a split char, or we didn't match it.
// We don't need to check the last char, because we know it's a /
return obj.path[i-1] == '/' // check if the char before is a /
}
// ParseIntoRelPath takes an input path and ensures it's an RelPath. It doesn't
// do anything particularly magical. It then runs Validate to ensure the path
// was valid overall. It also runs the stdlib path Clean function on it.
func ParseIntoRelPath(path string) (RelPath, error) {
if path == "" {
return RelPath{}, fmt.Errorf("path is empty")
}
path = stdlibPath.Clean(path)
relPath := RelPath{path: path}
return relPath, relPath.Validate()
}
// UnsafeParseIntoRelPath performs exactly as ParseIntoRelPath does, but it
// panics if the latter would have returned an error.
func UnsafeParseIntoRelPath(path string) RelPath {
relPath, err := ParseIntoRelPath(path)
if err != nil {
panic(err.Error())
}
return relPath
}
// ParseIntoPath takes an input path and a boolean that specifies if it is a dir
// and returns a type that fulfills the Path interface. The isDir boolean
// usually comes from the io/fs.FileMode IsDir() method. The returned underlying
// type will be one of AbsFile, AbsDir, RelFile, RelDir. It then runs Validate
// to ensure the path was valid overall.
func ParseIntoPath(path string, isDir bool) (Path, error) {
//var safePath Path
if isDir {
dir, err := ParseIntoDir(path)
if err != nil {
return nil, err
}
if absDir, ok := dir.(AbsDir); ok {
return absDir, absDir.Validate()
}
if relDir, ok := dir.(RelDir); ok {
return relDir, relDir.Validate()
}
return nil, fmt.Errorf("unknown dir") // bug
}
file, err := ParseIntoFile(path)
if err != nil {
return nil, err
}
if absFile, ok := file.(AbsFile); ok {
return absFile, absFile.Validate()
}
if relFile, ok := file.(RelFile); ok {
return relFile, relFile.Validate()
}
return nil, fmt.Errorf("unknown file") // bug
}
// UnsafeParseIntoPath performs exactly as ParseIntoPath does, but it panics if
// the latter would have returned an error.
func UnsafeParseIntoPath(path string, isDir bool) Path {
p, err := ParseIntoPath(path, isDir)
if err != nil {
panic(err.Error())
}
return p
}
// SmartParseIntoPath performs exactly as ParseIntoPath does, except it
// determines if something is a dir based on whetherthe string path has a
// trailing slash or not.
func SmartParseIntoPath(path string) (Path, error) {
return ParseIntoPath(path, IsDir(path))
}
// UnsafeSmartParseIntoPath performs exactly as SmartParseIntoPath does, but it
// panics if the latter would have returned an error.
func UnsafeSmartParseIntoPath(path string) Path {
p, err := SmartParseIntoPath(path)
if err != nil {
panic(err.Error())
}
return p
}
// ParseIntoFile takes an input path and returns a type that fulfills the File
// interface. The returned underlying type will be one of AbsFile or RelFile.
func ParseIntoFile(path string) (File, error) {
if strings.HasPrefix(path, "/") { // also matches "/", but that would error
return ParseIntoAbsFile(path)
}
return ParseIntoRelFile(path)
}
// UnsafeParseIntoFile performs exactly as ParseIntoFile does, but it panics if
// the latter would have returned an error.
func UnsafeParseIntoFile(path string) File {
p, err := ParseIntoFile(path)
if err != nil {
panic(err.Error())
}
return p
}
// ParseIntoDir takes an input path and returns a type that fulfills the Dir
// interface. The returned underlying type will be one of AbsDir or RelDir.
func ParseIntoDir(path string) (Dir, error) {
if strings.HasPrefix(path, "/") { // also matches "/"
return ParseIntoAbsDir(path)
}
return ParseIntoRelDir(path)
}
// UnsafeParseIntoDir performs exactly as ParseIntoDir does, but it panics if
// the latter would have returned an error.
func UnsafeParseIntoDir(path string) Dir {
p, err := ParseIntoDir(path)
if err != nil {
panic(err.Error())
}
return p
}
// JoinToAbsFile joins an absolute dir with a relative file to produce an
// absolute file.
func JoinToAbsFile(absDir AbsDir, relFile RelFile) AbsFile {
absDir.PanicValidate()
relFile.PanicValidate()
return AbsFile{
path: absDir.path + relFile.path,
}
}
// JoinToAbsDir joins an absolute dir with a relative dir to produce an absolute
// dir.
func JoinToAbsDir(absDir AbsDir, relDir RelDir) AbsDir {
absDir.PanicValidate()
relDir.PanicValidate()
return AbsDir{
path: absDir.path + relDir.path,
}
}
// JoinToRelFile joins a relative dir with a relative file to produce a relative
// file.
func JoinToRelFile(relDir RelDir, relFile RelFile) RelFile {
relDir.PanicValidate()
relFile.PanicValidate()
return RelFile{
path: relDir.path + relFile.path,
}
}
// JoinToRelDir joins any number of relative dir's to produce a relative dir.
func JoinToRelDir(relDir ...RelDir) RelDir {
p := ""
for _, x := range relDir {
x.PanicValidate()
p += x.path
}
return RelDir{
path: p,
}
}
// HasPrefix determines if the given path has the specified dir prefix. The
// prefix and path can be absolute or relative. Keep in mind, that if the path
// is absolute, then only an absolute dir can successfully match. Similarly, if
// the path is relative, then only a relative dir can successfully match.
func HasPrefix(path Path, prefix Dir) bool {
return strings.HasPrefix(path.String(), prefix.String())
}
// StripPrefix removes a dir prefix from a path if it is possible to do so. The
// prefix and path can be both absolute or both relative. The returned result is
// either a relative dir or a relative file. Keep in mind, that if the path is
// absolute, then only an absolute dir can successfully match. Similarly, if the
// path is relative, then only a relative dir can successfully match. The input
// path will be returned unchanged if it is not possible to match (although it
// will return an error in parallel) and if there is a match, then either a
// relative dir or a relative file will be returned with the path interface.
// This is logical, because after removing some prefix, only relative paths can
// possibly remain. This relative returned path will be a file if the input path
// was a file, and a dir if the input path was a dir.
// XXX: add tests!
func StripPrefix(path Path, prefix Dir) (Path, error) {
if !HasPrefix(path, prefix) {
return path, fmt.Errorf("no prefix")
}
p := strings.TrimPrefix(path.String(), prefix.String())
// XXX: what happens if we strip the entire dir path from itself? Empty path?
//if p == "" {
// return ???, ???
//}
if path.IsDir() {
return ParseIntoRelDir(p)
}
return ParseIntoRelFile(p)
}
// IsDir is a helper that returns true if a string path is considered as such by
// the presence of a trailing slash.
func IsDir(path string) bool {
return strings.HasSuffix(path, "/")
}
// IsAbs is a helper that returns true if a string path is considered as such by
// the presence of a leading slash.
func IsAbs(path string) bool {
return strings.HasPrefix(path, "/")
}
// hasExtInsensitive is the helper function for checking if the file ends with
// the given extension. It checks with a fancy case-insensitive match. As a
// special case, if you pass in an empty string as the extension to match, this
// will return false.
// TODO: add tests!
func hasExtInsensitive(path, ext string) bool {
if ext == "" { // special case, not consistent with strings.HasSuffix
return false
}
if len(ext) > len(path) { // file not long enough to have extension
return false
}
s := path[len(path)-len(ext):] // extract ext length of chars
if !strings.EqualFold(s, ext) { // fancy case-insensitive compare
return false
}
return true
}