1# 2# Copyright OpenEmbedded Contributors 3# 4# SPDX-License-Identifier: GPL-2.0-only 5# 6# Based on standard python library functions but avoid 7# repeated stat calls. Its assumed the files will not change from under us 8# so we can cache stat calls. 9# 10 11import os 12import errno 13import stat as statmod 14 15class CachedPath(object): 16 def __init__(self): 17 self.statcache = {} 18 self.lstatcache = {} 19 self.normpathcache = {} 20 return 21 22 def updatecache(self, x): 23 x = self.normpath(x) 24 if x in self.statcache: 25 del self.statcache[x] 26 if x in self.lstatcache: 27 del self.lstatcache[x] 28 29 def normpath(self, path): 30 if path in self.normpathcache: 31 return self.normpathcache[path] 32 newpath = os.path.normpath(path) 33 self.normpathcache[path] = newpath 34 return newpath 35 36 def _callstat(self, path): 37 if path in self.statcache: 38 return self.statcache[path] 39 try: 40 st = os.stat(path) 41 self.statcache[path] = st 42 return st 43 except os.error: 44 self.statcache[path] = False 45 return False 46 47 # We might as well call lstat and then only 48 # call stat as well in the symbolic link case 49 # since this turns out to be much more optimal 50 # in real world usage of this cache 51 def callstat(self, path): 52 path = self.normpath(path) 53 self.calllstat(path) 54 return self.statcache[path] 55 56 def calllstat(self, path): 57 path = self.normpath(path) 58 if path in self.lstatcache: 59 return self.lstatcache[path] 60 #bb.error("LStatpath:" + path) 61 try: 62 lst = os.lstat(path) 63 self.lstatcache[path] = lst 64 if not statmod.S_ISLNK(lst.st_mode): 65 self.statcache[path] = lst 66 else: 67 self._callstat(path) 68 return lst 69 except (os.error, AttributeError): 70 self.lstatcache[path] = False 71 self.statcache[path] = False 72 return False 73 74 # This follows symbolic links, so both islink() and isdir() can be true 75 # for the same path ono systems that support symlinks 76 def isfile(self, path): 77 """Test whether a path is a regular file""" 78 st = self.callstat(path) 79 if not st: 80 return False 81 return statmod.S_ISREG(st.st_mode) 82 83 # Is a path a directory? 84 # This follows symbolic links, so both islink() and isdir() 85 # can be true for the same path on systems that support symlinks 86 def isdir(self, s): 87 """Return true if the pathname refers to an existing directory.""" 88 st = self.callstat(s) 89 if not st: 90 return False 91 return statmod.S_ISDIR(st.st_mode) 92 93 def islink(self, path): 94 """Test whether a path is a symbolic link""" 95 st = self.calllstat(path) 96 if not st: 97 return False 98 return statmod.S_ISLNK(st.st_mode) 99 100 # Does a path exist? 101 # This is false for dangling symbolic links on systems that support them. 102 def exists(self, path): 103 """Test whether a path exists. Returns False for broken symbolic links""" 104 if self.callstat(path): 105 return True 106 return False 107 108 def lexists(self, path): 109 """Test whether a path exists. Returns True for broken symbolic links""" 110 if self.calllstat(path): 111 return True 112 return False 113 114 # WARNING - this is not currently a drop in replacement since they return False 115 # rather than raise exceptions. 116 def stat(self, path): 117 return self.callstat(path) 118 119 # WARNING - this is not currently a drop in replacement since they return False 120 # rather than raise exceptions. 121 def lstat(self, path): 122 return self.calllstat(path) 123 124 def walk(self, top, topdown=True, onerror=None, followlinks=False): 125 # Matches os.walk, not os.path.walk() 126 127 # We may not have read permission for top, in which case we can't 128 # get a list of the files the directory contains. os.path.walk 129 # always suppressed the exception then, rather than blow up for a 130 # minor reason when (say) a thousand readable directories are still 131 # left to visit. That logic is copied here. 132 try: 133 names = os.listdir(top) 134 except os.error as err: 135 if onerror is not None: 136 onerror(err) 137 return 138 139 dirs, nondirs = [], [] 140 for name in names: 141 if self.isdir(os.path.join(top, name)): 142 dirs.append(name) 143 else: 144 nondirs.append(name) 145 146 if topdown: 147 yield top, dirs, nondirs 148 for name in dirs: 149 new_path = os.path.join(top, name) 150 if followlinks or not self.islink(new_path): 151 for x in self.walk(new_path, topdown, onerror, followlinks): 152 yield x 153 if not topdown: 154 yield top, dirs, nondirs 155 156 ## realpath() related functions 157 def __is_path_below(self, file, root): 158 return (file + os.path.sep).startswith(root) 159 160 def __realpath_rel(self, start, rel_path, root, loop_cnt, assume_dir): 161 """Calculates real path of symlink 'start' + 'rel_path' below 162 'root'; no part of 'start' below 'root' must contain symlinks. """ 163 have_dir = True 164 165 for d in rel_path.split(os.path.sep): 166 if not have_dir and not assume_dir: 167 raise OSError(errno.ENOENT, "no such directory %s" % start) 168 169 if d == os.path.pardir: # '..' 170 if len(start) >= len(root): 171 # do not follow '..' before root 172 start = os.path.dirname(start) 173 else: 174 # emit warning? 175 pass 176 else: 177 (start, have_dir) = self.__realpath(os.path.join(start, d), 178 root, loop_cnt, assume_dir) 179 180 assert(self.__is_path_below(start, root)) 181 182 return start 183 184 def __realpath(self, file, root, loop_cnt, assume_dir): 185 while self.islink(file) and len(file) >= len(root): 186 if loop_cnt == 0: 187 raise OSError(errno.ELOOP, file) 188 189 loop_cnt -= 1 190 target = os.path.normpath(os.readlink(file)) 191 192 if not os.path.isabs(target): 193 tdir = os.path.dirname(file) 194 assert(self.__is_path_below(tdir, root)) 195 else: 196 tdir = root 197 198 file = self.__realpath_rel(tdir, target, root, loop_cnt, assume_dir) 199 200 try: 201 is_dir = self.isdir(file) 202 except: 203 is_dir = False 204 205 return (file, is_dir) 206 207 def realpath(self, file, root, use_physdir = True, loop_cnt = 100, assume_dir = False): 208 """ Returns the canonical path of 'file' with assuming a 209 toplevel 'root' directory. When 'use_physdir' is set, all 210 preceding path components of 'file' will be resolved first; 211 this flag should be set unless it is guaranteed that there is 212 no symlink in the path. When 'assume_dir' is not set, missing 213 path components will raise an ENOENT error""" 214 215 root = os.path.normpath(root) 216 file = os.path.normpath(file) 217 218 if not root.endswith(os.path.sep): 219 # letting root end with '/' makes some things easier 220 root = root + os.path.sep 221 222 if not self.__is_path_below(file, root): 223 raise OSError(errno.EINVAL, "file '%s' is not below root" % file) 224 225 try: 226 if use_physdir: 227 file = self.__realpath_rel(root, file[(len(root) - 1):], root, loop_cnt, assume_dir) 228 else: 229 file = self.__realpath(file, root, loop_cnt, assume_dir)[0] 230 except OSError as e: 231 if e.errno == errno.ELOOP: 232 # make ELOOP more readable; without catching it, there will 233 # be printed a backtrace with 100s of OSError exceptions 234 # else 235 raise OSError(errno.ELOOP, 236 "too much recursions while resolving '%s'; loop in '%s'" % 237 (file, e.strerror)) 238 239 raise 240 241 return file 242