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