@@ -674,14 +674,15 @@ def stat(self, entry_path: AnyStr, follow_symlinks: bool = True):
674
674
follow_symlinks ,
675
675
allow_fd = True ,
676
676
check_read_perm = False ,
677
+ check_exe_perm = False ,
677
678
)
678
679
except TypeError :
679
680
file_object = self .resolve (entry_path )
680
681
if not is_root ():
681
682
# make sure stat raises if a parent dir is not readable
682
683
parent_dir = file_object .parent_dir
683
684
if parent_dir :
684
- self .get_object (parent_dir .path ) # type: ignore[arg-type]
685
+ self .get_object (parent_dir .path , check_read_perm = False ) # type: ignore[arg-type]
685
686
686
687
self .raise_for_filepath_ending_with_separator (
687
688
entry_path , file_object , follow_symlinks
@@ -1597,6 +1598,7 @@ def get_object_from_normpath(
1597
1598
self ,
1598
1599
file_path : AnyPath ,
1599
1600
check_read_perm : bool = True ,
1601
+ check_exe_perm : bool = True ,
1600
1602
check_owner : bool = False ,
1601
1603
) -> AnyFile :
1602
1604
"""Search for the specified filesystem object within the fake
@@ -1607,6 +1609,8 @@ def get_object_from_normpath(
1607
1609
path that has already been normalized/resolved.
1608
1610
check_read_perm: If True, raises OSError if a parent directory
1609
1611
does not have read permission
1612
+ check_exe_perm: If True, raises OSError if a parent directory
1613
+ does not have execute (e.g. search) permission
1610
1614
check_owner: If True, and check_read_perm is also True,
1611
1615
only checks read permission if the current user id is
1612
1616
different from the file object user id
@@ -1638,24 +1642,27 @@ def get_object_from_normpath(
1638
1642
target = target .get_entry (component ) # type: ignore
1639
1643
if (
1640
1644
not is_root ()
1641
- and check_read_perm
1645
+ and ( check_read_perm or check_exe_perm )
1642
1646
and target
1643
- and not self ._can_read (target , check_owner )
1647
+ and not self ._can_read (
1648
+ target , check_read_perm , check_exe_perm , check_owner
1649
+ )
1644
1650
):
1645
1651
self .raise_os_error (errno .EACCES , target .path )
1646
1652
except KeyError :
1647
1653
self .raise_os_error (errno .ENOENT , path )
1648
1654
return target
1649
1655
1650
1656
@staticmethod
1651
- def _can_read (target , owner_can_read ):
1652
- if target .st_uid == helpers .get_uid ():
1653
- if owner_can_read or target .st_mode & 0o400 :
1654
- return True
1655
- if target .st_gid == get_gid ():
1656
- if target .st_mode & 0o040 :
1657
- return True
1658
- return target .st_mode & 0o004
1657
+ def _can_read (target , check_read_perm , check_exe_perm , owner_can_read ):
1658
+ if owner_can_read and target .st_uid == helpers .get_uid ():
1659
+ return True
1660
+ permission = helpers .PERM_READ if check_read_perm else 0
1661
+ if S_ISDIR (target .st_mode ) and check_exe_perm :
1662
+ permission |= helpers .PERM_EXE
1663
+ if not permission :
1664
+ return True
1665
+ return target .has_permission (permission )
1659
1666
1660
1667
def get_object (self , file_path : AnyPath , check_read_perm : bool = True ) -> FakeFile :
1661
1668
"""Search for the specified filesystem object within the fake
@@ -1684,6 +1691,7 @@ def resolve(
1684
1691
follow_symlinks : bool = True ,
1685
1692
allow_fd : bool = False ,
1686
1693
check_read_perm : bool = True ,
1694
+ check_exe_perm : bool = True ,
1687
1695
check_owner : bool = False ,
1688
1696
) -> FakeFile :
1689
1697
"""Search for the specified filesystem object, resolving all links.
@@ -1695,6 +1703,8 @@ def resolve(
1695
1703
allow_fd: If `True`, `file_path` may be an open file descriptor
1696
1704
check_read_perm: If True, raises OSError if a parent directory
1697
1705
does not have read permission
1706
+ check_read_perm: If True, raises OSError if a parent directory
1707
+ does not have execute permission
1698
1708
check_owner: If True, and check_read_perm is also True,
1699
1709
only checks read permission if the current user id is
1700
1710
different from the file object user id
@@ -1714,6 +1724,7 @@ def resolve(
1714
1724
return self .get_object_from_normpath (
1715
1725
self .resolve_path (file_path , allow_fd ),
1716
1726
check_read_perm ,
1727
+ check_exe_perm ,
1717
1728
check_owner ,
1718
1729
)
1719
1730
return self .lresolve (file_path )
@@ -1756,7 +1767,7 @@ def lresolve(self, path: AnyPath) -> FakeFile:
1756
1767
if not self .is_windows_fs and isinstance (parent_obj , FakeFile ):
1757
1768
self .raise_os_error (errno .ENOTDIR , path_str )
1758
1769
self .raise_os_error (errno .ENOENT , path_str )
1759
- if not parent_obj .st_mode & helpers .PERM_READ :
1770
+ if not parent_obj .has_permission ( helpers .PERM_READ ) :
1760
1771
self .raise_os_error (errno .EACCES , parent_directory )
1761
1772
return (
1762
1773
parent_obj .get_entry (to_string (child_name ))
@@ -1781,7 +1792,10 @@ def add_object(self, file_path: AnyStr, file_object: AnyFile) -> None:
1781
1792
if not file_path :
1782
1793
target_directory = self .root_dir
1783
1794
else :
1784
- target_directory = cast (FakeDirectory , self .resolve (file_path ))
1795
+ target_directory = cast (
1796
+ FakeDirectory ,
1797
+ self .resolve (file_path , check_read_perm = False , check_exe_perm = True ),
1798
+ )
1785
1799
if not S_ISDIR (target_directory .st_mode ):
1786
1800
error = errno .ENOENT if self .is_windows_fs else errno .ENOTDIR
1787
1801
self .raise_os_error (error , file_path )
@@ -2859,14 +2873,22 @@ def isjunction(self, path: AnyPath) -> bool:
2859
2873
return False
2860
2874
2861
2875
def confirmdir (
2862
- self , target_directory : AnyStr , check_owner : bool = False
2876
+ self ,
2877
+ target_directory : AnyStr ,
2878
+ check_read_perm : bool = True ,
2879
+ check_exe_perm : bool = True ,
2880
+ check_owner : bool = False ,
2863
2881
) -> FakeDirectory :
2864
2882
"""Test that the target is actually a directory, raising OSError
2865
2883
if not.
2866
2884
2867
2885
Args:
2868
2886
target_directory: Path to the target directory within the fake
2869
2887
filesystem.
2888
+ check_read_perm: If True, raises OSError if the directory
2889
+ does not have read permission
2890
+ check_exe_perm: If True, raises OSError if the directory
2891
+ does not have execute (e.g. search) permission
2870
2892
check_owner: If True, only checks read permission if the current
2871
2893
user id is different from the file object user id
2872
2894
@@ -2878,7 +2900,12 @@ def confirmdir(
2878
2900
"""
2879
2901
directory = cast (
2880
2902
FakeDirectory ,
2881
- self .resolve (target_directory , check_owner = check_owner ),
2903
+ self .resolve (
2904
+ target_directory ,
2905
+ check_read_perm = check_read_perm ,
2906
+ check_exe_perm = check_exe_perm ,
2907
+ check_owner = check_owner ,
2908
+ ),
2882
2909
)
2883
2910
if not directory .st_mode & S_IFDIR :
2884
2911
self .raise_os_error (errno .ENOTDIR , target_directory , 267 )
@@ -2972,7 +2999,7 @@ def listdir(self, target_directory: AnyStr) -> List[AnyStr]:
2972
2999
OSError: if the target is not a directory.
2973
3000
"""
2974
3001
target_directory = self .resolve_path (target_directory , allow_fd = True )
2975
- directory = self .confirmdir (target_directory )
3002
+ directory = self .confirmdir (target_directory , check_exe_perm = False )
2976
3003
directory_contents = list (directory .entries .keys ())
2977
3004
if self .shuffle_listdir_results :
2978
3005
random .shuffle (directory_contents )
0 commit comments