mirror of
https://github.com/vbatts/go-mtree.git
synced 2025-10-03 20:21:01 +00:00
The previous logic was overly complicated and can be simplified a lot (especially now that we have helpful generic stdlib packages for dealing with maps). There are still several bugs in this, but the intention of this patch is to just modernise the code without making any functional changes. Bugfixes will come later. Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
196 lines
6.1 KiB
Go
196 lines
6.1 KiB
Go
package mtree
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/vbatts/go-mtree/pkg/govis"
|
|
)
|
|
|
|
type byPos []Entry
|
|
|
|
func (bp byPos) Len() int { return len(bp) }
|
|
func (bp byPos) Less(i, j int) bool { return bp[i].Pos < bp[j].Pos }
|
|
func (bp byPos) Swap(i, j int) { bp[i], bp[j] = bp[j], bp[i] }
|
|
|
|
// Entry is each component of content in the mtree spec file
|
|
type Entry struct {
|
|
Parent *Entry // up
|
|
Children []*Entry // down
|
|
Prev, Next *Entry // left, right
|
|
Set *Entry // current `/set` for additional keywords
|
|
Pos int // order in the spec
|
|
Raw string // file or directory name
|
|
Name string // file or directory name
|
|
Keywords []KeyVal // TODO(vbatts) maybe a keyword typed set of values?
|
|
Type EntryType
|
|
}
|
|
|
|
// Descend searches thru an Entry's children to find the Entry associated with
|
|
// `filename`. Directories are stored at the end of an Entry's children so do a
|
|
// traverse backwards. If you descend to a "."
|
|
func (e Entry) Descend(filename string) *Entry {
|
|
if filename == "." || filename == "" {
|
|
return &e
|
|
}
|
|
numChildren := len(e.Children)
|
|
for i := range e.Children {
|
|
c := e.Children[numChildren-1-i]
|
|
if c.Name == filename {
|
|
return c
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Find is a wrapper around Descend that takes in a whole string path and tries
|
|
// to find that Entry
|
|
func (e Entry) Find(filepath string) *Entry {
|
|
resultnode := &e
|
|
for _, path := range strings.Split(filepath, "/") {
|
|
encoded, err := govis.Vis(path, DefaultVisFlags)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
resultnode = resultnode.Descend(encoded)
|
|
if resultnode == nil {
|
|
return nil
|
|
}
|
|
}
|
|
return resultnode
|
|
}
|
|
|
|
// Ascend gets the parent of an Entry. Serves mainly to maintain readability
|
|
// when traversing up and down an Entry tree
|
|
func (e Entry) Ascend() *Entry {
|
|
return e.Parent
|
|
}
|
|
|
|
// CleanPath makes a path safe for use with filepath.Join. This is done by not
|
|
// only cleaning the path, but also (if the path is relative) adding a leading
|
|
// '/' and cleaning it (then removing the leading '/'). This ensures that a
|
|
// path resulting from prepending another path will always resolve to lexically
|
|
// be a subdirectory of the prefixed path. This is all done lexically, so paths
|
|
// that include symlinks won't be safe as a result of using CleanPath.
|
|
//
|
|
// This code was copied from runc/libcontainer/utils/utils.go. It was
|
|
// originally written by myself, so I am dual-licensing it for the purpose of
|
|
// this project.
|
|
func CleanPath(path string) string {
|
|
// Deal with empty strings nicely.
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
|
|
// Ensure that all paths are cleaned (especially problematic ones like
|
|
// "/../../../../../" which can cause lots of issues).
|
|
path = filepath.Clean(path)
|
|
|
|
// If the path isn't absolute, we need to do more processing to fix paths
|
|
// such as "../../../../<etc>/some/path". We also shouldn't convert absolute
|
|
// paths to relative ones.
|
|
if !filepath.IsAbs(path) {
|
|
path = filepath.Clean(string(os.PathSeparator) + path)
|
|
// This can't fail, as (by definition) all paths are relative to root.
|
|
path, _ = filepath.Rel(string(os.PathSeparator), path)
|
|
}
|
|
|
|
// Clean the path again for good measure.
|
|
return filepath.Clean(path)
|
|
}
|
|
|
|
// Path provides the full path of the file, despite RelativeType or FullType. It
|
|
// will be in Unvis'd form.
|
|
func (e Entry) Path() (string, error) {
|
|
decodedName, err := govis.Unvis(e.Name, DefaultVisFlags)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
decodedName = CleanPath(decodedName)
|
|
if e.Parent == nil || e.Type == FullType {
|
|
return decodedName, nil
|
|
}
|
|
parentName, err := e.Parent.Path()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return CleanPath(filepath.Join(parentName, decodedName)), nil
|
|
}
|
|
|
|
// String joins a file with its associated keywords. The file name will be the
|
|
// Vis'd encoded version so that it can be parsed appropriately when Check'd.
|
|
func (e Entry) String() string {
|
|
if e.Raw != "" {
|
|
return e.Raw
|
|
}
|
|
if e.Type == BlankType {
|
|
return ""
|
|
}
|
|
if e.Type == DotDotType {
|
|
return e.Name
|
|
}
|
|
if e.Type == SpecialType || e.Type == FullType || inKeyValSlice("type=dir", e.Keywords) {
|
|
return fmt.Sprintf("%s %s", e.Name, strings.Join(KeyValToString(e.Keywords), " "))
|
|
}
|
|
return fmt.Sprintf(" %s %s", e.Name, strings.Join(KeyValToString(e.Keywords), " "))
|
|
}
|
|
|
|
// AllKeys returns the full set of KeyVal for the given entry, based on the
|
|
// /set keys as well as the entry-local keys. Entry-local keys always take
|
|
// precedence.
|
|
func (e Entry) AllKeys() []KeyVal {
|
|
if e.Set != nil {
|
|
return MergeKeyValSet(e.Set.Keywords, e.Keywords)
|
|
}
|
|
return e.Keywords
|
|
}
|
|
|
|
func (e Entry) allKeysMap() map[Keyword]KeyVal {
|
|
all := e.AllKeys()
|
|
keyMap := make(map[Keyword]KeyVal, len(all))
|
|
for _, kv := range all {
|
|
keyMap[kv.Keyword()] = kv
|
|
}
|
|
return keyMap
|
|
}
|
|
|
|
// IsDir checks the type= value for this entry on whether it is a directory
|
|
func (e Entry) IsDir() bool {
|
|
for _, kv := range e.AllKeys() {
|
|
if kv.Keyword().Prefix() == "type" {
|
|
return kv.Value() == "dir"
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EntryType are the formats of lines in an mtree spec file
|
|
type EntryType int
|
|
|
|
// The types of lines to be found in an mtree spec file
|
|
const (
|
|
SignatureType EntryType = iota // first line of the file, like `#mtree v2.0`
|
|
BlankType // blank lines are ignored
|
|
CommentType // Lines beginning with `#` are ignored
|
|
SpecialType // line that has `/` prefix issue a "special" command (currently only /set and /unset)
|
|
RelativeType // if the first white-space delimited word does not have a '/' in it. Options/keywords are applied.
|
|
DotDotType // .. - A relative path step. keywords/options are ignored
|
|
FullType // if the first word on the line has a `/` after the first character, it interpretted as a file pathname with options
|
|
)
|
|
|
|
// String returns the name of the EntryType
|
|
func (et EntryType) String() string {
|
|
return typeNames[et]
|
|
}
|
|
|
|
var typeNames = map[EntryType]string{
|
|
SignatureType: "SignatureType",
|
|
BlankType: "BlankType",
|
|
CommentType: "CommentType",
|
|
SpecialType: "SpecialType",
|
|
RelativeType: "RelativeType",
|
|
DotDotType: "DotDotType",
|
|
FullType: "FullType",
|
|
}
|