package pkglint import ( "io/ioutil" "os" "path" "path/filepath" "strings" ) // Path is a slash-separated path. // It may or may not resolve to an existing file. // It may be absolute or relative. // Some paths may contain placeholders like @VAR@ or ${VAR}. // The base directory of relative paths is unspecified. type Path string func NewPath(p string) Path { return Path(p) } func (p Path) String() string { return string(p) } func (p Path) GoString() string { return sprintf("%q", string(p)) } // IsEmpty returns true if the path is completely empty, // which is usually a sign of an uninitialized variable. func (p Path) IsEmpty() bool { return p == "" } // Dir returns the directory of the path, with only minimal cleaning. // Only redundant dots and slashes are removed, and only at the end. func (p Path) Dir() Path { s := p.String() end := len(s) for end > 0 && s[end-1] != '/' { end-- } for end > 1 && s[end-1] == '/' || end > 2 && hasPrefix(s[end-2:], "/.") { end-- } if end == 0 { return "." } return NewPath(s[:end]) } func (p Path) Base() RelPath { return NewRelPathString(path.Base(string(p))) } func (p Path) Split() (dir Path, base string) { strDir, strBase := path.Split(string(p)) return Path(strDir), strBase } // Parts splits the path into its components. // Multiple adjacent slashes are treated like a single slash. // Parts that are single dots are skipped. // Absolute paths have an empty string as its first part. // All other parts are nonempty. func (p Path) Parts() []string { if p.IsEmpty() { return nil } parts := strings.Split(string(p), "/") j := 0 for i, part := range parts { if (i == 0 || part != "") && part != "." { parts[j] = part j++ } } parts = parts[:j] if len(parts) == 0 { return []string{"."} } return parts } // Count returns the number of meaningful parts of the path. // See Parts. func (p Path) Count() int { return len(p.Parts()) } func (p Path) HasPrefixText(prefix string) bool { return hasPrefix(string(p), prefix) } // HasPrefixPath returns whether the path starts with the given prefix. // The basic unit of comparison is a path component, not a character. func (p Path) HasPrefixPath(prefix Path) bool { // Handle the simple case first, without any memory allocations. if hasPrefix(string(p), string(prefix)) { return len(p) == len(prefix) || p[len(prefix)] == '/' } if prefix == "." { return !p.IsAbs() } si := 0 pi := 0 for { for si < len(p) && (p[si] == '.' || p[si] == '/') { si++ } for pi < len(prefix) && (prefix[pi] == '.' || prefix[pi] == '/') { pi++ } if si >= len(p) || pi >= len(prefix) { break } if p[si] != prefix[pi] { return false } si++ pi++ } parts := p.Parts() prefixParts := prefix.Parts() if len(prefixParts) > len(parts) { return false } for i, prefixPart := range prefixParts { if parts[i] != prefixPart { return false } } return true } func (p Path) ContainsText(contained string) bool { return contains(string(p), contained) } // ContainsPath returns whether the sub path is part of the path. // The basic unit of comparison is a path component, not a character. // // Note that the paths used in pkglint may contain seemingly unnecessary // components, like "../../wip/mk/../../devel/gettext-lib". To ignore these // components, use ContainsPathCanonical instead. func (p Path) ContainsPath(sub Path) bool { limit := len(p) - len(sub) for i := 0; i <= limit; i++ { if (i == 0 || p[i-1] == '/') && p[i:].HasPrefixPath(sub) { return true } } return sub == "." } func (p Path) HasSuffixText(suffix string) bool { return hasSuffix(string(p), suffix) } // HasSuffixPath returns whether the path ends with the given suffix. // The basic unit of comparison is a path component, not a character. func (p Path) HasSuffixPath(suffix Path) bool { return hasSuffix(string(p), string(suffix)) && (len(p) == len(suffix) || p[len(p)-len(suffix)-1] == '/') } func (p Path) HasBase(base string) bool { return p.Base().String() == base } func (p Path) TrimSuffix(suffix string) Path { return Path(strings.TrimSuffix(string(p), suffix)) } func (p Path) Replace(from, to string) Path { return Path(strings.Replace(string(p), from, to, -1)) } func (p Path) JoinClean(s RelPath) Path { return Path(path.Join(string(p), string(s))) } func (p Path) JoinNoClean(s RelPath) Path { return Path(string(p) + "/" + string(s)) } func (p Path) Clean() Path { return NewPath(path.Clean(string(p))) } // CleanDot returns the path with single dots removed and double slashes // collapsed. func (p Path) CleanDot() Path { if !p.ContainsText(".") && !p.ContainsText("//") { return p } parts := p.Parts() return NewPath(strings.Join(parts, "/")) } // CleanPath is similar to path.Clean. // The difference is that only "../../" is replaced, not "../". // Also, the initial directory is always kept. // This is to provide the package path as context in deeply nested .include chains. func (p Path) CleanPath() Path { parts := p.Parts() for i := 2; i+3 < len(parts); /* nothing */ { if parts[i] != ".." && parts[i+1] != ".." && parts[i+2] == ".." && parts[i+3] == ".." { if i+4 == len(parts) || parts[i+4] != ".." { parts = append(parts[:i], parts[i+4:]...) continue } } i++ } if len(parts) == 0 { return "." } return NewPath(strings.Join(parts, "/")) } func (p Path) IsAbs() bool { return len(p) > 0 && (p[0] == '/' || len(p) > 2 && p[1] == ':' && p[2] == '/') } // Rel returns the relative path from this path to the other. // // The returned path is a canonical relative path. // It starts with a possibly empty sequence of "../", // followed by a possibly empty sequence of non-dotdot directories. // It may have a single dot at the end, which means the path goes to a directory. func (p Path) Rel(other Path) RelPath { fp := filepath.FromSlash(p.String()) fpOther := filepath.FromSlash(other.String()) rel, err := filepath.Rel(fp, fpOther) assertNil(err, "Relpath from %q to %q", p, other) return NewRelPath(NewPath(filepath.ToSlash(rel))) } // CurrPath is a path that is either absolute or relative to the current // working directory. It is used in command line arguments and for // loading files from the file system, and later in the diagnostics. type CurrPath string func NewCurrPath(p Path) CurrPath { return CurrPath(p) } func NewCurrPathString(p string) CurrPath { return CurrPath(p) } func NewCurrPathSlash(p string) CurrPath { return CurrPath(filepath.ToSlash(p)) } func (p CurrPath) GoString() string { return p.AsPath().GoString() } func (p CurrPath) String() string { return string(p) } func (p CurrPath) AsPath() Path { return Path(p) } func (p CurrPath) IsEmpty() bool { return p.AsPath().IsEmpty() } func (p CurrPath) Dir() CurrPath { return CurrPath(p.AsPath().Dir()) } func (p CurrPath) Base() RelPath { return p.AsPath().Base() } func (p CurrPath) Split() (dir CurrPath, base string) { pathDir, pathBase := p.AsPath().Split() return NewCurrPath(pathDir), pathBase } func (p CurrPath) Parts() []string { return p.AsPath().Parts() } func (p CurrPath) IsAbs() bool { return p.AsPath().IsAbs() } func (p CurrPath) HasPrefixPath(prefix CurrPath) bool { return p.AsPath().HasPrefixPath(prefix.AsPath()) } func (p CurrPath) ContainsPath(sub Path) bool { return p.AsPath().ContainsPath(sub) } func (p CurrPath) ContainsText(text string) bool { return p.AsPath().ContainsText(text) } func (p CurrPath) HasSuffixPath(suffix Path) bool { return p.AsPath().HasSuffixPath(suffix) } func (p CurrPath) HasSuffixText(suffix string) bool { return p.AsPath().HasSuffixText(suffix) } func (p CurrPath) HasBase(base string) bool { return p.AsPath().HasBase(base) } func (p CurrPath) TrimSuffix(suffix string) CurrPath { return NewCurrPath(p.AsPath().TrimSuffix(suffix)) } func (p CurrPath) ReplaceSuffix(from string, to string) CurrPath { trimmed := p.TrimSuffix(from) assert(trimmed != p) return NewCurrPathString(trimmed.String() + to) } func (p CurrPath) Clean() CurrPath { return CurrPath(p.AsPath().Clean()) } func (p CurrPath) CleanDot() CurrPath { return NewCurrPath(p.AsPath().CleanDot()) } func (p CurrPath) CleanPath() CurrPath { return CurrPath(p.AsPath().CleanPath()) } func (p CurrPath) JoinNoClean(other RelPath) CurrPath { return CurrPath(p.AsPath().JoinNoClean(other)) } func (p CurrPath) JoinClean(other RelPath) CurrPath { return NewCurrPath(p.AsPath().JoinClean(other)) } func (p CurrPath) Rel(rel CurrPath) RelPath { return p.AsPath().Rel(rel.AsPath()) } func (p CurrPath) Rename(newName CurrPath) error { return os.Rename(string(p), string(newName)) } func (p CurrPath) Lstat() (os.FileInfo, error) { return os.Lstat(string(p)) } func (p CurrPath) Stat() (os.FileInfo, error) { return os.Stat(string(p)) } func (p CurrPath) Exists() bool { _, err := p.Lstat() return err == nil } func (p CurrPath) IsFile() bool { info, err := p.Lstat() return err == nil && info.Mode().IsRegular() } func (p CurrPath) IsDir() bool { info, err := p.Lstat() return err == nil && info.IsDir() } func (p CurrPath) Chmod(mode os.FileMode) error { return os.Chmod(string(p), mode) } func (p CurrPath) ReadDir() ([]os.FileInfo, error) { return ioutil.ReadDir(string(p)) } func (p CurrPath) ReadPaths() []CurrPath { infos, err := p.ReadDir() if err != nil { return nil } var filenames []CurrPath for _, info := range infos { if !isIgnoredFilename(info.Name()) { joined := p.JoinNoClean(NewRelPathString(info.Name())).CleanPath() filenames = append(filenames, joined) } } return filenames } func (p CurrPath) Open() (*os.File, error) { return os.Open(string(p)) } func (p CurrPath) ReadString() (string, error) { bytes, err := ioutil.ReadFile(string(p)) return string(bytes), err } func (p CurrPath) WriteString(s string) error { return ioutil.WriteFile(string(p), []byte(s), 0666) } // PkgsrcPath is a path relative to the pkgsrc root. type PkgsrcPath string func NewPkgsrcPath(p Path) PkgsrcPath { _ = NewRelPath(p) return PkgsrcPath(p) } func (p PkgsrcPath) String() string { return string(p) } func (p PkgsrcPath) AsPath() Path { return NewPath(string(p)) } func (p PkgsrcPath) AsRelPath() RelPath { return RelPath(p) } func (p PkgsrcPath) Dir() PkgsrcPath { return NewPkgsrcPath(p.AsPath().Dir()) } func (p PkgsrcPath) Base() RelPath { return p.AsPath().Base() } func (p PkgsrcPath) Count() int { return p.AsPath().Count() } func (p PkgsrcPath) HasPrefixPath(prefix Path) bool { return p.AsPath().HasPrefixPath(prefix) } func (p PkgsrcPath) JoinNoClean(other RelPath) PkgsrcPath { return NewPkgsrcPath(p.AsPath().JoinNoClean(other)) } // PackagePath is a path relative to the package directory. It is used // for the PATCHDIR and PKGDIR variables, as well as dependencies and // conflicts on other packages. // // It can have two forms: // - patches (further down) // - ../../category/package/* (up to the pkgsrc root, then down again) type PackagePath string func NewPackagePath(p RelPath) PackagePath { return PackagePath(p) } func NewPackagePathString(p string) PackagePath { return PackagePath(NewRelPathString(p)) } func (p PackagePath) AsPath() Path { return Path(p) } func (p PackagePath) AsRelPath() RelPath { return RelPath(p) } func (p PackagePath) String() string { return p.AsPath().String() } func (p PackagePath) JoinNoClean(other RelPath) PackagePath { return NewPackagePathString(p.AsPath().JoinNoClean(other).String()) } func (p PackagePath) CleanPath() PackagePath { return NewPackagePathString(p.AsPath().CleanPath().String()) } func (p PackagePath) IsEmpty() bool { return p.AsPath().IsEmpty() } func (p PackagePath) HasPrefixPath(sub Path) bool { return p.AsPath().HasPrefixPath(sub) } func (p PackagePath) ContainsPath(sub Path) bool { return p.AsPath().ContainsPath(sub) } func (p PackagePath) ContainsText(contained string) bool { return p.AsPath().ContainsText(contained) } func (p PackagePath) Replace(from, to string) PackagePath { return NewPackagePathString(strings.Replace(string(p), from, to, -1)) } // RelPath is a path that is relative to some base directory that is not // further specified. type RelPath string func NewRelPath(p Path) RelPath { assert(!p.IsAbs()) return RelPath(p) } func NewRelPathString(p string) RelPath { return NewRelPath(NewPath(p)) } func (p RelPath) AsPath() Path { return NewPath(string(p)) } func (p RelPath) String() string { return p.AsPath().String() } func (p RelPath) IsEmpty() bool { return p.AsPath().IsEmpty() } func (p RelPath) Split() (RelPath, string) { dir, base := p.AsPath().Split() return NewRelPath(dir), base } func (p RelPath) Dir() RelPath { return RelPath(p.AsPath().Dir()) } func (p RelPath) Base() RelPath { return p.AsPath().Base() } func (p RelPath) HasBase(base string) bool { return p.AsPath().HasBase(base) } func (p RelPath) Parts() []string { return p.AsPath().Parts() } func (p RelPath) Count() int { return p.AsPath().Count() } func (p RelPath) Clean() RelPath { return NewRelPath(p.AsPath().Clean()) } func (p RelPath) CleanDot() RelPath { return NewRelPath(p.AsPath().CleanDot()) } func (p RelPath) CleanPath() RelPath { return RelPath(p.AsPath().CleanPath()) } func (p RelPath) JoinNoClean(other RelPath) RelPath { return RelPath(p.AsPath().JoinNoClean(other)) } func (p RelPath) Replace(from string, to string) RelPath { return RelPath(p.AsPath().Replace(from, to)) } func (p RelPath) HasPrefixPath(prefix Path) bool { return p.AsPath().HasPrefixPath(prefix) } func (p RelPath) HasPrefixText(prefix string) bool { return p.AsPath().HasPrefixText(prefix) } func (p RelPath) ContainsPath(sub Path) bool { return p.AsPath().ContainsPath(sub) } func (p RelPath) ContainsText(text string) bool { return p.AsPath().ContainsText(text) } func (p RelPath) HasSuffixPath(suffix Path) bool { return p.AsPath().HasSuffixPath(suffix) } func (p RelPath) HasSuffixText(suffix string) bool { return p.AsPath().HasSuffixText(suffix) } func (p RelPath) Rel(other Path) RelPath { return p.AsPath().Rel(other) }