// Mgmt // Copyright (C) James Shubin and the project contributors // Written by James Shubin 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 . // // 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 }