mirror of
https://github.com/cheat/cheat.git
synced 2026-05-27 19:48:44 +02:00
1f9b2647da
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.16.5 to 5.19.0. - [Release notes](https://github.com/go-git/go-git/releases) - [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md) - [Commits](https://github.com/go-git/go-git/compare/v5.16.5...v5.19.0) --- updated-dependencies: - dependency-name: github.com/go-git/go-git/v5 dependency-version: 5.19.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
445 lines
9.6 KiB
Go
445 lines
9.6 KiB
Go
package chroot
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/go-git/go-billy/v5"
|
|
"github.com/go-git/go-billy/v5/helper/polyfill"
|
|
)
|
|
|
|
// ChrootHelper is a helper to implement billy.Chroot.
|
|
// It is not a security boundary, callers that need containment should use a
|
|
// filesystem implementation that enforces paths at the OS boundary instead.
|
|
type ChrootHelper struct {
|
|
underlying billy.Filesystem
|
|
base string
|
|
}
|
|
|
|
const maxFollowedSymlinks = 8 // Aligns with POSIX_SYMLOOP_MAX
|
|
|
|
// New creates a new filesystem wrapping up the given 'fs'.
|
|
// The created filesystem has its base in the given ChrootHelperectory of the
|
|
// underlying filesystem.
|
|
func New(fs billy.Basic, base string) billy.Filesystem {
|
|
return &ChrootHelper{
|
|
underlying: polyfill.New(fs),
|
|
base: base,
|
|
}
|
|
}
|
|
|
|
func (fs *ChrootHelper) underlyingPath(filename string) (string, error) {
|
|
if isCrossBoundaries(filename) {
|
|
return "", billy.ErrCrossedBoundary
|
|
}
|
|
|
|
return fs.Join(fs.Root(), filename), nil
|
|
}
|
|
|
|
func (fs *ChrootHelper) followedPath(filename string, followFinal bool, op string) (string, error) {
|
|
fullpath, err := fs.underlyingPath(filename)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
sl, ok := fs.underlying.(billy.Symlink)
|
|
if !ok {
|
|
return fullpath, nil
|
|
}
|
|
|
|
rel, err := fs.relativeToRoot(fullpath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
fullpath, err = fs.resolveFollowedPath(rel, followFinal, op, sl)
|
|
if errors.Is(err, billy.ErrNotSupported) {
|
|
return fs.underlyingPath(filename)
|
|
}
|
|
|
|
return fullpath, err
|
|
}
|
|
|
|
func (fs *ChrootHelper) resolveFollowedPath(rel string, followFinal bool, op string, sl billy.Symlink) (string, error) {
|
|
if rel == "" {
|
|
return fs.resolveFollowedRoot(followFinal, op, sl)
|
|
}
|
|
|
|
parts := splitRelativePath(rel)
|
|
resolved := ""
|
|
followed := 0
|
|
|
|
for len(parts) > 0 {
|
|
part := parts[0]
|
|
parts = parts[1:]
|
|
|
|
currentRel := joinRelativePath(resolved, part)
|
|
currentPath := fs.Join(fs.Root(), currentRel)
|
|
if len(parts) == 0 && !followFinal {
|
|
return currentPath, nil
|
|
}
|
|
|
|
fi, err := sl.Lstat(currentPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return fs.Join(fs.Root(), joinRelativePath(append([]string{currentRel}, parts...)...)), nil
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
if fi.Mode()&os.ModeSymlink == 0 {
|
|
resolved = currentRel
|
|
continue
|
|
}
|
|
|
|
followed++
|
|
if followed > maxFollowedSymlinks {
|
|
return "", symlinkLoopError(op, currentPath)
|
|
}
|
|
|
|
target, err := sl.Readlink(currentPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
targetRel, err := fs.linkTargetRel(currentPath, target)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if targetRel == currentRel {
|
|
return "", symlinkLoopError(op, currentPath)
|
|
}
|
|
|
|
parts = append(splitRelativePath(targetRel), parts...)
|
|
resolved = ""
|
|
}
|
|
|
|
return fs.Join(fs.Root(), resolved), nil
|
|
}
|
|
|
|
func symlinkLoopError(op, path string) error {
|
|
return &os.PathError{Op: op, Path: path, Err: syscall.ELOOP}
|
|
}
|
|
|
|
func (fs *ChrootHelper) resolveFollowedRoot(followFinal bool, op string, sl billy.Symlink) (string, error) {
|
|
root := fs.Join(fs.Root(), "")
|
|
if !followFinal {
|
|
return root, nil
|
|
}
|
|
|
|
fi, err := sl.Lstat(root)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return root, nil
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
if fi.Mode()&os.ModeSymlink == 0 {
|
|
return root, nil
|
|
}
|
|
|
|
target, err := sl.Readlink(root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
targetRel, err := fs.linkTargetRel(root, target)
|
|
if err != nil {
|
|
return root, err
|
|
}
|
|
if targetRel == "" {
|
|
return "", symlinkLoopError(op, root)
|
|
}
|
|
|
|
return fs.resolveFollowedPath(targetRel, followFinal, op, sl)
|
|
}
|
|
|
|
func (fs *ChrootHelper) relativeToRoot(filename string) (string, error) {
|
|
rel, err := filepath.Rel(filepath.Clean(fs.Root()), filepath.Clean(filename))
|
|
if err != nil || isCrossBoundaries(rel) {
|
|
return "", billy.ErrCrossedBoundary
|
|
}
|
|
|
|
if rel == "." {
|
|
return "", nil
|
|
}
|
|
return rel, nil
|
|
}
|
|
|
|
func (fs *ChrootHelper) linkTargetRel(linkPath, target string) (string, error) {
|
|
target = filepath.FromSlash(target)
|
|
if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
|
|
return fs.relativeToRoot(target)
|
|
}
|
|
|
|
return fs.relativeToRoot(fs.Join(filepath.Dir(linkPath), target))
|
|
}
|
|
|
|
func splitRelativePath(filename string) []string {
|
|
filename = filepath.Clean(filename)
|
|
if filename == "" || filename == "." {
|
|
return nil
|
|
}
|
|
|
|
return strings.Split(filepath.ToSlash(filename), "/")
|
|
}
|
|
|
|
func joinRelativePath(elem ...string) string {
|
|
parts := make([]string, 0, len(elem))
|
|
for _, part := range elem {
|
|
if part == "" || part == "." {
|
|
continue
|
|
}
|
|
parts = append(parts, part)
|
|
}
|
|
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
return filepath.Join(parts...)
|
|
}
|
|
|
|
func isCreateExclusive(flag int) bool {
|
|
return flag&os.O_CREATE != 0 && flag&os.O_EXCL != 0
|
|
}
|
|
|
|
func isCrossBoundaries(name string) bool {
|
|
name = filepath.ToSlash(name)
|
|
name = strings.TrimLeft(name, "/")
|
|
name = path.Clean(name)
|
|
|
|
return name == ".." || strings.HasPrefix(name, "../")
|
|
}
|
|
|
|
func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
|
|
fullpath, err := fs.followedPath(filename, true, "create")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f, err := fs.underlying.Create(fullpath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newFile(fs, f, filename), nil
|
|
}
|
|
|
|
func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
|
|
fullpath, err := fs.followedPath(filename, true, "open")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f, err := fs.underlying.Open(fullpath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newFile(fs, f, filename), nil
|
|
}
|
|
|
|
func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) {
|
|
fullpath, err := fs.followedPath(filename, !isCreateExclusive(flag), "open")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f, err := fs.underlying.OpenFile(fullpath, flag, mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newFile(fs, f, filename), nil
|
|
}
|
|
|
|
func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) {
|
|
fullpath, err := fs.followedPath(filename, true, "stat")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fi, err := fs.underlying.Stat(fullpath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fileInfo{FileInfo: fi, name: filepath.Base(filename)}, nil
|
|
}
|
|
|
|
func (fs *ChrootHelper) Rename(from, to string) error {
|
|
var err error
|
|
from, err = fs.underlyingPath(from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
to, err = fs.underlyingPath(to)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fs.underlying.Rename(from, to)
|
|
}
|
|
|
|
func (fs *ChrootHelper) Remove(path string) error {
|
|
fullpath, err := fs.underlyingPath(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fs.underlying.Remove(fullpath)
|
|
}
|
|
|
|
func (fs *ChrootHelper) Join(elem ...string) string {
|
|
return fs.underlying.Join(elem...)
|
|
}
|
|
|
|
func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) {
|
|
fullpath, err := fs.underlyingPath(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f, err := fs.underlying.(billy.TempFile).TempFile(fullpath, prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newFile(fs, f, fs.Join(dir, filepath.Base(f.Name()))), nil
|
|
}
|
|
|
|
func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) {
|
|
fullpath, err := fs.followedPath(path, true, "readdir")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return fs.underlying.(billy.Dir).ReadDir(fullpath)
|
|
}
|
|
|
|
func (fs *ChrootHelper) MkdirAll(filename string, perm os.FileMode) error {
|
|
fullpath, err := fs.underlyingPath(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fs.underlying.(billy.Dir).MkdirAll(fullpath, perm)
|
|
}
|
|
|
|
func (fs *ChrootHelper) Lstat(filename string) (os.FileInfo, error) {
|
|
fullpath, err := fs.underlyingPath(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return fs.underlying.(billy.Symlink).Lstat(fullpath)
|
|
}
|
|
|
|
func (fs *ChrootHelper) Symlink(target, link string) error {
|
|
target = filepath.FromSlash(target)
|
|
|
|
// only rewrite target if it's already absolute
|
|
if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
|
|
target = fs.Join(fs.Root(), target)
|
|
target = filepath.Clean(filepath.FromSlash(target))
|
|
}
|
|
|
|
link, err := fs.underlyingPath(link)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fs.underlying.(billy.Symlink).Symlink(target, link)
|
|
}
|
|
|
|
func (fs *ChrootHelper) Readlink(link string) (string, error) {
|
|
fullpath, err := fs.underlyingPath(link)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
target, err := fs.underlying.(billy.Symlink).Readlink(fullpath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !filepath.IsAbs(target) && !strings.HasPrefix(target, string(filepath.Separator)) {
|
|
return target, nil
|
|
}
|
|
|
|
target, err = filepath.Rel(fs.base, target)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(os.PathSeparator) + target, nil
|
|
}
|
|
|
|
func (fs *ChrootHelper) Chmod(path string, mode os.FileMode) error {
|
|
fullpath, err := fs.underlyingPath(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c, ok := fs.underlying.(billy.Chmod)
|
|
if !ok {
|
|
return errors.New("underlying fs does not implement billy.Chmod")
|
|
}
|
|
return c.Chmod(fullpath, mode)
|
|
}
|
|
|
|
func (fs *ChrootHelper) Chroot(path string) (billy.Filesystem, error) {
|
|
fullpath, err := fs.underlyingPath(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return New(fs.underlying, fullpath), nil
|
|
}
|
|
|
|
func (fs *ChrootHelper) Root() string {
|
|
return fs.base
|
|
}
|
|
|
|
func (fs *ChrootHelper) Underlying() billy.Basic {
|
|
return fs.underlying
|
|
}
|
|
|
|
// Capabilities implements the Capable interface.
|
|
func (fs *ChrootHelper) Capabilities() billy.Capability {
|
|
return billy.Capabilities(fs.underlying)
|
|
}
|
|
|
|
type file struct {
|
|
billy.File
|
|
name string
|
|
}
|
|
|
|
type fileInfo struct {
|
|
os.FileInfo
|
|
name string
|
|
}
|
|
|
|
func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
|
|
filename = fs.Join(fs.Root(), filename)
|
|
filename, _ = filepath.Rel(fs.Root(), filename)
|
|
|
|
return &file{
|
|
File: f,
|
|
name: filename,
|
|
}
|
|
}
|
|
|
|
func (f *file) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
func (fi fileInfo) Name() string {
|
|
return fi.name
|
|
}
|