1# Copyright 2009 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15""" Faked ``os.path`` module replacement. See ``fake_filesystem`` for usage.
16"""
17import errno
18import os
19import sys
20from stat import (
21    S_IFDIR,
22    S_IFMT,
23)
24from types import ModuleType
25from typing import (
26    List,
27    Optional,
28    Union,
29    Any,
30    Dict,
31    Tuple,
32    AnyStr,
33    overload,
34    ClassVar,
35    TYPE_CHECKING,
36)
37
38from pyfakefs.helpers import (
39    make_string_path,
40    to_string,
41    matching_string,
42)
43
44if TYPE_CHECKING:
45    from pyfakefs.fake_filesystem import FakeFilesystem
46    from pyfakefs.fake_os import FakeOsModule
47
48
49def _copy_module(old: ModuleType) -> ModuleType:
50    """Recompiles and creates new module object."""
51    saved = sys.modules.pop(old.__name__, None)
52    new = __import__(old.__name__)
53    if saved is not None:
54        sys.modules[old.__name__] = saved
55    return new
56
57
58class FakePathModule:
59    """Faked os.path module replacement.
60
61    FakePathModule should *only* be instantiated by FakeOsModule.  See the
62    FakeOsModule docstring for details.
63    """
64
65    _OS_PATH_COPY: Any = _copy_module(os.path)
66
67    devnull: ClassVar[str] = ""
68    sep: ClassVar[str] = ""
69    altsep: ClassVar[Optional[str]] = None
70    linesep: ClassVar[str] = ""
71    pathsep: ClassVar[str] = ""
72
73    @staticmethod
74    def dir() -> List[str]:
75        """Return the list of patched function names. Used for patching
76        functions imported from the module.
77        """
78        dir_list = [
79            "abspath",
80            "dirname",
81            "exists",
82            "expanduser",
83            "getatime",
84            "getctime",
85            "getmtime",
86            "getsize",
87            "isabs",
88            "isdir",
89            "isfile",
90            "islink",
91            "ismount",
92            "join",
93            "lexists",
94            "normcase",
95            "normpath",
96            "realpath",
97            "relpath",
98            "split",
99            "splitdrive",
100            "samefile",
101        ]
102        if sys.version_info >= (3, 12):
103            dir_list += ["isjunction", "splitroot"]
104        return dir_list
105
106    def __init__(self, filesystem: "FakeFilesystem", os_module: "FakeOsModule"):
107        """Init.
108
109        Args:
110            filesystem: FakeFilesystem used to provide file system information
111        """
112        self.filesystem = filesystem
113        self._os_path = self._OS_PATH_COPY
114        self._os_path.os = self.os = os_module  # type: ignore[attr-defined]
115        self.reset(filesystem)
116
117    @classmethod
118    def reset(cls, filesystem: "FakeFilesystem") -> None:
119        cls.sep = filesystem.path_separator
120        cls.altsep = filesystem.alternative_path_separator
121        cls.linesep = filesystem.line_separator()
122        cls.devnull = "nul" if filesystem.is_windows_fs else "/dev/null"
123        cls.pathsep = ";" if filesystem.is_windows_fs else ":"
124
125    def exists(self, path: AnyStr) -> bool:
126        """Determine whether the file object exists within the fake filesystem.
127
128        Args:
129            path: The path to the file object.
130
131        Returns:
132            (bool) `True` if the file exists.
133        """
134        return self.filesystem.exists(path)
135
136    def lexists(self, path: AnyStr) -> bool:
137        """Test whether a path exists.  Returns True for broken symbolic links.
138
139        Args:
140          path:  path to the symlink object.
141
142        Returns:
143          bool (if file exists).
144        """
145        return self.filesystem.exists(path, check_link=True)
146
147    def getsize(self, path: AnyStr):
148        """Return the file object size in bytes.
149
150        Args:
151          path:  path to the file object.
152
153        Returns:
154          file size in bytes.
155        """
156        file_obj = self.filesystem.resolve(path)
157        if (
158            self.filesystem.ends_with_path_separator(path)
159            and S_IFMT(file_obj.st_mode) != S_IFDIR
160        ):
161            error_nr = errno.EINVAL if self.filesystem.is_windows_fs else errno.ENOTDIR
162            self.filesystem.raise_os_error(error_nr, path)
163        return file_obj.st_size
164
165    def isabs(self, path: AnyStr) -> bool:
166        """Return True if path is an absolute pathname."""
167        if self.filesystem.is_windows_fs:
168            path = self.splitdrive(path)[1]
169        path = make_string_path(path)
170        return self.filesystem.starts_with_sep(path)
171
172    def isdir(self, path: AnyStr) -> bool:
173        """Determine if path identifies a directory."""
174        return self.filesystem.isdir(path)
175
176    def isfile(self, path: AnyStr) -> bool:
177        """Determine if path identifies a regular file."""
178        return self.filesystem.isfile(path)
179
180    def islink(self, path: AnyStr) -> bool:
181        """Determine if path identifies a symbolic link.
182
183        Args:
184            path: Path to filesystem object.
185
186        Returns:
187            `True` if path points to a symbolic link.
188
189        Raises:
190            TypeError: if path is None.
191        """
192        return self.filesystem.islink(path)
193
194    if sys.version_info >= (3, 12):
195
196        def isjunction(self, path: AnyStr) -> bool:
197            """Returns False. Junctions are never faked."""
198            return self.filesystem.isjunction(path)
199
200        def splitroot(self, path: AnyStr):
201            """Split a pathname into drive, root and tail.
202            Implementation taken from ntpath and posixpath.
203            """
204            return self.filesystem.splitroot(path)
205
206    def getmtime(self, path: AnyStr) -> float:
207        """Returns the modification time of the fake file.
208
209        Args:
210            path: the path to fake file.
211
212        Returns:
213            (int, float) the modification time of the fake file
214                         in number of seconds since the epoch.
215
216        Raises:
217            OSError: if the file does not exist.
218        """
219        try:
220            file_obj = self.filesystem.resolve(path)
221            return file_obj.st_mtime
222        except OSError:
223            self.filesystem.raise_os_error(errno.ENOENT, winerror=3)
224
225    def getatime(self, path: AnyStr) -> float:
226        """Returns the last access time of the fake file.
227
228        Note: Access time is not set automatically in fake filesystem
229            on access.
230
231        Args:
232            path: the path to fake file.
233
234        Returns:
235            (int, float) the access time of the fake file in number of seconds
236                since the epoch.
237
238        Raises:
239            OSError: if the file does not exist.
240        """
241        try:
242            file_obj = self.filesystem.resolve(path)
243        except OSError:
244            self.filesystem.raise_os_error(errno.ENOENT)
245        return file_obj.st_atime
246
247    def getctime(self, path: AnyStr) -> float:
248        """Returns the creation time of the fake file.
249
250        Args:
251            path: the path to fake file.
252
253        Returns:
254            (int, float) the creation time of the fake file in number of
255                seconds since the epoch.
256
257        Raises:
258            OSError: if the file does not exist.
259        """
260        try:
261            file_obj = self.filesystem.resolve(path)
262        except OSError:
263            self.filesystem.raise_os_error(errno.ENOENT)
264        return file_obj.st_ctime
265
266    def abspath(self, path: AnyStr) -> AnyStr:
267        """Return the absolute version of a path."""
268
269        def getcwd():
270            """Return the current working directory."""
271            # pylint: disable=undefined-variable
272            if isinstance(path, bytes):
273                return self.os.getcwdb()
274            else:
275                return self.os.getcwd()
276
277        path = make_string_path(path)
278        if not self.isabs(path):
279            path = self.join(getcwd(), path)
280        elif self.filesystem.is_windows_fs and self.filesystem.starts_with_sep(path):
281            cwd = getcwd()
282            if self.filesystem.starts_with_drive_letter(cwd):
283                path = self.join(cwd[:2], path)
284        return self.normpath(path)
285
286    def join(self, *p: AnyStr) -> AnyStr:
287        """Return the completed path with a separator of the parts."""
288        return self.filesystem.joinpaths(*p)
289
290    def split(self, path: AnyStr) -> Tuple[AnyStr, AnyStr]:
291        """Split the path into the directory and the filename of the path."""
292        return self.filesystem.splitpath(path)
293
294    def splitdrive(self, path: AnyStr) -> Tuple[AnyStr, AnyStr]:
295        """Split the path into the drive part and the rest of the path, if
296        supported."""
297        return self.filesystem.splitdrive(path)
298
299    def normpath(self, path: AnyStr) -> AnyStr:
300        """Normalize path, eliminating double slashes, etc."""
301        return self.filesystem.normpath(path)
302
303    def normcase(self, path: AnyStr) -> AnyStr:
304        """Convert to lower case under windows, replaces additional path
305        separator."""
306        path = self.filesystem.normcase(path)
307        if self.filesystem.is_windows_fs:
308            path = path.lower()
309        return path
310
311    def relpath(self, path: AnyStr, start: Optional[AnyStr] = None) -> AnyStr:
312        """We mostly rely on the native implementation and adapt the
313        path separator."""
314        if not path:
315            raise ValueError("no path specified")
316        path = make_string_path(path)
317        path = self.filesystem.replace_windows_root(path)
318        sep = matching_string(path, self.filesystem.path_separator)
319        if start is not None:
320            start = make_string_path(start)
321        else:
322            start = matching_string(path, self.filesystem.cwd)
323        start = self.filesystem.replace_windows_root(start)
324        system_sep = matching_string(path, self._os_path.sep)
325        if self.filesystem.alternative_path_separator is not None:
326            altsep = matching_string(path, self.filesystem.alternative_path_separator)
327            path = path.replace(altsep, system_sep)
328            start = start.replace(altsep, system_sep)
329        path = path.replace(sep, system_sep)
330        start = start.replace(sep, system_sep)
331        path = self._os_path.relpath(path, start)
332        return path.replace(system_sep, sep)
333
334    def realpath(self, filename: AnyStr, strict: Optional[bool] = None) -> AnyStr:
335        """Return the canonical path of the specified filename, eliminating any
336        symbolic links encountered in the path.
337        """
338        if strict is not None and sys.version_info < (3, 10):
339            raise TypeError("realpath() got an unexpected " "keyword argument 'strict'")
340        if strict:
341            # raises in strict mode if the file does not exist
342            self.filesystem.resolve(filename)
343        if self.filesystem.is_windows_fs:
344            return self.abspath(filename)
345        filename = make_string_path(filename)
346        path, ok = self._join_real_path(filename[:0], filename, {})
347        path = self.abspath(path)
348        return path
349
350    def samefile(self, path1: AnyStr, path2: AnyStr) -> bool:
351        """Return whether path1 and path2 point to the same file.
352
353        Args:
354            path1: first file path or path object (Python >=3.6)
355            path2: second file path or path object (Python >=3.6)
356
357        Raises:
358            OSError: if one of the paths does not point to an existing
359                file system object.
360        """
361        stat1 = self.filesystem.stat(path1)
362        stat2 = self.filesystem.stat(path2)
363        return stat1.st_ino == stat2.st_ino and stat1.st_dev == stat2.st_dev
364
365    @overload
366    def _join_real_path(
367        self, path: str, rest: str, seen: Dict[str, Optional[str]]
368    ) -> Tuple[str, bool]:
369        ...
370
371    @overload
372    def _join_real_path(
373        self, path: bytes, rest: bytes, seen: Dict[bytes, Optional[bytes]]
374    ) -> Tuple[bytes, bool]:
375        ...
376
377    def _join_real_path(
378        self, path: AnyStr, rest: AnyStr, seen: Dict[AnyStr, Optional[AnyStr]]
379    ) -> Tuple[AnyStr, bool]:
380        """Join two paths, normalizing and eliminating any symbolic links
381        encountered in the second path.
382        Taken from Python source and adapted.
383        """
384        curdir = matching_string(path, ".")
385        pardir = matching_string(path, "..")
386
387        sep = self.filesystem.get_path_separator(path)
388        if self.isabs(rest):
389            rest = rest[1:]
390            path = sep
391
392        while rest:
393            name, _, rest = rest.partition(sep)
394            if not name or name == curdir:
395                # current dir
396                continue
397            if name == pardir:
398                # parent dir
399                if path:
400                    path, name = self.filesystem.splitpath(path)
401                    if name == pardir:
402                        path = self.filesystem.joinpaths(path, pardir, pardir)
403                else:
404                    path = pardir
405                continue
406            newpath = self.filesystem.joinpaths(path, name)
407            if not self.filesystem.islink(newpath):
408                path = newpath
409                continue
410            # Resolve the symbolic link
411            if newpath in seen:
412                # Already seen this path
413                seen_path = seen[newpath]
414                if seen_path is not None:
415                    # use cached value
416                    path = seen_path
417                    continue
418                # The symlink is not resolved, so we must have a symlink loop.
419                # Return already resolved part + rest of the path unchanged.
420                return self.filesystem.joinpaths(newpath, rest), False
421            seen[newpath] = None  # not resolved symlink
422            path, ok = self._join_real_path(
423                path,
424                matching_string(path, self.filesystem.readlink(newpath)),
425                seen,
426            )
427            if not ok:
428                return self.filesystem.joinpaths(path, rest), False
429            seen[newpath] = path  # resolved symlink
430        return path, True
431
432    def dirname(self, path: AnyStr) -> AnyStr:
433        """Returns the first part of the result of `split()`."""
434        return self.split(path)[0]
435
436    def expanduser(self, path: AnyStr) -> AnyStr:
437        """Return the argument with an initial component of ~ or ~user
438        replaced by that user's home directory.
439        """
440        path = self._os_path.expanduser(path)
441        return path.replace(
442            matching_string(path, self._os_path.sep),
443            matching_string(path, self.sep),
444        )
445
446    def ismount(self, path: AnyStr) -> bool:
447        """Return true if the given path is a mount point.
448
449        Args:
450            path: Path to filesystem object to be checked
451
452        Returns:
453            `True` if path is a mount point added to the fake file system.
454            Under Windows also returns True for drive and UNC roots
455            (independent of their existence).
456        """
457        if not path:
458            return False
459        path_str = to_string(make_string_path(path))
460        normed_path = self.filesystem.absnormpath(path_str)
461        sep = self.filesystem.path_separator
462        if self.filesystem.is_windows_fs:
463            path_seps: Union[Tuple[str, Optional[str]], Tuple[str]]
464            if self.filesystem.alternative_path_separator is not None:
465                path_seps = (sep, self.filesystem.alternative_path_separator)
466            else:
467                path_seps = (sep,)
468            drive, rest = self.filesystem.splitdrive(normed_path)
469            if drive and drive[:1] in path_seps:
470                return (not rest) or (rest in path_seps)
471            if rest in path_seps:
472                return True
473        for mount_point in self.filesystem.mount_points:
474            if to_string(normed_path).rstrip(sep) == to_string(mount_point).rstrip(sep):
475                return True
476        return False
477
478    def __getattr__(self, name: str) -> Any:
479        """Forwards any non-faked calls to the real os.path."""
480        return getattr(self._os_path, name)
481
482
483if sys.platform == "win32":
484
485    class FakeNtModule:
486        """Under windows, a few function of `os.path` are taken from the `nt` module
487        for performance reasons. These are patched here.
488        """
489
490        @staticmethod
491        def dir():
492            if sys.version_info >= (3, 12):
493                return ["_path_exists", "_path_isfile", "_path_isdir", "_path_islink"]
494            else:
495                return ["_isdir"]
496
497        def __init__(self, filesystem: "FakeFilesystem"):
498            """Init.
499
500            Args:
501                filesystem: FakeFilesystem used to provide file system information
502            """
503            import nt  # type:ignore[import]
504
505            self.filesystem = filesystem
506            self.nt_module: Any = nt
507
508        if sys.version_info >= (3, 12):
509
510            def _path_isdir(self, path: AnyStr) -> bool:
511                return self.filesystem.isdir(path)
512
513            def _path_isfile(self, path: AnyStr) -> bool:
514                return self.filesystem.isfile(path)
515
516            def _path_islink(self, path: AnyStr) -> bool:
517                return self.filesystem.islink(path)
518
519            def _path_exists(self, path: AnyStr) -> bool:
520                return self.filesystem.exists(path)
521
522        else:
523
524            def _isdir(self, path: AnyStr) -> bool:
525                return self.filesystem.isdir(path)
526
527        def __getattr__(self, name: str) -> Any:
528            """Forwards any non-faked calls to the real nt module."""
529            return getattr(self.nt_module, name)
530