github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/strutil/pathiter.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package strutil
    21  
    22  import (
    23  	"fmt"
    24  	"path/filepath"
    25  	"strings"
    26  )
    27  
    28  // PathIterator traverses through parts (directories and files) of some
    29  // path. The filesystem is never consulted, traversal is done purely in memory.
    30  //
    31  // The iterator is useful in implementing secure traversal of absolute paths
    32  // using the common idiom of opening the root directory followed by a chain of
    33  // openat calls.
    34  //
    35  // A simple example on how to use the iterator:
    36  // ```
    37  // iter:= NewPathIterator(path)
    38  // for iter.Next() {
    39  //    // Use iter.CurrentName() with openat(2) family of functions.
    40  //    // Use iter.CurrentPath() or iter.CurrentBase() for context.
    41  // }
    42  // ```
    43  type PathIterator struct {
    44  	path        string
    45  	left, right int
    46  	depth       int
    47  }
    48  
    49  // NewPathIterator returns an iterator for traversing the given path.
    50  // The path is passed through filepath.Clean automatically.
    51  func NewPathIterator(path string) (*PathIterator, error) {
    52  	cleanPath := filepath.Clean(path)
    53  	if cleanPath != path && cleanPath+"/" != path {
    54  		return nil, fmt.Errorf("cannot iterate over unclean path %q", path)
    55  	}
    56  	return &PathIterator{path: path}, nil
    57  }
    58  
    59  // Path returns the path being traversed.
    60  func (iter *PathIterator) Path() string {
    61  	return iter.path
    62  }
    63  
    64  // CurrentName returns the name of the current path element.
    65  // The return value may end with '/'. Use CleanName to avoid that.
    66  func (iter *PathIterator) CurrentName() string {
    67  	return iter.path[iter.left:iter.right]
    68  }
    69  
    70  // CurrentCleanName returns the same value as Name with right slash trimmed.
    71  func (iter *PathIterator) CurrentCleanName() string {
    72  	if iter.right > 0 && iter.path[iter.right-1:iter.right] == "/" {
    73  		return iter.path[iter.left : iter.right-1]
    74  	}
    75  	return iter.path[iter.left:iter.right]
    76  }
    77  
    78  // CurrentPath returns the prefix of path that was traversed, including the current name.
    79  func (iter *PathIterator) CurrentPath() string {
    80  	return iter.path[:iter.right]
    81  }
    82  
    83  // CurrentBase returns the prefix of the path that was traversed,
    84  // excluding the current name.  The result never ends in '/' except if
    85  // current base is root.
    86  func (iter *PathIterator) CurrentBase() string {
    87  	if iter.left > 0 && iter.path[iter.left-1] == '/' && iter.path[:iter.left] != "/" {
    88  		return iter.path[:iter.left-1]
    89  	}
    90  	return iter.path[:iter.left]
    91  }
    92  
    93  // Depth returns the directory depth of the current path.
    94  //
    95  // This is equal to the number of traversed directories, including that of the
    96  // root directory.
    97  func (iter *PathIterator) Depth() int {
    98  	return iter.depth
    99  }
   100  
   101  // Next advances the iterator to the next name, returning true if one is found.
   102  //
   103  // If this method returns false then no change is made and all helper methods
   104  // retain their previous return values.
   105  func (iter *PathIterator) Next() bool {
   106  	// Initial state
   107  	// P: "foo/bar"
   108  	// L:  ^
   109  	// R:  ^
   110  	//
   111  	// Next is called
   112  	// P: "foo/bar"
   113  	// L:  ^  |
   114  	// R:     ^
   115  	//
   116  	// Next is called
   117  	// P: "foo/bar"
   118  	// L:     ^   |
   119  	// R:         ^
   120  
   121  	// Next is called but returns false
   122  	// P: "foo/bar"
   123  	// L:     ^   |
   124  	// R:         ^
   125  	if iter.right >= len(iter.path) {
   126  		return false
   127  	}
   128  	iter.left = iter.right
   129  	if idx := strings.IndexRune(iter.path[iter.right:], '/'); idx != -1 {
   130  		iter.right += idx + 1
   131  	} else {
   132  		iter.right = len(iter.path)
   133  	}
   134  	iter.depth++
   135  	return true
   136  }
   137  
   138  // Rewind returns the iterator to the initial state, allowing the path to be traversed again.
   139  func (iter *PathIterator) Rewind() {
   140  	iter.left = 0
   141  	iter.right = 0
   142  	iter.depth = 0
   143  }