xref: /OK3568_Linux_fs/yocto/poky/meta/lib/oe/cachedpath.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
1#
2# SPDX-License-Identifier: GPL-2.0-only
3#
4# 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
236