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