apparmor.d/pkg/paths/paths.go
2024-10-12 15:31:24 +01:00

638 lines
18 KiB
Go

/*
* This file is part of PathsHelper library.
*
* Copyright 2018 Arduino AG (http://www.arduino.cc/)
*
* PathsHelper library 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 2 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, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* As a special exception, you may use this file as part of a free software
* library without restriction. Specifically, if other files instantiate
* templates or use macros or inline functions from this file, or you compile
* this file and link it with other files to produce an executable, this
* file does not by itself cause the resulting executable to be covered by
* the GNU General Public License. This exception does not however
* invalidate any other reasons why the executable file might be covered by
* the GNU General Public License.
*/
package paths
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"syscall"
"time"
"github.com/roddhjav/apparmor.d/pkg/util"
)
// Path represents a path
type Path struct {
path string
}
// New creates a new Path object. If path is the empty string
// then nil is returned.
func New(path ...string) *Path {
if len(path) == 0 {
return nil
}
if len(path) == 1 && path[0] == "" {
return nil
}
res := &Path{path: path[0]}
if len(path) > 1 {
return res.Join(path[1:]...)
}
return res
}
// NewFromFile creates a new Path object using the path name
// obtained from the File object (see os.File.Name function).
func NewFromFile(file *os.File) *Path {
return New(file.Name())
}
// Stat returns a FileInfo describing the named file. The result is
// cached internally for next queries. To ensure that the cached
// FileInfo entry is updated just call Stat again.
func (p *Path) Stat() (fs.FileInfo, error) {
return os.Stat(p.path)
}
// Lstat returns a FileInfo describing the named file. If the file is
// a symbolic link, the returned FileInfo describes the symbolic link.
// Lstat makes no attempt to follow the link. If there is an error, it
// will be of type *PathError.
func (p *Path) Lstat() (fs.FileInfo, error) {
return os.Lstat(p.path)
}
// Clone create a copy of the Path object
func (p *Path) Clone() *Path {
return New(p.path)
}
// Join create a new Path by joining the provided paths
func (p *Path) Join(paths ...string) *Path {
return New(filepath.Join(p.path, filepath.Join(paths...)))
}
// JoinPath create a new Path by joining the provided paths
func (p *Path) JoinPath(paths ...*Path) *Path {
res := p.Clone()
for _, path := range paths {
res = res.Join(path.path)
}
return res
}
// Base returns the last element of path
func (p *Path) Base() string {
return filepath.Base(p.path)
}
// Ext returns the file name extension used by path
func (p *Path) Ext() string {
return filepath.Ext(p.path)
}
// HasPrefix returns true if the file name has one of the
// given prefixes (the Base() method is used to obtain the
// file name used for the comparison)
func (p *Path) HasPrefix(prefixes ...string) bool {
filename := p.Base()
for _, prefix := range prefixes {
if strings.HasPrefix(filename, prefix) {
return true
}
}
return false
}
// HasSuffix returns true if the file name has one of the
// given suffixies
func (p *Path) HasSuffix(suffixies ...string) bool {
filename := p.String()
for _, suffix := range suffixies {
if strings.HasSuffix(filename, suffix) {
return true
}
}
return false
}
// RelTo returns a relative Path that is lexically equivalent to r when
// joined to the current Path.
//
// For example paths.New("/my/path/ab/cd").RelTo(paths.New("/my/path"))
// returns "../..".
func (p *Path) RelTo(r *Path) (*Path, error) {
rel, err := filepath.Rel(p.path, r.path)
if err != nil {
return nil, err
}
return New(rel), nil
}
// RelFrom returns a relative Path that when joined with r is lexically
// equivalent to the current path.
//
// For example paths.New("/my/path/ab/cd").RelFrom(paths.New("/my/path"))
// returns "ab/cd".
func (p *Path) RelFrom(r *Path) (*Path, error) {
rel, err := filepath.Rel(r.path, p.path)
if err != nil {
return nil, err
}
return New(rel), nil
}
// Abs returns the absolute path of the current Path
func (p *Path) Abs() (*Path, error) {
abs, err := filepath.Abs(p.path)
if err != nil {
return nil, err
}
return New(abs), nil
}
// IsAbs returns true if the Path is absolute
func (p *Path) IsAbs() bool {
return filepath.IsAbs(p.path)
}
// ToAbs transform the current Path to the corresponding absolute path
func (p *Path) ToAbs() error {
abs, err := filepath.Abs(p.path)
if err != nil {
return err
}
p.path = abs
return nil
}
// Clean Clean returns the shortest path name equivalent to path by
// purely lexical processing
func (p *Path) Clean() *Path {
return New(filepath.Clean(p.path))
}
// IsInsideDir returns true if the current path is inside the provided
// dir
func (p *Path) IsInsideDir(dir *Path) (bool, error) {
rel, err := filepath.Rel(dir.path, p.path)
if err != nil {
// If the dir cannot be made relative to this path it means
// that it belong to a different filesystems, so it cannot be
// inside this path.
return false, nil
}
return !strings.Contains(rel, ".."+string(os.PathSeparator)) &&
rel != ".." &&
rel != ".", nil
}
// Parent returns all but the last element of path, typically the path's
// directory or the parent directory if the path is already a directory
func (p *Path) Parent() *Path {
return New(filepath.Dir(p.path))
}
// Mkdir create a directory denoted by the current path
func (p *Path) Mkdir() error {
return os.Mkdir(p.path, 0755)
}
// MkdirAll creates a directory named path, along with any necessary
// parents, and returns nil, or else returns an error
func (p *Path) MkdirAll() error {
return os.MkdirAll(p.path, os.FileMode(0755))
}
// Remove removes the named file or directory
func (p *Path) Remove() error {
return os.Remove(p.path)
}
// RemoveAll removes path and any children it contains. It removes
// everything it can but returns the first error it encounters. If
// the path does not exist, RemoveAll returns nil (no error).
func (p *Path) RemoveAll() error {
return os.RemoveAll(p.path)
}
// Rename renames (moves) the path to newpath. If newpath already exists
// and is not a directory, Rename replaces it. OS-specific restrictions
// may apply when oldpath and newpath are in different directories. If
// there is an error, it will be of type *os.LinkError.
func (p *Path) Rename(newpath *Path) error {
return os.Rename(p.path, newpath.path)
}
// MkTempDir creates a new temporary directory inside the path
// pointed by the Path object with a name beginning with prefix
// and returns the path of the new directory.
func (p *Path) MkTempDir(prefix string) (*Path, error) {
return MkTempDir(p.path, prefix)
}
// FollowSymLink transforms the current path to the path pointed by the
// symlink if path is a symlink, otherwise it does nothing
func (p *Path) FollowSymLink() error {
resolvedPath, err := filepath.EvalSymlinks(p.path)
if err != nil {
return err
}
p.path = resolvedPath
return nil
}
// Exist return true if the file denoted by this path exists, false
// in any other case (also in case of error).
func (p *Path) Exist() bool {
exist, err := p.ExistCheck()
return exist && err == nil
}
// NotExist return true if the file denoted by this path DO NOT exists, false
// in any other case (also in case of error).
func (p *Path) NotExist() bool {
exist, err := p.ExistCheck()
return !exist && err == nil
}
// ExistCheck return true if the path exists or false if the path doesn't exists.
// In case the check fails false is returned together with the corresponding error.
func (p *Path) ExistCheck() (bool, error) {
_, err := p.Stat()
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
if err.(*os.PathError).Err == syscall.ENOTDIR {
return false, nil
}
return false, err
}
// IsDir returns true if the path exists and is a directory. In all the other
// cases (and also in case of any error) false is returned.
func (p *Path) IsDir() bool {
isdir, err := p.IsDirCheck()
return isdir && err == nil
}
// IsNotDir returns true if the path exists and is NOT a directory. In all the other
// cases (and also in case of any error) false is returned.
func (p *Path) IsNotDir() bool {
isdir, err := p.IsDirCheck()
return !isdir && err == nil
}
// IsDirCheck return true if the path exists and is a directory or false
// if the path exists and is not a directory. In all the other case false and
// the corresponding error is returned.
func (p *Path) IsDirCheck() (bool, error) {
info, err := p.Stat()
if err == nil {
return info.IsDir(), nil
}
return false, err
}
// CopyTo copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file. The file mode will be copied from the source and
// the copied data is synced/flushed to stable storage.
func (p *Path) CopyTo(dst *Path) error {
if p.EqualsTo(dst) {
return fmt.Errorf("%s and %s are the same file", p.path, dst.path)
}
in, err := os.Open(p.path)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst.path)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
if err := out.Sync(); err != nil {
return err
}
si, err := p.Stat()
if err != nil {
return err
}
err = os.Chmod(dst.path, si.Mode())
if err != nil {
return err
}
return nil
}
// CopyTo recursivelly copy all files from a source path to a destination path.
func CopyTo(src *Path, dst *Path) error {
files, err := src.ReadDirRecursiveFiltered(nil,
FilterOutDirectories(),
FilterOutNames("README.md"),
)
if err != nil {
return err
}
for _, file := range files {
destination, err := file.RelFrom(src)
if err != nil {
return err
}
destination = dst.JoinPath(destination)
if err := destination.Parent().MkdirAll(); err != nil {
return err
}
if err := file.CopyTo(destination); err != nil {
return err
}
}
return nil
}
// CopyDirTo recursively copies the directory denoted by the current path to
// the destination path. The source directory must exist and the destination
// directory must NOT exist (no implicit destination name allowed).
// Symlinks are not copied, they will be supported in future versions.
func (p *Path) CopyDirTo(dst *Path) error {
src := p.Clean()
dst = dst.Clean()
srcFiles, err := src.ReadDir()
if err != nil {
return fmt.Errorf("error reading source dir %s: %s", src, err)
}
if exist, err := dst.ExistCheck(); exist {
return fmt.Errorf("destination %s already exists", dst)
} else if err != nil {
return fmt.Errorf("checking if %s exists: %s", dst, err)
}
if err := dst.MkdirAll(); err != nil {
return fmt.Errorf("creating destination dir %s: %s", dst, err)
}
srcInfo, err := src.Stat()
if err != nil {
return fmt.Errorf("getting stat info for %s: %s", src, err)
}
if err := os.Chmod(dst.path, srcInfo.Mode()); err != nil {
return fmt.Errorf("setting permission for dir %s: %s", dst, err)
}
for _, srcPath := range srcFiles {
srcPathInfo, err := srcPath.Stat()
if err != nil {
return fmt.Errorf("getting stat info for %s: %s", srcPath, err)
}
dstPath := dst.Join(srcPath.Base())
if srcPathInfo.IsDir() {
if err := srcPath.CopyDirTo(dstPath); err != nil {
return fmt.Errorf("copying %s to %s: %s", srcPath, dstPath, err)
}
continue
}
// Skip symlinks.
if srcPathInfo.Mode()&os.ModeSymlink != 0 {
// TODO
continue
}
if err := srcPath.CopyTo(dstPath); err != nil {
return fmt.Errorf("copying %s to %s: %s", srcPath, dstPath, err)
}
}
return nil
}
// Chmod changes the mode of the named file to mode. If the file is a
// symbolic link, it changes the mode of the link's target. If there
// is an error, it will be of type *os.PathError.
func (p *Path) Chmod(mode fs.FileMode) error {
return os.Chmod(p.path, mode)
}
// Chtimes changes the access and modification times of the named file,
// similar to the Unix utime() or utimes() functions.
func (p *Path) Chtimes(atime, mtime time.Time) error {
return os.Chtimes(p.path, atime, mtime)
}
// ReadFile reads the file named by filename and returns the contents
func (p *Path) ReadFile() ([]byte, error) {
return os.ReadFile(p.path)
}
// WriteFile writes data to a file named by filename. If the file
// does not exist, WriteFile creates it otherwise WriteFile truncates
// it before writing.
func (p *Path) WriteFile(data []byte) error {
return os.WriteFile(p.path, data, os.FileMode(0644))
}
// WriteToTempFile writes data to a newly generated temporary file.
// dir and prefix have the same meaning for MkTempFile.
// In case of success the Path to the temp file is returned.
func WriteToTempFile(data []byte, dir *Path, prefix string) (res *Path, err error) {
f, err := MkTempFile(dir, prefix)
if err != nil {
return nil, err
}
defer f.Close()
if n, err := f.Write(data); err != nil {
return nil, err
} else if n < len(data) {
return nil, fmt.Errorf("could not write all data (written %d bytes out of %d)", n, len(data))
}
return New(f.Name()), nil
}
// ReadFileAsString read a file and return its content as a string.
func (p *Path) ReadFileAsString() (string, error) {
content, err := p.ReadFile()
if err != nil {
return "", err
}
return string(content), nil
}
// MustReadFileAsString read a file and return its content as a string. Panic if an error occurs.
func (p *Path) MustReadFileAsString() string {
content, err := p.ReadFile()
if err != nil {
panic(err)
}
return string(content)
}
// ReadFileAsLines reads the file named by filename and returns it as an
// array of lines. This function takes care of the newline encoding
// differences between different OS
func (p *Path) ReadFileAsLines() ([]string, error) {
data, err := p.ReadFile()
if err != nil {
return nil, err
}
txt := string(data)
txt = strings.Replace(txt, "\r\n", "\n", -1)
return strings.Split(txt, "\n"), nil
}
// MustReadFileAsLines read a file and return its content as a slice of string. Panic if an error occurs.
func (p *Path) MustReadFileAsLines() []string {
lines, err := p.ReadFileAsLines()
if err != nil {
panic(err)
}
return lines
}
// MustReadFilteredFileAsLines read a file and return its content as a slice of string.
// It filter out comments and empty lines. Panic if an error occurs.
func (p *Path) MustReadFilteredFileAsLines() []string {
data, err := p.ReadFile()
if err != nil {
panic(err)
}
txt := string(data)
txt = strings.Replace(txt, "\r\n", "\n", -1)
txt = util.Filter(txt)
res := strings.Split(txt, "\n")
if slices.Contains(res, "") {
idx := slices.Index(res, "")
res = slices.Delete(res, idx, idx+1)
}
return res
}
// Truncate create an empty file named by path or if the file already
// exist it truncates it (delete all contents)
func (p *Path) Truncate() error {
return p.WriteFile([]byte{})
}
// Open opens a file for reading. It calls os.Open on the
// underlying path.
func (p *Path) Open() (*os.File, error) {
return os.Open(p.path)
}
// Create creates or truncates a file. It calls os.Create
// on the underlying path.
func (p *Path) Create() (*os.File, error) {
return os.Create(p.path)
}
// Append opens a file for append or creates it if the file doesn't exist.
func (p *Path) Append() (*os.File, error) {
return os.OpenFile(p.path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)
}
// EqualsTo return true if both paths are equal
func (p *Path) EqualsTo(other *Path) bool {
return p.path == other.path
}
// EquivalentTo return true if both paths are equivalent (they points to the
// same file even if they are lexicographically different) based on the current
// working directory.
func (p *Path) EquivalentTo(other *Path) bool {
if p.Clean().path == other.Clean().path {
return true
}
if infoP, err := p.Stat(); err != nil {
// go ahead with the next test...
} else if infoOther, err := other.Stat(); err != nil {
// go ahead with the next test...
} else if os.SameFile(infoP, infoOther) {
return true
}
if absP, err := p.Abs(); err != nil {
return false
} else if absOther, err := other.Abs(); err != nil {
return false
} else {
return absP.path == absOther.path
}
}
// Parents returns all the parents directories of the current path. If the path is absolute
// it starts from the current path to the root, if the path is relative is starts from the
// current path to the current directory.
// The path should be clean for this method to work properly (no .. or . or other shortcuts).
// This function does not performs any check on the returned paths.
func (p *Path) Parents() []*Path {
res := []*Path{}
dir := p
for {
res = append(res, dir)
parent := dir.Parent()
if parent.EquivalentTo(dir) {
break
}
dir = parent
}
return res
}
func (p *Path) String() string {
return p.path
}
// Canonical return a "canonical" Path for the given filename.
// The meaning of "canonical" is OS-dependent but the goal of this method
// is to always return the same path for a given file (factoring out all the
// possible ambiguities including, for example, relative paths traversal,
// symlinks, drive volume letter case, etc).
func (p *Path) Canonical() *Path {
canonical := p.Clone()
// https://github.com/golang/go/issues/17084#issuecomment-246645354
canonical.FollowSymLink()
if absPath, err := canonical.Abs(); err == nil {
canonical = absPath
}
return canonical
}