Skip to content

Commit b7fedd3

Browse files
committed
Fix permission handling for opening file via file descriptor
- fixes #967
1 parent c3a1cc8 commit b7fedd3

8 files changed

+167
-59
lines changed

CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ The released versions correspond to PyPI releases.
2929
* fixed handling of missing attribute in `os.getxattr` (see [#971](../../issues/971))
3030
* fixed permission problem with `shutil.rmtree` if emulating Windows under POSIX
3131
(see [#979](../../issues/979))
32+
* fixed handling of errors on opening files via file descriptor (see [#967](../../issues/967))
3233

3334
### Infrastructure
3435
* replace `undefined` by own minimal implementation to avoid importing it

pyfakefs/fake_file.py

+32-7
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
AnyPath,
5454
AnyString,
5555
get_locale_encoding,
56+
_OpenModes,
5657
)
5758

5859
if TYPE_CHECKING:
@@ -134,6 +135,7 @@ def __init__(
134135
encoding: Optional[str] = None,
135136
errors: Optional[str] = None,
136137
side_effect: Optional[Callable[["FakeFile"], None]] = None,
138+
open_modes: Optional[_OpenModes] = None,
137139
):
138140
"""
139141
Args:
@@ -152,6 +154,7 @@ def __init__(
152154
errors: The error mode used for encoding/decoding errors.
153155
side_effect: function handle that is executed when file is written,
154156
must accept the file object as an argument.
157+
open_modes: The modes the file was opened with (e.g. can read, write etc.)
155158
"""
156159
# to be backwards compatible regarding argument order, we raise on None
157160
if filesystem is None:
@@ -180,6 +183,7 @@ def __init__(
180183
# Linux specific: extended file system attributes
181184
self.xattr: Dict = {}
182185
self.opened_as: AnyString = ""
186+
self.open_modes = open_modes
183187

184188
@property
185189
def byte_contents(self) -> Optional[bytes]:
@@ -755,6 +759,7 @@ def __init__(
755759
errors: Optional[str],
756760
buffering: int,
757761
raw_io: bool,
762+
opened_as_fd: bool,
758763
is_stream: bool = False,
759764
):
760765
self.file_object = file_object
@@ -766,6 +771,7 @@ def __init__(
766771
self._file_epoch = file_object.epoch
767772
self.raw_io = raw_io
768773
self._binary = binary
774+
self.opened_as_fd = opened_as_fd
769775
self.is_stream = is_stream
770776
self._changed = False
771777
self._buffer_size = buffering
@@ -844,8 +850,17 @@ def close(self) -> None:
844850
return
845851

846852
# for raw io, all writes are flushed immediately
847-
if self.allow_update and not self.raw_io:
848-
self.flush()
853+
if not self.raw_io:
854+
try:
855+
self.flush()
856+
except OSError as e:
857+
if e.errno == errno.EBADF:
858+
# if we get here, we have an open file descriptor
859+
# without write permission, which has to be closed
860+
assert self.filedes
861+
self._filesystem._close_open_file(self.filedes)
862+
raise
863+
849864
if self._filesystem.is_windows_fs and self._changed:
850865
self.file_object.st_mtime = helpers.now()
851866

@@ -880,8 +895,12 @@ def _try_flush(self, old_pos: int) -> None:
880895

881896
def flush(self) -> None:
882897
"""Flush file contents to 'disk'."""
898+
if self.is_stream:
899+
return
900+
883901
self._check_open_file()
884-
if self.allow_update and not self.is_stream:
902+
903+
if self.allow_update:
885904
contents = self._io.getvalue()
886905
if self._append:
887906
self._sync_io()
@@ -901,9 +920,15 @@ def flush(self) -> None:
901920
self.file_object.st_ctime = current_time
902921
self.file_object.st_mtime = current_time
903922
self._file_epoch = self.file_object.epoch
904-
905-
if not self.is_stream:
906-
self._flush_related_files()
923+
self._flush_related_files()
924+
else:
925+
buf_length = len(self._io.getvalue())
926+
content_length = 0
927+
if self.file_object.byte_contents is not None:
928+
content_length = len(self.file_object.byte_contents)
929+
# an error is only raised if there is something to flush
930+
if content_length != buf_length:
931+
self._filesystem.raise_os_error(errno.EBADF)
907932

908933
def update_flush_pos(self) -> None:
909934
self._flush_pos = self._io.tell()
@@ -1146,7 +1171,7 @@ def __getattr__(self, name: str) -> Any:
11461171
self._check_open_file()
11471172
if not self._read and reading:
11481173
return self._read_error()
1149-
if not self.allow_update and writing:
1174+
if not self.opened_as_fd and not self.allow_update and writing:
11501175
return self._write_error()
11511176

11521177
if reading:

pyfakefs/fake_filesystem.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ def _handle_utime_arg_errors(
814814
if ns is not None and len(ns) != 2:
815815
raise TypeError("utime: 'ns' must be a tuple of two ints")
816816

817-
def _add_open_file(self, file_obj: AnyFileWrapper) -> int:
817+
def add_open_file(self, file_obj: AnyFileWrapper) -> int:
818818
"""Add file_obj to the list of open files on the filesystem.
819819
Used internally to manage open files.
820820
@@ -860,13 +860,29 @@ def get_open_file(self, file_des: int) -> AnyFileWrapper:
860860
Returns:
861861
Open file object.
862862
"""
863+
try:
864+
return self.get_open_files(file_des)[0]
865+
except IndexError:
866+
self.raise_os_error(errno.EBADF, str(file_des))
867+
868+
def get_open_files(self, file_des: int) -> List[AnyFileWrapper]:
869+
"""Return the list of open files for a file descriptor.
870+
871+
Args:
872+
file_des: File descriptor of the open files.
873+
874+
Raises:
875+
OSError: an invalid file descriptor.
876+
TypeError: filedes is not an integer.
877+
878+
Returns:
879+
List of open file objects.
880+
"""
863881
if not is_int_type(file_des):
864882
raise TypeError("an integer is required")
865883
valid = file_des < len(self.open_files)
866884
if valid:
867-
file_list = self.open_files[file_des]
868-
if file_list is not None:
869-
return file_list[0]
885+
return self.open_files[file_des] or []
870886
self.raise_os_error(errno.EBADF, str(file_des))
871887

872888
def has_open_file(self, file_object: FakeFile) -> bool:
@@ -3008,9 +3024,9 @@ def __str__(self) -> str:
30083024
return str(self.root_dir)
30093025

30103026
def _add_standard_streams(self) -> None:
3011-
self._add_open_file(StandardStreamWrapper(sys.stdin))
3012-
self._add_open_file(StandardStreamWrapper(sys.stdout))
3013-
self._add_open_file(StandardStreamWrapper(sys.stderr))
3027+
self.add_open_file(StandardStreamWrapper(sys.stdin))
3028+
self.add_open_file(StandardStreamWrapper(sys.stdout))
3029+
self.add_open_file(StandardStreamWrapper(sys.stderr))
30143030

30153031
def _tempdir_name(self):
30163032
"""This logic is extracted from tempdir._candidate_tempdir_list.

pyfakefs/fake_open.py

+41-17
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import errno
1818
import os
1919
import sys
20-
from collections import namedtuple
2120
from stat import (
2221
S_ISDIR,
2322
)
@@ -43,17 +42,13 @@
4342
is_root,
4443
PERM_READ,
4544
PERM_WRITE,
45+
_OpenModes,
4646
)
4747

4848
if TYPE_CHECKING:
4949
from pyfakefs.fake_filesystem import FakeFilesystem
5050

5151

52-
_OpenModes = namedtuple(
53-
"_OpenModes",
54-
"must_exist can_read can_write truncate append must_not_exist",
55-
)
56-
5752
_OPEN_MODE_MAP = {
5853
# mode name:(file must exist, can read, can write,
5954
# truncate, append, must not exist)
@@ -148,6 +143,13 @@ def call(
148143
raise ValueError("binary mode doesn't take an encoding argument")
149144

150145
newline, open_modes = self._handle_file_mode(mode, newline, open_modes)
146+
opened_as_fd = isinstance(file_, int)
147+
if opened_as_fd and not helpers.IS_PYPY:
148+
fd: int = cast(int, file_)
149+
# cannot open the same file descriptor twice, except in PyPy
150+
for f in self.filesystem.get_open_files(fd):
151+
if isinstance(f, FakeFileWrapper) and f.opened_as_fd:
152+
raise OSError(errno.EBADF, "Bad file descriptor")
151153

152154
# the pathlib opener is defined in a Path instance that may not be
153155
# patched under some circumstances; as it just calls standard open(),
@@ -157,7 +159,9 @@ def call(
157159
# here as if directly passed
158160
file_ = opener(file_, self._open_flags_from_open_modes(open_modes))
159161

160-
file_object, file_path, filedes, real_path = self._handle_file_arg(file_)
162+
file_object, file_path, filedes, real_path, can_write = self._handle_file_arg(
163+
file_
164+
)
161165
if file_object is None and file_path is None:
162166
# file must be a fake pipe wrapper, find it...
163167
if (
@@ -176,7 +180,7 @@ def call(
176180
existing_wrapper.can_write,
177181
mode,
178182
)
179-
file_des = self.filesystem._add_open_file(wrapper)
183+
file_des = self.filesystem.add_open_file(wrapper)
180184
wrapper.filedes = file_des
181185
return wrapper
182186

@@ -197,7 +201,11 @@ def call(
197201

198202
assert real_path is not None
199203
file_object = self._init_file_object(
200-
file_object, file_path, open_modes, real_path
204+
file_object,
205+
file_path,
206+
open_modes,
207+
real_path,
208+
check_file_permission=not opened_as_fd,
201209
)
202210

203211
if S_ISDIR(file_object.st_mode):
@@ -218,7 +226,7 @@ def call(
218226
fakefile = FakeFileWrapper(
219227
file_object,
220228
file_path,
221-
update=open_modes.can_write,
229+
update=open_modes.can_write and can_write,
222230
read=open_modes.can_read,
223231
append=open_modes.append,
224232
delete_on_close=self._delete_on_close,
@@ -230,6 +238,7 @@ def call(
230238
errors=errors,
231239
buffering=buffering,
232240
raw_io=self.raw_io,
241+
opened_as_fd=opened_as_fd,
233242
)
234243
if filedes is not None:
235244
fakefile.filedes = filedes
@@ -238,7 +247,7 @@ def call(
238247
assert open_files_list is not None
239248
open_files_list.append(fakefile)
240249
else:
241-
fakefile.filedes = self.filesystem._add_open_file(fakefile)
250+
fakefile.filedes = self.filesystem.add_open_file(fakefile)
242251
return fakefile
243252

244253
@staticmethod
@@ -267,16 +276,25 @@ def _init_file_object(
267276
file_path: AnyStr,
268277
open_modes: _OpenModes,
269278
real_path: AnyString,
279+
check_file_permission: bool,
270280
) -> FakeFile:
271281
if file_object:
272-
if not is_root() and (
273-
(open_modes.can_read and not file_object.has_permission(PERM_READ))
274-
or (open_modes.can_write and not file_object.has_permission(PERM_WRITE))
282+
if (
283+
check_file_permission
284+
and not is_root()
285+
and (
286+
(open_modes.can_read and not file_object.has_permission(PERM_READ))
287+
or (
288+
open_modes.can_write
289+
and not file_object.has_permission(PERM_WRITE)
290+
)
291+
)
275292
):
276293
self.filesystem.raise_os_error(errno.EACCES, file_path)
277294
if open_modes.can_write:
278295
if open_modes.truncate:
279296
file_object.set_contents("")
297+
file_object
280298
else:
281299
if open_modes.must_exist:
282300
self.filesystem.raise_os_error(errno.ENOENT, file_path)
@@ -304,16 +322,21 @@ def _init_file_object(
304322

305323
def _handle_file_arg(
306324
self, file_: Union[AnyStr, int]
307-
) -> Tuple[Optional[FakeFile], Optional[AnyStr], Optional[int], Optional[AnyStr]]:
325+
) -> Tuple[
326+
Optional[FakeFile], Optional[AnyStr], Optional[int], Optional[AnyStr], bool
327+
]:
308328
file_object = None
309329
if isinstance(file_, int):
310330
# opening a file descriptor
311331
filedes: int = file_
312332
wrapper = self.filesystem.get_open_file(filedes)
333+
can_write = True
313334
if isinstance(wrapper, FakePipeWrapper):
314-
return None, None, filedes, None
335+
return None, None, filedes, None, can_write
315336
if isinstance(wrapper, FakeFileWrapper):
316337
self._delete_on_close = wrapper.delete_on_close
338+
can_write = wrapper.allow_update
339+
317340
file_object = cast(
318341
FakeFile, self.filesystem.get_open_file(filedes).get_object()
319342
)
@@ -324,6 +347,7 @@ def _handle_file_arg(
324347
cast(AnyStr, path), # pytype: disable=invalid-annotation
325348
filedes,
326349
cast(AnyStr, path), # pytype: disable=invalid-annotation
350+
can_write,
327351
)
328352

329353
# open a file file by path
@@ -337,7 +361,7 @@ def _handle_file_arg(
337361
file_object = self.filesystem.get_object_from_normpath(
338362
real_path, check_read_perm=False
339363
)
340-
return file_object, file_path, None, real_path
364+
return file_object, file_path, None, real_path, True
341365

342366
def _handle_file_mode(
343367
self,

pyfakefs/fake_os.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def open(
291291
) or open_modes.can_write:
292292
self.filesystem.raise_os_error(errno.EISDIR, path)
293293
dir_wrapper = FakeDirWrapper(obj, path, self.filesystem)
294-
file_des = self.filesystem._add_open_file(dir_wrapper)
294+
file_des = self.filesystem.add_open_file(dir_wrapper)
295295
dir_wrapper.filedes = file_des
296296
return file_des
297297

@@ -373,10 +373,10 @@ def write(self, fd: int, contents: bytes) -> int:
373373
def pipe(self) -> Tuple[int, int]:
374374
read_fd, write_fd = os.pipe()
375375
read_wrapper = FakePipeWrapper(self.filesystem, read_fd, False)
376-
file_des = self.filesystem._add_open_file(read_wrapper)
376+
file_des = self.filesystem.add_open_file(read_wrapper)
377377
read_wrapper.filedes = file_des
378378
write_wrapper = FakePipeWrapper(self.filesystem, write_fd, True)
379-
file_des = self.filesystem._add_open_file(write_wrapper)
379+
file_des = self.filesystem.add_open_file(write_wrapper)
380380
write_wrapper.filedes = file_des
381381
return read_wrapper.filedes, write_wrapper.filedes
382382

pyfakefs/helpers.py

+6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import stat
2020
import sys
2121
import time
22+
from collections import namedtuple
2223
from copy import copy
2324
from stat import S_IFLNK
2425
from typing import Union, Optional, Any, AnyStr, overload, cast
@@ -37,6 +38,11 @@
3738
PERM_DEF_FILE = 0o666 # Default permission bits (regular file)
3839
PERM_ALL = 0o7777 # All permission bits.
3940

41+
_OpenModes = namedtuple(
42+
"_OpenModes",
43+
"must_exist can_read can_write truncate append must_not_exist",
44+
)
45+
4046
if sys.platform == "win32":
4147
USER_ID = 1
4248
GROUP_ID = 1

pyfakefs/tests/fake_open_test.py

+8
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,14 @@ def test_use_opener_with_read(self):
959959
with self.assertRaises(OSError):
960960
f.write("foo")
961961

962+
def test_no_opener_with_read(self):
963+
file_path = self.make_path("foo")
964+
self.create_file(file_path, contents="test")
965+
with self.open(file_path) as f:
966+
assert f.read() == "test"
967+
with self.assertRaises(OSError):
968+
f.write("foo")
969+
962970
def test_use_opener_with_read_plus(self):
963971
file_path = self.make_path("foo")
964972
self.create_file(file_path, contents="test")

0 commit comments

Comments
 (0)