...

Source file src/sigs.k8s.io/kustomize/kyaml/filesys/fsnode.go

Documentation: sigs.k8s.io/kustomize/kyaml/filesys

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package filesys
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"log"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"sort"
    15  	"strings"
    16  
    17  	"sigs.k8s.io/kustomize/kyaml/errors"
    18  )
    19  
    20  var _ File = &fsNode{}
    21  var _ FileSystem = &fsNode{}
    22  
    23  // fsNode is either a file or a directory.
    24  type fsNode struct {
    25  	// What node owns me?
    26  	parent *fsNode
    27  
    28  	// Value to return as the Name() when the
    29  	// parent is nil.
    30  	nilParentName string
    31  
    32  	// A directory mapping names to nodes.
    33  	// If dir is nil, then self node is a file.
    34  	// If dir is non-nil, then self node is a directory,
    35  	// albeit possibly an empty directory.
    36  	dir map[string]*fsNode
    37  
    38  	// if this node is a file, this is the content.
    39  	content []byte
    40  
    41  	// if offset is not nil the file is open and it tracks
    42  	// the current file offset.
    43  	offset *int
    44  }
    45  
    46  // MakeEmptyDirInMemory returns an empty directory.
    47  // The paths of nodes in this object will never
    48  // report a leading Separator, meaning they
    49  // aren't "absolute" in the sense defined by
    50  // https://golang.org/pkg/path/filepath/#IsAbs.
    51  func MakeEmptyDirInMemory() *fsNode {
    52  	return &fsNode{
    53  		dir: make(map[string]*fsNode),
    54  	}
    55  }
    56  
    57  // MakeFsInMemory returns an empty 'file system'.
    58  // The paths of nodes in this object will always
    59  // report a leading Separator, meaning they
    60  // are "absolute" in the sense defined by
    61  // https://golang.org/pkg/path/filepath/#IsAbs.
    62  // This is a relevant difference when using Walk,
    63  // Glob, Match, etc.
    64  func MakeFsInMemory() FileSystem {
    65  	return &fsNode{
    66  		nilParentName: Separator,
    67  		dir:           make(map[string]*fsNode),
    68  	}
    69  }
    70  
    71  // Name returns the name of the node.
    72  func (n *fsNode) Name() string {
    73  	if n.parent == nil {
    74  		// Unable to lookup name in parent.
    75  		return n.nilParentName
    76  	}
    77  	if !n.parent.isNodeADir() {
    78  		log.Fatal("parent not a dir")
    79  	}
    80  	for key, value := range n.parent.dir {
    81  		if value == n {
    82  			return key
    83  		}
    84  	}
    85  	log.Fatal("unable to find fsNode name")
    86  	return ""
    87  }
    88  
    89  // Path returns the full path to the node.
    90  func (n *fsNode) Path() string {
    91  	if n.parent == nil {
    92  		return n.nilParentName
    93  	}
    94  	if !n.parent.isNodeADir() {
    95  		log.Fatal("parent not a dir, structural error")
    96  	}
    97  	return filepath.Join(n.parent.Path(), n.Name())
    98  }
    99  
   100  // mySplit trims trailing separators from the directory
   101  // result of filepath.Split.
   102  func mySplit(s string) (string, string) {
   103  	dName, fName := filepath.Split(s)
   104  	return StripTrailingSeps(dName), fName
   105  }
   106  
   107  func (n *fsNode) addFile(name string, c []byte) (result *fsNode, err error) {
   108  	parent := n
   109  	dName, fileName := mySplit(name)
   110  	if dName != "" {
   111  		parent, err = parent.addDir(dName)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  	}
   116  	if !isLegalFileNameForCreation(fileName) {
   117  		return nil, fmt.Errorf(
   118  			"illegal name '%s' in file creation", fileName)
   119  	}
   120  	result, ok := parent.dir[fileName]
   121  	if ok {
   122  		// File already exists; overwrite it.
   123  		if result.offset != nil {
   124  			return nil, fmt.Errorf("cannot add already opened file '%s'", n.Path())
   125  		}
   126  		result.content = append(result.content[:0], c...)
   127  		return result, nil
   128  	}
   129  	result = &fsNode{
   130  		content: append([]byte(nil), c...),
   131  		parent:  parent,
   132  	}
   133  	parent.dir[fileName] = result
   134  	return result, nil
   135  }
   136  
   137  // Create implements FileSystem.
   138  // Create makes an empty file.
   139  func (n *fsNode) Create(path string) (result File, err error) {
   140  	f, err := n.AddFile(path, nil)
   141  	if err != nil {
   142  		return f, err
   143  	}
   144  	f.offset = new(int)
   145  	return f, nil
   146  }
   147  
   148  // WriteFile implements FileSystem.
   149  func (n *fsNode) WriteFile(path string, d []byte) error {
   150  	_, err := n.AddFile(path, d)
   151  	return err
   152  }
   153  
   154  // AddFile adds a file and any necessary containing
   155  // directories to the node.
   156  func (n *fsNode) AddFile(
   157  	name string, c []byte) (result *fsNode, err error) {
   158  	if n.dir == nil {
   159  		return nil, fmt.Errorf(
   160  			"cannot add a file to a non-directory '%s'", n.Name())
   161  	}
   162  	return n.addFile(cleanQueryPath(name), c)
   163  }
   164  
   165  func (n *fsNode) addDir(path string) (result *fsNode, err error) {
   166  	parent := n
   167  	dName, subDirName := mySplit(path)
   168  	if dName != "" {
   169  		parent, err = n.addDir(dName)
   170  		if err != nil {
   171  			return nil, err
   172  		}
   173  	}
   174  	switch subDirName {
   175  	case "", SelfDir:
   176  		return n, nil
   177  	case ParentDir:
   178  		if n.parent == nil {
   179  			return nil, fmt.Errorf(
   180  				"cannot add a directory above '%s'", n.Path())
   181  		}
   182  		return n.parent, nil
   183  	default:
   184  		if !isLegalFileNameForCreation(subDirName) {
   185  			return nil, fmt.Errorf(
   186  				"illegal name '%s' in directory creation", subDirName)
   187  		}
   188  		result, ok := parent.dir[subDirName]
   189  		if ok {
   190  			if result.isNodeADir() {
   191  				// it's already there.
   192  				return result, nil
   193  			}
   194  			return nil, fmt.Errorf(
   195  				"cannot make dir '%s'; a file of that name already exists in '%s'",
   196  				subDirName, parent.Name())
   197  		}
   198  		result = &fsNode{
   199  			dir:    make(map[string]*fsNode),
   200  			parent: parent,
   201  		}
   202  		parent.dir[subDirName] = result
   203  		return result, nil
   204  	}
   205  }
   206  
   207  // Mkdir implements FileSystem.
   208  // Mkdir creates a directory.
   209  func (n *fsNode) Mkdir(path string) error {
   210  	_, err := n.AddDir(path)
   211  	return err
   212  }
   213  
   214  // MkdirAll implements FileSystem.
   215  // MkdirAll creates a directory.
   216  func (n *fsNode) MkdirAll(path string) error {
   217  	_, err := n.AddDir(path)
   218  	return err
   219  }
   220  
   221  // AddDir adds a directory to the node, not complaining
   222  // if it is already there.
   223  func (n *fsNode) AddDir(path string) (result *fsNode, err error) {
   224  	if n.dir == nil {
   225  		return nil, fmt.Errorf(
   226  			"cannot add a directory to file node '%s'", n.Name())
   227  	}
   228  	return n.addDir(cleanQueryPath(path))
   229  }
   230  
   231  // CleanedAbs implements FileSystem.
   232  func (n *fsNode) CleanedAbs(path string) (ConfirmedDir, string, error) {
   233  	node, err := n.Find(path)
   234  	if err != nil {
   235  		return "", "", errors.WrapPrefixf(err, "unable to clean")
   236  	}
   237  	if node == nil {
   238  		return "", "", notExistError(path)
   239  	}
   240  	if node.isNodeADir() {
   241  		return ConfirmedDir(node.Path()), "", nil
   242  	}
   243  	return ConfirmedDir(node.parent.Path()), node.Name(), nil
   244  }
   245  
   246  // Exists implements FileSystem.
   247  // Exists returns true if the path exists.
   248  func (n *fsNode) Exists(path string) bool {
   249  	if !n.isNodeADir() {
   250  		return n.Name() == path
   251  	}
   252  	result, err := n.Find(path)
   253  	if err != nil {
   254  		return false
   255  	}
   256  	return result != nil
   257  }
   258  
   259  func cleanQueryPath(path string) string {
   260  	// Always ignore leading separator?
   261  	// Remember that filepath.Clean returns "." if
   262  	// given an empty string argument.
   263  	return filepath.Clean(StripLeadingSeps(path))
   264  }
   265  
   266  // Find finds the given node, else nil if not found.
   267  // Return error on structural/argument errors.
   268  func (n *fsNode) Find(path string) (*fsNode, error) {
   269  	if !n.isNodeADir() {
   270  		return nil, fmt.Errorf("can only find inside a dir")
   271  	}
   272  	if path == "" {
   273  		// Special case; check *before* cleaning and *before*
   274  		// comparison to nilParentName.
   275  		return nil, nil
   276  	}
   277  	if (n.parent == nil && path == n.nilParentName) || path == SelfDir {
   278  		// Special case
   279  		return n, nil
   280  	}
   281  	return n.findIt(cleanQueryPath(path))
   282  }
   283  
   284  func (n *fsNode) findIt(path string) (result *fsNode, err error) {
   285  	parent := n
   286  	dName, item := mySplit(path)
   287  	if dName != "" {
   288  		parent, err = n.findIt(dName)
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  		if parent == nil {
   293  			// all done, target doesn't exist.
   294  			return nil, nil
   295  		}
   296  	}
   297  	if !parent.isNodeADir() {
   298  		return nil, fmt.Errorf("'%s' is not a directory", parent.Path())
   299  	}
   300  	return parent.dir[item], nil
   301  }
   302  
   303  // RemoveAll implements FileSystem.
   304  // RemoveAll removes an item and everything it contains.
   305  func (n *fsNode) RemoveAll(path string) error {
   306  	result, err := n.Find(path)
   307  	if err != nil {
   308  		return err
   309  	}
   310  	if result == nil {
   311  		// If the path doesn't exist, no need to remove anything.
   312  		return nil
   313  	}
   314  	return result.Remove()
   315  }
   316  
   317  // Remove drop the node, and everything it contains, from its parent.
   318  func (n *fsNode) Remove() error {
   319  	if n.parent == nil {
   320  		return fmt.Errorf("cannot remove a root node")
   321  	}
   322  	if !n.parent.isNodeADir() {
   323  		log.Fatal("parent not a dir")
   324  	}
   325  	for key, value := range n.parent.dir {
   326  		if value == n {
   327  			delete(n.parent.dir, key)
   328  			return nil
   329  		}
   330  	}
   331  	log.Fatal("unable to find self in parent")
   332  	return nil
   333  }
   334  
   335  // isNodeADir returns true if the node is a directory.
   336  // Cannot collide with the poorly named "IsDir".
   337  func (n *fsNode) isNodeADir() bool {
   338  	return n.dir != nil
   339  }
   340  
   341  // IsDir implements FileSystem.
   342  // IsDir returns true if the argument resolves
   343  // to a directory rooted at the node.
   344  func (n *fsNode) IsDir(path string) bool {
   345  	result, err := n.Find(path)
   346  	if err != nil || result == nil {
   347  		return false
   348  	}
   349  	return result.isNodeADir()
   350  }
   351  
   352  // ReadDir implements FileSystem.
   353  func (n *fsNode) ReadDir(path string) ([]string, error) {
   354  	if !n.Exists(path) {
   355  		return nil, notExistError(path)
   356  	}
   357  	if !n.IsDir(path) {
   358  		return nil, fmt.Errorf("%s is not a directory", path)
   359  	}
   360  
   361  	dir, err := n.Find(path)
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  	if dir == nil {
   366  		return nil, fmt.Errorf("could not find directory %s", path)
   367  	}
   368  
   369  	keys := make([]string, len(dir.dir))
   370  	i := 0
   371  	for k := range dir.dir {
   372  		keys[i] = k
   373  		i++
   374  	}
   375  	return keys, nil
   376  }
   377  
   378  // Size returns the size of the node.
   379  func (n *fsNode) Size() int64 {
   380  	if n.isNodeADir() {
   381  		return int64(len(n.dir))
   382  	}
   383  	return int64(len(n.content))
   384  }
   385  
   386  // Open implements FileSystem.
   387  // Open opens the node in read-write mode and sets the offset its start.
   388  // Writing right after opening the file will replace the original content
   389  // and move the offset forward, as with a file opened with O_RDWR | O_CREATE.
   390  //
   391  // As an example, let's consider a file with content "content":
   392  // - open: sets offset to start, content is "content"
   393  // - write "@": offset increases by one, the content is now "@ontent"
   394  // - read the rest: since offset is 1, the read operation returns "ontent"
   395  // - write "$": offset is at EOF, so "$" is appended and content is now "@ontent$"
   396  // - read the rest: returns 0 bytes and EOF
   397  // - close: the content is still "@ontent$"
   398  func (n *fsNode) Open(path string) (File, error) {
   399  	result, err := n.Find(path)
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  	if result == nil {
   404  		return nil, notExistError(path)
   405  	}
   406  	if result.offset != nil {
   407  		return nil, fmt.Errorf("cannot open previously opened file '%s'", path)
   408  	}
   409  	result.offset = new(int)
   410  	return result, nil
   411  }
   412  
   413  // Close marks the node closed.
   414  func (n *fsNode) Close() error {
   415  	if n.offset == nil {
   416  		return fmt.Errorf("cannot close already closed file '%s'", n.Path())
   417  	}
   418  	n.offset = nil
   419  	return nil
   420  }
   421  
   422  // ReadFile implements FileSystem.
   423  func (n *fsNode) ReadFile(path string) (c []byte, err error) {
   424  	result, err := n.Find(path)
   425  	if err != nil {
   426  		return nil, err
   427  	}
   428  	if result == nil {
   429  		return nil, notExistError(path)
   430  	}
   431  	if result.isNodeADir() {
   432  		return nil, fmt.Errorf("cannot read content from non-file '%s'", n.Path())
   433  	}
   434  	c = make([]byte, len(result.content))
   435  	copy(c, result.content)
   436  	return c, nil
   437  }
   438  
   439  // Read returns the content of the file node.
   440  func (n *fsNode) Read(d []byte) (c int, err error) {
   441  	if n.isNodeADir() {
   442  		return 0, fmt.Errorf(
   443  			"cannot read content from non-file '%s'", n.Path())
   444  	}
   445  	if n.offset == nil {
   446  		return 0, fmt.Errorf("cannot read from closed file '%s'", n.Path())
   447  	}
   448  
   449  	rest := n.content[*n.offset:]
   450  	if len(d) < len(rest) {
   451  		rest = rest[:len(d)]
   452  	} else {
   453  		err = io.EOF
   454  	}
   455  	copy(d, rest)
   456  	*n.offset += len(rest)
   457  	return len(rest), err
   458  }
   459  
   460  // Write saves the contents of the argument to the file node.
   461  func (n *fsNode) Write(p []byte) (c int, err error) {
   462  	if n.isNodeADir() {
   463  		return 0, fmt.Errorf(
   464  			"cannot write content to non-file '%s'", n.Path())
   465  	}
   466  	if n.offset == nil {
   467  		return 0, fmt.Errorf("cannot write to closed file '%s'", n.Path())
   468  	}
   469  	n.content = append(n.content[:*n.offset], p...)
   470  	*n.offset = len(n.content)
   471  	return len(p), nil
   472  }
   473  
   474  // ContentMatches returns true if v matches fake file's content.
   475  func (n *fsNode) ContentMatches(v []byte) bool {
   476  	return bytes.Equal(v, n.content)
   477  }
   478  
   479  // GetContent the content of a fake file.
   480  func (n *fsNode) GetContent() []byte {
   481  	return n.content
   482  }
   483  
   484  // Stat returns an instance of FileInfo.
   485  func (n *fsNode) Stat() (os.FileInfo, error) {
   486  	return fileInfo{node: n}, nil
   487  }
   488  
   489  // Walk implements FileSystem.
   490  func (n *fsNode) Walk(path string, walkFn filepath.WalkFunc) error {
   491  	result, err := n.Find(path)
   492  	if err != nil {
   493  		return err
   494  	}
   495  	if result == nil {
   496  		return notExistError(path)
   497  	}
   498  	return result.WalkMe(walkFn)
   499  }
   500  
   501  // Walk runs the given walkFn on each node.
   502  func (n *fsNode) WalkMe(walkFn filepath.WalkFunc) error {
   503  	fi, err := n.Stat()
   504  	// always visit self first
   505  	err = walkFn(n.Path(), fi, err)
   506  	if !n.isNodeADir() {
   507  		// it's a file, so nothing more to do
   508  		return err
   509  	}
   510  	// process self as a directory
   511  	if err == filepath.SkipDir {
   512  		return nil
   513  	}
   514  	// Walk is supposed to visit in lexical order.
   515  	for _, k := range n.sortedDirEntries() {
   516  		if err := n.dir[k].WalkMe(walkFn); err != nil {
   517  			if err == filepath.SkipDir {
   518  				// stop processing this directory
   519  				break
   520  			}
   521  			// bail out completely
   522  			return err
   523  		}
   524  	}
   525  	return nil
   526  }
   527  
   528  func (n *fsNode) sortedDirEntries() []string {
   529  	keys := make([]string, len(n.dir))
   530  	i := 0
   531  	for k := range n.dir {
   532  		keys[i] = k
   533  		i++
   534  	}
   535  	sort.Strings(keys)
   536  	return keys
   537  }
   538  
   539  // FileCount returns a count of files.
   540  // Directories, empty or otherwise, not counted.
   541  func (n *fsNode) FileCount() int {
   542  	count := 0
   543  	n.WalkMe(func(path string, info os.FileInfo, err error) error {
   544  		if err != nil {
   545  			return err
   546  		}
   547  		if !info.IsDir() {
   548  			count++
   549  		}
   550  		return nil
   551  	})
   552  	return count
   553  }
   554  
   555  func (n *fsNode) DebugPrint() {
   556  	n.WalkMe(func(path string, info os.FileInfo, err error) error {
   557  		if err != nil {
   558  			fmt.Printf("err '%v' at path %q\n", err, path)
   559  			return nil
   560  		}
   561  		if info.IsDir() {
   562  			if info.Size() == 0 {
   563  				fmt.Println("empty dir: " + path)
   564  			}
   565  		} else {
   566  			fmt.Println("     file: " + path)
   567  		}
   568  		return nil
   569  	})
   570  }
   571  
   572  var legalFileNamePattern = regexp.MustCompile("^[a-zA-Z0-9-_.:]+$")
   573  
   574  // This rules enforced here should be simpler and tighter
   575  // than what's allowed on a real OS.
   576  // Should be fine for testing or in-memory purposes.
   577  func isLegalFileNameForCreation(n string) bool {
   578  	if n == "" || n == SelfDir || !legalFileNamePattern.MatchString(n) {
   579  		return false
   580  	}
   581  	return !strings.Contains(n, ParentDir)
   582  }
   583  
   584  // RegExpGlob returns a list of file paths matching the regexp.
   585  // Excludes directories.
   586  func (n *fsNode) RegExpGlob(pattern string) ([]string, error) {
   587  	var result []string
   588  	var expression = regexp.MustCompile(pattern)
   589  	err := n.WalkMe(func(path string, info os.FileInfo, err error) error {
   590  		if err != nil {
   591  			return err
   592  		}
   593  		if !info.IsDir() {
   594  			if expression.MatchString(path) {
   595  				result = append(result, path)
   596  			}
   597  		}
   598  		return nil
   599  	})
   600  	if err != nil {
   601  		return nil, err
   602  	}
   603  	sort.Strings(result)
   604  	return result, nil
   605  }
   606  
   607  // Glob implements FileSystem.
   608  // Glob returns the list of file paths matching
   609  // per filepath.Match semantics, i.e. unlike RegExpGlob,
   610  // Match("foo/a*") will not match sub-sub directories of foo.
   611  // This is how /bin/ls behaves.
   612  func (n *fsNode) Glob(pattern string) ([]string, error) {
   613  	var result []string
   614  	var allFiles []string
   615  	err := n.WalkMe(func(path string, info os.FileInfo, err error) error {
   616  		if err != nil {
   617  			return err
   618  		}
   619  		if !info.IsDir() {
   620  			match, err := filepath.Match(pattern, path)
   621  			if err != nil {
   622  				return err
   623  			}
   624  			if match {
   625  				allFiles = append(allFiles, path)
   626  			}
   627  		}
   628  		return nil
   629  	})
   630  	if err != nil {
   631  		return nil, err
   632  	}
   633  	if IsHiddenFilePath(pattern) {
   634  		result = allFiles
   635  	} else {
   636  		result = RemoveHiddenFiles(allFiles)
   637  	}
   638  	sort.Strings(result)
   639  	return result, nil
   640  }
   641  
   642  // notExistError indicates that a file or directory does not exist.
   643  // Unwrapping returns os.ErrNotExist so errors.Is(err, os.ErrNotExist) works correctly.
   644  type notExistError string
   645  
   646  func (err notExistError) Error() string { return fmt.Sprintf("'%s' doesn't exist", string(err)) }
   647  func (err notExistError) Unwrap() error { return os.ErrNotExist }
   648  

View as plain text