github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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 }