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 def stat(self, path): 115 return self.callstat(path) 116 117 def lstat(self, path): 118 return self.calllstat(path) 119 120 def walk(self, top, topdown=True, onerror=None, followlinks=False): 121 # Matches os.walk, not os.path.walk() 122 123 # We may not have read permission for top, in which case we can't 124 # get a list of the files the directory contains. os.path.walk 125 # always suppressed the exception then, rather than blow up for a 126 # minor reason when (say) a thousand readable directories are still 127 # left to visit. That logic is copied here. 128 try: 129 names = os.listdir(top) 130 except os.error as err: 131 if onerror is not None: 132 onerror(err) 133 return 134 135 dirs, nondirs = [], [] 136 for name in names: 137 if self.isdir(os.path.join(top, name)): 138 dirs.append(name) 139 else: 140 nondirs.append(name) 141 142 if topdown: 143 yield top, dirs, nondirs 144 for name in dirs: 145 new_path = os.path.join(top, name) 146 if followlinks or not self.islink(new_path): 147 for x in self.walk(new_path, topdown, onerror, followlinks): 148 yield x 149 if not topdown: 150 yield top, dirs, nondirs 151 152 ## realpath() related functions 153 def __is_path_below(self, file, root): 154 return (file + os.path.sep).startswith(root) 155 156 def __realpath_rel(self, start, rel_path, root, loop_cnt, assume_dir): 157 """Calculates real path of symlink 'start' + 'rel_path' below 158 'root'; no part of 'start' below 'root' must contain symlinks. """ 159 have_dir = True 160 161 for d in rel_path.split(os.path.sep): 162 if not have_dir and not assume_dir: 163 raise OSError(errno.ENOENT, "no such directory %s" % start) 164 165 if d == os.path.pardir: # '..' 166 if len(start) >= len(root): 167 # do not follow '..' before root 168 start = os.path.dirname(start) 169 else: 170 # emit warning? 171 pass 172 else: 173 (start, have_dir) = self.__realpath(os.path.join(start, d), 174 root, loop_cnt, assume_dir) 175 176 assert(self.__is_path_below(start, root)) 177 178 return start 179 180 def __realpath(self, file, root, loop_cnt, assume_dir): 181 while self.islink(file) and len(file) >= len(root): 182 if loop_cnt == 0: 183 raise OSError(errno.ELOOP, file) 184 185 loop_cnt -= 1 186 target = os.path.normpath(os.readlink(file)) 187 188 if not os.path.isabs(target): 189 tdir = os.path.dirname(file) 190 assert(self.__is_path_below(tdir, root)) 191 else: 192 tdir = root 193 194 file = self.__realpath_rel(tdir, target, root, loop_cnt, assume_dir) 195 196 try: 197 is_dir = self.isdir(file) 198 except: 199 is_dir = False 200 201 return (file, is_dir) 202 203 def realpath(self, file, root, use_physdir = True, loop_cnt = 100, assume_dir = False): 204 """ Returns the canonical path of 'file' with assuming a 205 toplevel 'root' directory. When 'use_physdir' is set, all 206 preceding path components of 'file' will be resolved first; 207 this flag should be set unless it is guaranteed that there is 208 no symlink in the path. When 'assume_dir' is not set, missing 209 path components will raise an ENOENT error""" 210 211 root = os.path.normpath(root) 212 file = os.path.normpath(file) 213 214 if not root.endswith(os.path.sep): 215 # letting root end with '/' makes some things easier 216 root = root + os.path.sep 217 218 if not self.__is_path_below(file, root): 219 raise OSError(errno.EINVAL, "file '%s' is not below root" % file) 220 221 try: 222 if use_physdir: 223 file = self.__realpath_rel(root, file[(len(root) - 1):], root, loop_cnt, assume_dir) 224 else: 225 file = self.__realpath(file, root, loop_cnt, assume_dir)[0] 226 except OSError as e: 227 if e.errno == errno.ELOOP: 228 # make ELOOP more readable; without catching it, there will 229 # be printed a backtrace with 100s of OSError exceptions 230 # else 231 raise OSError(errno.ELOOP, 232 "too much recursions while resolving '%s'; loop in '%s'" % 233 (file, e.strerror)) 234 235 raise 236 237 return file 238