Skip to main content

rustpython_vm/stdlib/
_io.rs

1/*
2 * I/O core tools.
3 */
4pub(crate) use _io::module_def;
5
6#[cfg(all(unix, feature = "threading", feature = "host_env"))]
7pub(crate) use _io::reinit_std_streams_after_fork;
8
9cfg_select! {
10    any(not(target_arch = "wasm32"), target_os = "wasi") => {
11        use rustpython_host_env::crt_fd::Offset;
12        // EAGAIN constant for BlockingIOError
13        const EAGAIN: i32 = libc::EAGAIN;
14    }
15    _ => {
16        type Offset = i64;
17        // EAGAIN constant for BlockingIOError
18        const EAGAIN: i32 = rustpython_host_env::errno::errors::EAGAIN;
19    }
20}
21
22use crate::{
23    AsObject, PyObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine,
24    builtins::{PyModule, PyOSError},
25};
26pub use _io::{OpenArgs, io_open as open};
27use rustpython_host_env::io as host_io;
28
29fn file_closed(file: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
30    file.get_attr("closed", vm)?.try_to_bool(vm)
31}
32
33const DEFAULT_BUFFER_SIZE: usize = 128 * 1024;
34
35/// iobase_finalize in Modules/_io/iobase.c
36fn iobase_finalize(zelf: &PyObject, vm: &VirtualMachine) {
37    // If `closed` doesn't exist or can't be evaluated as bool, then the
38    // object is probably in an unusable state, so ignore.
39    let closed = match vm.get_attribute_opt(zelf, "closed") {
40        Ok(Some(val)) => match val.try_to_bool(vm) {
41            Ok(b) => b,
42            Err(_) => return,
43        },
44        _ => return,
45    };
46    if !closed {
47        // Signal close() that it was called as part of the object
48        // finalization process.
49        let _ = zelf.set_attr("_finalizing", vm.ctx.true_value.clone(), vm);
50        if let Err(e) = vm.call_method(zelf, "close", ()) {
51            // BrokenPipeError during GC finalization is expected when pipe
52            // buffer objects are collected after the subprocess dies. The
53            // underlying fd is still properly closed by raw.close().
54            // Popen.__del__ catches BrokenPipeError, but our tracing GC may
55            // finalize pipe buffers before Popen.__del__ runs.
56            if !e.fast_isinstance(vm.ctx.exceptions.broken_pipe_error) {
57                vm.run_unraisable(e, None, zelf.to_owned());
58            }
59        }
60    }
61}
62
63// not used on all platforms
64#[derive(Copy, Clone)]
65#[repr(transparent)]
66pub struct Fildes(pub i32);
67
68impl TryFromObject for Fildes {
69    fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
70        use crate::builtins::int;
71        let int = match obj.downcast::<int::PyInt>() {
72            Ok(i) => i,
73            Err(obj) => {
74                let fileno_meth = vm.get_attribute_opt(&obj, "fileno")?.ok_or_else(|| {
75                    vm.new_type_error("argument must be an int, or have a fileno() method.")
76                })?;
77                fileno_meth
78                    .call((), vm)?
79                    .downcast()
80                    .map_err(|_| vm.new_type_error("fileno() returned a non-integer"))?
81            }
82        };
83        let fd = int.try_to_primitive(vm)?;
84        if fd < 0 {
85            return Err(vm.new_value_error(format!(
86                "file descriptor cannot be a negative integer ({fd})"
87            )));
88        }
89        Ok(Self(fd))
90    }
91}
92
93impl From<Fildes> for i32 {
94    fn from(fildes: Fildes) -> Self {
95        fildes.0
96    }
97}
98
99#[cfg(unix)]
100impl std::os::fd::AsFd for Fildes {
101    fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
102        // SAFETY: none, really. but, python's os api of passing around file descriptors
103        //         everywhere isn't really io-safe anyway, so, this is passed to the user.
104        unsafe { std::os::fd::BorrowedFd::borrow_raw(self.0) }
105    }
106}
107
108#[cfg(unix)]
109impl std::os::fd::AsRawFd for Fildes {
110    fn as_raw_fd(&self) -> std::os::fd::RawFd {
111        self.0
112    }
113}
114
115#[pymodule]
116mod _io {
117    use super::*;
118    use crate::{
119        AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
120        TryFromBorrowedObject, TryFromObject,
121        builtins::{
122            PyBaseExceptionRef, PyBool, PyByteArray, PyBytes, PyBytesRef, PyDict, PyMemoryView,
123            PyStr, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef, PyUtf8Str, PyUtf8StrRef,
124        },
125        class::{PyClassDef, StaticType},
126        common::lock::{
127            PyMappedThreadMutexGuard, PyRwLock, PyRwLockReadGuard, PyRwLockWriteGuard,
128            PyThreadMutex, PyThreadMutexGuard,
129        },
130        common::wtf8::{Wtf8, Wtf8Buf},
131        convert::ToPyObject,
132        exceptions::nul_char_error,
133        function::{
134            ArgBytesLike, ArgContiguousBytesLike, ArgIterable, ArgMemoryBuffer, Either, FsPath,
135            FuncArgs, IntoFuncArgs, OptionalArg, OptionalOption, PySetterValue, PySsize,
136        },
137        protocol::{
138            BufferDescriptor, BufferMethods, BufferResizeGuard, PyBuffer, PyIterReturn, VecBuffer,
139        },
140        recursion::ReprGuard,
141        types::{
142            Callable, Constructor, DefaultConstructor, Destructor, Initializer, IterNext, Iterable,
143            Representable,
144        },
145        vm::VirtualMachine,
146    };
147    use alloc::borrow::Cow;
148    use bstr::ByteSlice;
149    use core::{
150        hint::cold_path,
151        ops::Range,
152        sync::atomic::{AtomicBool, Ordering},
153    };
154    use crossbeam_utils::atomic::AtomicCell;
155    use malachite_bigint::BigInt;
156    use num_traits::ToPrimitive;
157    use std::io::{self, Cursor, SeekFrom, prelude::*};
158
159    fn validate_whence(whence: i32) -> bool {
160        host_io::validate_whence(whence)
161    }
162
163    fn ensure_unclosed(file: &PyObject, msg: &str, vm: &VirtualMachine) -> PyResult<()> {
164        if file.get_attr("closed", vm)?.try_to_bool(vm)? {
165            Err(vm.new_value_error(msg))
166        } else {
167            Ok(())
168        }
169    }
170
171    /// Check if an error is an OSError with errno == EINTR.
172    /// If so, call check_signals() and return Ok(None) to indicate retry.
173    /// Otherwise, return Ok(Some(val)) for success or Err for other errors.
174    /// This mirrors CPythons _PyIO_trap_eintr() pattern.
175    #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
176    fn trap_eintr<T>(result: PyResult<T>, vm: &VirtualMachine) -> PyResult<Option<T>> {
177        match result {
178            Ok(val) => Ok(Some(val)),
179            Err(exc) => {
180                // Check if its an OSError with errno == EINTR
181                if exc.fast_isinstance(vm.ctx.exceptions.os_error)
182                    && let Ok(errno_attr) = exc.as_object().get_attr("errno", vm)
183                    && let Ok(errno_val) = i32::try_from_object(vm, errno_attr)
184                    && host_io::is_interrupted_errno(errno_val)
185                {
186                    vm.check_signals()?;
187                    return Ok(None);
188                }
189                Err(exc)
190            }
191        }
192    }
193
194    /// WASM version: no EINTR handling needed
195    #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
196    fn trap_eintr<T>(result: PyResult<T>, _vm: &VirtualMachine) -> PyResult<Option<T>> {
197        result.map(Some)
198    }
199
200    pub(super) fn new_unsupported_operation<T: Into<String>>(
201        msg: T,
202        vm: &VirtualMachine,
203    ) -> PyBaseExceptionRef {
204        vm.new_os_subtype_error(unsupported_operation().to_owned(), None, msg.into())
205            .upcast()
206    }
207
208    fn _unsupported<T>(vm: &VirtualMachine, zelf: &PyObject, operation: &str) -> PyResult<T> {
209        Err(new_unsupported_operation(
210            format!("{}.{}() not supported", zelf.class().name(), operation),
211            vm,
212        ))
213    }
214
215    #[derive(Clone, Copy, FromArgs)]
216    pub(super) struct OptionalSize {
217        // Missing reads the rest of the stream. None stays None so -1 and omission match.
218        #[pyarg(positional, default, py_default = "-1")]
219        size: Option<PySsize>,
220    }
221
222    // truncate reports None. Missing still means the current position.
223    #[derive(Clone, Copy, FromArgs)]
224    struct OptionalPos {
225        #[pyarg(positional, optional)]
226        size: Option<PySsize>,
227    }
228
229    #[derive(Clone, Copy, FromArgs)]
230    struct StringIOPos {
231        #[pyarg(positional, optional)]
232        pos: Option<PySsize>,
233    }
234
235    impl StringIOPos {
236        fn as_optional(self) -> OptionalPos {
237            OptionalPos { size: self.pos }
238        }
239    }
240
241    impl OptionalPos {
242        fn try_usize(self, vm: &VirtualMachine) -> PyResult<Option<usize>> {
243            OptionalSize { size: self.size }.try_usize(vm)
244        }
245    }
246
247    // peek ignores the value and returns the buffer.
248    #[derive(Clone, Copy, FromArgs)]
249    #[allow(dead_code)]
250    struct PeekSize {
251        #[pyarg(positional, default)]
252        size: isize,
253    }
254
255    #[derive(FromArgs)]
256    struct WhenceArg {
257        #[pyarg(positional, default)]
258        whence: i32,
259    }
260
261    #[derive(FromArgs)]
262    pub(super) struct HowArg {
263        #[pyarg(positional, default)]
264        pub whence: i32,
265    }
266
267    #[derive(FromArgs)]
268    #[allow(dead_code)]
269    struct IgnoredWhence {
270        #[pyarg(positional, default)]
271        whence: i32,
272    }
273
274    #[derive(FromArgs)]
275    #[allow(dead_code)]
276    struct IgnoredSize {
277        #[pyarg(positional, default = -1)]
278        size: isize,
279    }
280
281    #[derive(FromArgs)]
282    #[allow(dead_code)]
283    struct IgnoredPos {
284        #[pyarg(positional, optional)]
285        size: Option<PyObjectRef>,
286    }
287
288    #[derive(FromArgs)]
289    struct ReadlinesHint {
290        // Explicit None is unlimited, same as omission.
291        #[pyarg(positional, optional, py_default = "-1")]
292        hint: Option<isize>,
293    }
294
295    #[derive(FromArgs)]
296    struct ObjPos {
297        #[pyarg(positional, optional)]
298        pos: Option<PyObjectRef>,
299    }
300
301    #[cfg(feature = "host_env")]
302    #[derive(FromArgs)]
303    pub(super) struct ObjLen {
304        #[pyarg(positional, optional)]
305        pub size: Option<PyObjectRef>,
306    }
307
308    // An explicit None object is preserved; only omission becomes None.
309    #[derive(FromArgs)]
310    struct KeepNonePos {
311        #[pyarg(positional, optional, py_default = "None")]
312        pos: OptionalArg<PyObjectRef>,
313    }
314
315    #[derive(FromArgs)]
316    struct StackLevelArg {
317        #[pyarg(positional, default = 2)]
318        stacklevel: i32,
319    }
320
321    #[cfg(all(feature = "host_env", windows))]
322    #[derive(FromArgs)]
323    pub(super) struct SignedSize {
324        #[pyarg(positional, default = -1)]
325        pub size: isize,
326    }
327
328    impl OptionalSize {
329        pub(super) fn to_usize(self) -> Option<usize> {
330            self.size?.to_usize()
331        }
332
333        pub(super) fn try_usize(self, vm: &VirtualMachine) -> PyResult<Option<usize>> {
334            self.size
335                .map(|v| {
336                    if v >= 0 {
337                        Ok(v as usize)
338                    } else {
339                        Err(vm.new_value_error(format!("Negative size value {v}")))
340                    }
341                })
342                .transpose()
343        }
344    }
345
346    fn os_err(vm: &VirtualMachine, err: io::Error) -> PyBaseExceptionRef {
347        use crate::convert::ToPyException;
348        err.to_pyexception(vm)
349    }
350
351    pub(super) fn io_closed_error(vm: &VirtualMachine) -> PyBaseExceptionRef {
352        vm.new_value_error("I/O operation on closed file")
353    }
354
355    #[pyattr]
356    const DEFAULT_BUFFER_SIZE: usize = super::DEFAULT_BUFFER_SIZE;
357
358    pub(super) fn seekfrom(
359        vm: &VirtualMachine,
360        offset: PyObjectRef,
361        how: i32,
362    ) -> PyResult<SeekFrom> {
363        let seek = match how {
364            0 => SeekFrom::Start(offset.try_into_value(vm)?),
365            1 => SeekFrom::Current(offset.try_into_value(vm)?),
366            2 => SeekFrom::End(offset.try_into_value(vm)?),
367            _ => return Err(vm.new_value_error("invalid value for how")),
368        };
369        Ok(seek)
370    }
371
372    #[derive(Debug)]
373    struct BufferedIO {
374        cursor: Cursor<Vec<u8>>,
375    }
376
377    impl BufferedIO {
378        const fn new(cursor: Cursor<Vec<u8>>) -> Self {
379            Self { cursor }
380        }
381
382        fn write(&mut self, data: &[u8]) -> Option<u64> {
383            if data.is_empty() {
384                return Some(0);
385            }
386            let length = data.len();
387            self.cursor.write_all(data).ok()?;
388            Some(length as u64)
389        }
390
391        //return the entire contents of the underlying
392        fn getvalue(&self) -> Vec<u8> {
393            self.cursor.clone().into_inner()
394        }
395
396        //skip to the jth position
397        fn seek(&mut self, seek: SeekFrom) -> io::Result<u64> {
398            self.cursor.seek(seek)
399        }
400
401        //Read k bytes from the object and return.
402        fn read(&mut self, bytes: Option<usize>) -> Option<Vec<u8>> {
403            let pos = self.cursor.position().to_usize()?;
404            let avail_slice = self.cursor.get_ref().get(pos..)?;
405            // if we don't specify the number of bytes, or it's too big, give the whole rest of the slice
406            let n = bytes.map_or_else(
407                || avail_slice.len(),
408                |n| core::cmp::min(n, avail_slice.len()),
409            );
410            let b = avail_slice[..n].to_vec();
411            self.cursor.set_position((pos + n) as u64);
412            Some(b)
413        }
414
415        const fn tell(&self) -> u64 {
416            self.cursor.position()
417        }
418
419        fn readline(&mut self, size: Option<usize>, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
420            self.read_until(size, b'\n', vm)
421        }
422
423        fn read_until(
424            &mut self,
425            size: Option<usize>,
426            byte: u8,
427            vm: &VirtualMachine,
428        ) -> PyResult<Vec<u8>> {
429            let size = match size {
430                None => {
431                    let mut buf: Vec<u8> = Vec::new();
432                    self.cursor
433                        .read_until(byte, &mut buf)
434                        .map_err(|err| os_err(vm, err))?;
435                    return Ok(buf);
436                }
437                Some(0) => {
438                    return Ok(Vec::new());
439                }
440                Some(size) => size,
441            };
442
443            let available = {
444                // For Cursor, fill_buf returns all of the remaining data unlike other BufReads which have outer reading source.
445                // Unless we add other data by write, there will be no more data.
446                let buf = self.cursor.fill_buf().map_err(|err| os_err(vm, err))?;
447                if size < buf.len() { &buf[..size] } else { buf }
448            };
449            let buf = match available.find_byte(byte) {
450                Some(i) => available[..=i].to_vec(),
451                _ => available.to_vec(),
452            };
453            self.cursor.consume(buf.len());
454            Ok(buf)
455        }
456
457        fn truncate(&mut self, pos: Option<usize>) -> usize {
458            let pos = pos.unwrap_or_else(|| self.tell() as usize);
459            self.cursor.get_mut().truncate(pos);
460            pos
461        }
462    }
463
464    fn check_closed(file: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
465        if file_closed(file, vm)? {
466            Err(io_closed_error(vm))
467        } else {
468            Ok(())
469        }
470    }
471
472    fn check_readable(file: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
473        if vm.call_method(file, "readable", ())?.try_to_bool(vm)? {
474            Ok(())
475        } else {
476            Err(new_unsupported_operation(
477                "File or stream is not readable",
478                vm,
479            ))
480        }
481    }
482
483    fn check_writable(file: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
484        if vm.call_method(file, "writable", ())?.try_to_bool(vm)? {
485            Ok(())
486        } else {
487            Err(new_unsupported_operation(
488                "File or stream is not writable.",
489                vm,
490            ))
491        }
492    }
493
494    fn check_seekable(file: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
495        if vm.call_method(file, "seekable", ())?.try_to_bool(vm)? {
496            Ok(())
497        } else {
498            Err(new_unsupported_operation(
499                "File or stream is not seekable",
500                vm,
501            ))
502        }
503    }
504
505    fn check_decoded(decoded: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyStrRef> {
506        decoded.downcast().map_err(|obj| {
507            vm.new_type_error(format!(
508                "decoder should return a string result, not '{}'",
509                obj.class().name()
510            ))
511        })
512    }
513
514    #[pyattr]
515    #[pyclass(name = "_IOBase")]
516    #[derive(Debug, Default, PyPayload)]
517    // Buffered and text subclasses contain eight-byte-aligned fields on 32-bit targets.
518    #[repr(align(8))]
519    pub(super) struct _IOBase;
520
521    #[pyclass(
522        with(IterNext, Iterable, Destructor),
523        flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
524    )]
525    impl _IOBase {
526        #[pymethod]
527        fn seek(
528            zelf: PyObjectRef,
529            _offset: PyObjectRef,
530            _whence: IgnoredWhence,
531            vm: &VirtualMachine,
532        ) -> PyResult {
533            _unsupported(vm, &zelf, "seek")
534        }
535
536        #[pymethod]
537        fn tell(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
538            vm.call_method(&zelf, "seek", (0, 1))
539        }
540
541        #[pymethod]
542        fn truncate(zelf: PyObjectRef, _pos: IgnoredPos, vm: &VirtualMachine) -> PyResult {
543            _unsupported(vm, &zelf, "truncate")
544        }
545
546        #[pymethod]
547        fn fileno(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
548            _unsupported(vm, &zelf, "fileno")
549        }
550
551        #[pyattr]
552        fn __closed(ctx: &Context) -> PyRef<PyBool> {
553            ctx.new_bool(false)
554        }
555
556        #[pymethod]
557        fn __enter__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
558            check_closed(&zelf, vm)?;
559            Ok(zelf)
560        }
561
562        #[pymethod]
563        fn __exit__(zelf: PyObjectRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
564            vm.call_method(&zelf, "close", ())?;
565            Ok(())
566        }
567
568        #[pymethod]
569        fn flush(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
570            // just check if this is closed; if it isn't, do nothing
571            check_closed(&zelf, vm)
572        }
573
574        #[pymethod]
575        fn seekable(_zelf: PyObjectRef) -> bool {
576            false
577        }
578
579        #[pymethod]
580        fn readable(_zelf: PyObjectRef) -> bool {
581            false
582        }
583
584        #[pymethod]
585        fn writable(_zelf: PyObjectRef) -> bool {
586            false
587        }
588
589        #[pymethod]
590        fn isatty(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
591            check_closed(&zelf, vm)?;
592            Ok(false)
593        }
594
595        #[pygetset]
596        fn closed(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult {
597            instance.get_attr("__closed", vm)
598        }
599
600        #[pymethod]
601        fn close(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
602            iobase_close(&zelf, vm)
603        }
604
605        #[pymethod]
606        fn readline(
607            instance: PyObjectRef,
608            size: OptionalSize,
609            vm: &VirtualMachine,
610        ) -> PyResult<Vec<u8>> {
611            let size = size.to_usize();
612            let read = instance.get_attr("read", vm)?;
613            let mut res = Vec::new();
614            while size.is_none_or(|s| res.len() < s) {
615                let read_res = ArgBytesLike::try_from_object(vm, read.call((1,), vm)?)?;
616                if read_res.with_ref(|b| b.is_empty()) {
617                    break;
618                }
619                read_res.with_ref(|b| res.extend_from_slice(b));
620                if res.ends_with(b"\n") {
621                    break;
622                }
623            }
624            Ok(res)
625        }
626
627        #[pymethod]
628        fn readlines(
629            instance: PyObjectRef,
630            hint: ReadlinesHint,
631            vm: &VirtualMachine,
632        ) -> PyResult<Vec<PyObjectRef>> {
633            let hint = hint.hint.unwrap_or(-1);
634            if hint <= 0 {
635                return instance.try_to_value(vm);
636            }
637            let hint = hint as usize;
638            let mut ret = Vec::new();
639            let it = ArgIterable::<PyObjectRef>::try_from_object(vm, instance)?;
640            let mut full_len = 0;
641            for line in it.iter(vm)? {
642                let line = line?;
643                let line_len = line.length(vm)?;
644                ret.push(line);
645                full_len += line_len;
646                if full_len > hint {
647                    break;
648                }
649            }
650            Ok(ret)
651        }
652
653        #[pymethod]
654        fn writelines(
655            instance: PyObjectRef,
656            lines: ArgIterable,
657            vm: &VirtualMachine,
658        ) -> PyResult<()> {
659            check_closed(&instance, vm)?;
660            for line in lines.iter(vm)? {
661                vm.call_method(&instance, "write", (line?,))?;
662            }
663            Ok(())
664        }
665
666        #[pymethod(name = "_checkClosed")]
667        fn check_closed(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
668            check_closed(&zelf, vm)
669        }
670
671        #[pymethod(name = "_checkReadable")]
672        fn check_readable(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
673            check_readable(&zelf, vm)
674        }
675
676        #[pymethod(name = "_checkWritable")]
677        fn check_writable(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
678            check_writable(&zelf, vm)
679        }
680
681        #[pymethod(name = "_checkSeekable")]
682        fn check_seekable(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
683            check_seekable(&zelf, vm)
684        }
685    }
686
687    impl Destructor for _IOBase {
688        fn slot_del(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
689            // C-level IO types (FileIO, Buffered*, TextIOWrapper) have their own
690            // slot_del that calls iobase_finalize with proper _finalizing flag
691            // and _dealloc_warn chain. This base fallback is only reached by
692            // Python-level subclasses, where we silently discard close() errors
693            // to avoid surfacing unraisable from partially initialized objects.
694            let _ = vm.call_method(zelf, "close", ());
695            Ok(())
696        }
697
698        #[cold]
699        fn del(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<()> {
700            unreachable!("slot_del is implemented")
701        }
702    }
703
704    impl Iterable for _IOBase {
705        fn slot_iter(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
706            check_closed(&zelf, vm)?;
707            Ok(zelf)
708        }
709
710        fn iter(_zelf: PyRef<Self>, _vm: &VirtualMachine) -> PyResult {
711            unreachable!("slot_iter is implemented")
712        }
713    }
714
715    impl IterNext for _IOBase {
716        fn slot_iternext(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
717            let line = vm.call_method(zelf, "readline", ())?;
718            Ok(if !line.try_to_bool(vm)? {
719                PyIterReturn::StopIteration(None)
720            } else {
721                PyIterReturn::Return(line)
722            })
723        }
724
725        fn next(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyIterReturn> {
726            unreachable!("slot_iternext is implemented")
727        }
728    }
729
730    pub(super) fn iobase_close(file: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
731        if !file_closed(file, vm)? {
732            let res = vm.call_method(file, "flush", ());
733            file.set_attr("__closed", vm.new_pyobj(true), vm)?;
734            res?;
735        }
736        Ok(())
737    }
738
739    #[pyattr]
740    #[pyclass(name = "_RawIOBase", base = _IOBase)]
741    #[derive(Debug, Default)]
742    #[repr(transparent)]
743    pub(super) struct _RawIOBase(_IOBase);
744
745    #[pyclass(flags(BASETYPE, HAS_DICT, HAS_WEAKREF))]
746    impl _RawIOBase {
747        #[pymethod]
748        fn read(zelf: PyObjectRef, size: OptionalSize, vm: &VirtualMachine) -> PyResult {
749            if let Some(size) = size.to_usize() {
750                let b = PyByteArray::from(vm.new_zeroed_bytes(size)?).into_ref(&vm.ctx);
751                let n = <Option<isize>>::try_from_object(
752                    vm,
753                    vm.call_method(&zelf, "readinto", (b.clone(),))?,
754                )?;
755                Ok(match n {
756                    None => vm.ctx.none(),
757                    Some(n) => {
758                        // Validate the return value is within bounds
759                        if n < 0 || (n as usize) > size {
760                            return Err(vm.new_value_error(format!(
761                                "readinto returned {n} outside buffer size {size}"
762                            )));
763                        }
764                        let n = n as usize;
765                        let mut bytes = b.borrow_buf_mut();
766                        bytes.truncate(n);
767                        // FIXME: try to use Arc::unwrap on the bytearray to get at the inner buffer
768                        bytes.clone().to_pyobject(vm)
769                    }
770                })
771            } else {
772                vm.call_method(&zelf, "readall", ())
773            }
774        }
775
776        #[pymethod]
777        fn readall(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<Option<Vec<u8>>> {
778            let mut chunks = Vec::new();
779            let mut total_len = 0;
780            loop {
781                // Loop with EINTR handling (PEP 475)
782                let data = loop {
783                    let res = vm.call_method(&zelf, "read", (DEFAULT_BUFFER_SIZE,));
784                    match trap_eintr(res, vm)? {
785                        Some(val) => break val,
786                        None => continue,
787                    }
788                };
789                let data = <Option<PyBytesRef>>::try_from_object(vm, data)?;
790                match data {
791                    None => {
792                        if chunks.is_empty() {
793                            return Ok(None);
794                        }
795                        break;
796                    }
797                    Some(b) => {
798                        if b.as_bytes().is_empty() {
799                            break;
800                        }
801                        total_len += b.as_bytes().len();
802                        chunks.push(b)
803                    }
804                }
805            }
806            let mut ret = Vec::with_capacity(total_len);
807            for b in chunks {
808                ret.extend_from_slice(b.as_bytes())
809            }
810            Ok(Some(ret))
811        }
812
813        #[pymethod]
814        fn readinto(_zelf: PyObjectRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
815            Err(vm.new_not_implemented_error(String::new()))
816        }
817
818        #[pymethod]
819        fn write(_zelf: PyObjectRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
820            Err(vm.new_not_implemented_error(String::new()))
821        }
822    }
823
824    #[pyattr]
825    #[pyclass(name = "_BufferedIOBase", base = _IOBase)]
826    #[derive(Debug, Default)]
827    #[repr(transparent)]
828    struct _BufferedIOBase(_IOBase);
829
830    #[pyclass(flags(BASETYPE, HAS_WEAKREF))]
831    impl _BufferedIOBase {
832        #[pymethod]
833        fn read(zelf: PyObjectRef, _size: IgnoredSize, vm: &VirtualMachine) -> PyResult {
834            _unsupported(vm, &zelf, "read")
835        }
836
837        #[pymethod]
838        fn read1(zelf: PyObjectRef, _size: IgnoredSize, vm: &VirtualMachine) -> PyResult {
839            _unsupported(vm, &zelf, "read1")
840        }
841
842        fn _readinto(
843            zelf: &PyObject,
844            buf_obj: &PyObject,
845            method: &str,
846            vm: &VirtualMachine,
847        ) -> PyResult<usize> {
848            let b = ArgMemoryBuffer::try_from_borrowed_object(vm, buf_obj)?;
849            let l = b.len();
850            let data = vm.call_method(zelf, method, (l,))?;
851            if data.is(buf_obj) {
852                return Ok(l);
853            }
854            let mut buf = b.borrow_buf_mut();
855            let data = ArgBytesLike::try_from_object(vm, data)?;
856            let data = data.borrow_buf();
857            match buf.get_mut(..data.len()) {
858                Some(slice) => {
859                    slice.copy_from_slice(&data);
860                    Ok(data.len())
861                }
862                None => {
863                    Err(vm.new_value_error("readinto: buffer and read data have different lengths"))
864                }
865            }
866        }
867        #[pymethod]
868        fn readinto(
869            zelf: PyObjectRef,
870            buffer: PyObjectRef,
871            vm: &VirtualMachine,
872        ) -> PyResult<usize> {
873            Self::_readinto(&zelf, &buffer, "read", vm)
874        }
875
876        #[pymethod]
877        fn readinto1(
878            zelf: PyObjectRef,
879            buffer: PyObjectRef,
880            vm: &VirtualMachine,
881        ) -> PyResult<usize> {
882            Self::_readinto(&zelf, &buffer, "read1", vm)
883        }
884
885        #[pymethod]
886        fn write(zelf: PyObjectRef, _b: PyObjectRef, vm: &VirtualMachine) -> PyResult {
887            _unsupported(vm, &zelf, "write")
888        }
889
890        #[pymethod]
891        fn detach(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
892            _unsupported(vm, &zelf, "detach")
893        }
894    }
895
896    // TextIO Base has no public constructor
897    #[pyattr]
898    #[pyclass(name = "_TextIOBase", base = _IOBase)]
899    #[derive(Debug, Default)]
900    #[repr(transparent)]
901    struct _TextIOBase(_IOBase);
902
903    #[pyclass(flags(BASETYPE, HAS_WEAKREF))]
904    impl _TextIOBase {
905        #[pymethod]
906        fn read(zelf: PyObjectRef, _size: IgnoredSize, vm: &VirtualMachine) -> PyResult {
907            _unsupported(vm, &zelf, "read")
908        }
909
910        #[pymethod]
911        fn write(zelf: PyObjectRef, _s: PyObjectRef, vm: &VirtualMachine) -> PyResult {
912            _unsupported(vm, &zelf, "write")
913        }
914
915        #[pymethod]
916        fn truncate(zelf: PyObjectRef, _pos: IgnoredPos, vm: &VirtualMachine) -> PyResult {
917            _unsupported(vm, &zelf, "truncate")
918        }
919
920        #[pymethod]
921        fn readline(zelf: PyObjectRef, _size: IgnoredSize, vm: &VirtualMachine) -> PyResult {
922            _unsupported(vm, &zelf, "readline")
923        }
924
925        #[pymethod]
926        fn detach(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
927            _unsupported(vm, &zelf, "detach")
928        }
929
930        #[pygetset]
931        fn encoding(_zelf: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
932            vm.ctx.none()
933        }
934
935        #[pygetset]
936        fn newlines(_zelf: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
937            vm.ctx.none()
938        }
939
940        #[pygetset]
941        fn errors(_zelf: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
942            vm.ctx.none()
943        }
944    }
945
946    #[derive(FromArgs, Clone)]
947    struct BufferSize {
948        // 128 * 1024, the module buffer size.
949        #[pyarg(any, default = 131072)]
950        buffer_size: isize,
951    }
952
953    #[derive(FromArgs)]
954    struct BufferedInitArgs {
955        #[pyarg(any)]
956        raw: PyObjectRef,
957        #[pyarg(any, default = 131072)]
958        buffer_size: isize,
959    }
960
961    #[derive(FromArgs)]
962    struct BufferedRWPairArgs {
963        #[pyarg(positional)]
964        reader: PyObjectRef,
965        #[pyarg(positional)]
966        writer: PyObjectRef,
967        #[pyarg(positional, default = 131072)]
968        buffer_size: isize,
969    }
970
971    bitflags::bitflags! {
972        #[derive(Copy, Clone, Debug, PartialEq, Default)]
973        struct BufferedFlags: u8 {
974            const DETACHED = 1 << 0;
975            const WRITABLE = 1 << 1;
976            const READABLE = 1 << 2;
977        }
978    }
979
980    type RawCell = crate::object::PyAtomicRef<Option<PyObject>>;
981
982    #[derive(Debug, Default)]
983    struct BufferedData {
984        flags: BufferedFlags,
985        abs_pos: Offset,
986        buffer: Vec<u8>,
987        pos: Offset,
988        raw_pos: Offset,
989        read_end: Offset,
990        write_pos: Offset,
991        write_end: Offset,
992    }
993
994    impl BufferedData {
995        fn missing_raw_msg(&self) -> &'static str {
996            if self.flags.contains(BufferedFlags::DETACHED) {
997                "raw stream has been detached"
998            } else {
999                "I/O operation on uninitialized object"
1000            }
1001        }
1002
1003        fn check_init<'a>(&self, raw: &'a RawCell, vm: &VirtualMachine) -> PyResult<&'a PyObject> {
1004            raw.deref()
1005                .ok_or_else(|| vm.new_value_error(self.missing_raw_msg()))
1006        }
1007
1008        fn raw_owned(&self, raw: &RawCell, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
1009            raw.load_owned()
1010                .ok_or_else(|| vm.new_value_error(self.missing_raw_msg()))
1011        }
1012
1013        #[inline]
1014        const fn writable(&self) -> bool {
1015            self.flags.contains(BufferedFlags::WRITABLE)
1016        }
1017
1018        #[inline]
1019        const fn readable(&self) -> bool {
1020            self.flags.contains(BufferedFlags::READABLE)
1021        }
1022
1023        #[inline]
1024        const fn valid_read(&self) -> bool {
1025            self.readable() && self.read_end != -1
1026        }
1027
1028        #[inline]
1029        const fn valid_write(&self) -> bool {
1030            self.writable() && self.write_end != -1
1031        }
1032
1033        #[inline]
1034        const fn raw_offset(&self) -> Offset {
1035            if (self.valid_read() || self.valid_write()) && self.raw_pos >= 0 {
1036                self.raw_pos - self.pos
1037            } else {
1038                0
1039            }
1040        }
1041
1042        #[inline]
1043        const fn readahead(&self) -> Offset {
1044            if self.valid_read() {
1045                self.read_end - self.pos
1046            } else {
1047                0
1048            }
1049        }
1050
1051        const fn reset_read(&mut self) {
1052            self.read_end = -1;
1053        }
1054
1055        const fn reset_write(&mut self) {
1056            self.write_pos = 0;
1057            self.write_end = -1;
1058        }
1059
1060        fn flush(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult<()> {
1061            if !self.valid_write() || self.write_pos == self.write_end {
1062                self.reset_write();
1063                return Ok(());
1064            }
1065
1066            let rewind = self.raw_offset() + (self.pos - self.write_pos);
1067            if rewind != 0 {
1068                self.raw_seek(raw, -rewind, 1, vm)?;
1069                self.raw_pos -= rewind;
1070            }
1071
1072            while self.write_pos < self.write_end {
1073                let n = self.raw_write(
1074                    raw,
1075                    None,
1076                    self.write_pos as usize..self.write_end as usize,
1077                    vm,
1078                )?;
1079                let n = match n {
1080                    Some(n) => n,
1081                    None => {
1082                        // BlockingIOError(errno, msg, characters_written=0)
1083                        return Err(vm
1084                            .new_payload_exception::<PyOSError>(
1085                                vm.ctx.exceptions.blocking_io_error.to_owned(),
1086                                vec![
1087                                    vm.new_pyobj(EAGAIN),
1088                                    vm.new_pyobj("write could not complete without blocking"),
1089                                    vm.new_pyobj(0),
1090                                ]
1091                                .into(),
1092                            )?
1093                            .upcast());
1094                    }
1095                };
1096                self.write_pos += n as Offset;
1097                self.raw_pos = self.write_pos;
1098                vm.check_signals()?;
1099            }
1100
1101            self.reset_write();
1102
1103            Ok(())
1104        }
1105
1106        fn flush_rewind(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult<()> {
1107            self.flush(raw, vm)?;
1108            if self.readable() {
1109                let res = self.raw_seek(raw, -self.raw_offset(), 1, vm);
1110                self.reset_read();
1111                res?;
1112            }
1113            Ok(())
1114        }
1115
1116        fn raw_seek(
1117            &mut self,
1118            raw: &RawCell,
1119            pos: Offset,
1120            whence: i32,
1121            vm: &VirtualMachine,
1122        ) -> PyResult<Offset> {
1123            let ret = vm.call_method(self.check_init(raw, vm)?, "seek", (pos, whence))?;
1124            let offset = get_offset(&ret, vm)?;
1125            if offset < 0 {
1126                return Err(
1127                    vm.new_os_error(format!("Raw stream returned invalid position {offset}"))
1128                );
1129            }
1130            self.abs_pos = offset;
1131            Ok(offset)
1132        }
1133
1134        fn seek(
1135            &mut self,
1136            raw: &RawCell,
1137            target: Offset,
1138            whence: i32,
1139            vm: &VirtualMachine,
1140        ) -> PyResult<Offset> {
1141            if matches!(whence, 0 | 1) && self.readable() {
1142                let current = self.raw_tell_cache(raw, vm)?;
1143                let available = self.readahead();
1144                if available > 0 {
1145                    let offset = if whence == 0 {
1146                        target - (current - self.raw_offset())
1147                    } else {
1148                        target
1149                    };
1150                    if offset >= -self.pos && offset <= available {
1151                        self.pos += offset;
1152                        // GH-95782: character devices may report raw position 0
1153                        // even after reading, which would make this negative
1154                        let result = current - available + offset;
1155                        return Ok(if result < 0 { 0 } else { result });
1156                    }
1157                }
1158            }
1159            // raw.get_attr("seek", vm)?.call(args, vm)
1160            if self.writable() {
1161                self.flush(raw, vm)?;
1162            }
1163            let target = if whence == 1 {
1164                target - self.raw_offset()
1165            } else {
1166                target
1167            };
1168            let res = self.raw_seek(raw, target, whence, vm);
1169            self.raw_pos = -1;
1170            if res.is_ok() && self.readable() {
1171                self.reset_read();
1172            }
1173            res
1174        }
1175
1176        fn raw_tell(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult<Offset> {
1177            let raw_obj = self.check_init(raw, vm)?;
1178            let ret = vm.call_method(raw_obj, "tell", ())?;
1179            let offset = get_offset(&ret, vm)?;
1180            if offset < 0 {
1181                return Err(
1182                    vm.new_os_error(format!("Raw stream returned invalid position {offset}"))
1183                );
1184            }
1185            self.abs_pos = offset;
1186            Ok(offset)
1187        }
1188
1189        fn raw_tell_cache(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult<Offset> {
1190            if self.abs_pos == -1 {
1191                self.raw_tell(raw, vm)
1192            } else {
1193                Ok(self.abs_pos)
1194            }
1195        }
1196
1197        /// None means non-blocking failed
1198        fn raw_write(
1199            &mut self,
1200            raw: &RawCell,
1201            buf: Option<PyBuffer>,
1202            buf_range: Range<usize>,
1203            vm: &VirtualMachine,
1204        ) -> PyResult<Option<usize>> {
1205            let len = buf_range.len();
1206
1207            // Prepare the memoryview; if using the internal buffer, stash it
1208            // in write_buf so we can restore it after the write.
1209            let (mem_obj, write_buf) = if let Some(buf) = buf {
1210                let mem_obj =
1211                    PyMemoryView::from_buffer_range(buf, buf_range, vm)?.into_ref(&vm.ctx);
1212                (mem_obj, None)
1213            } else {
1214                let v = core::mem::take(&mut self.buffer);
1215                let wb = VecBuffer::from(v).into_ref(&vm.ctx);
1216                let mem_obj =
1217                    PyMemoryView::from_buffer_range(wb.clone().into_pybuffer(true), buf_range, vm)?
1218                        .into_ref(&vm.ctx);
1219                (mem_obj, Some(wb))
1220            };
1221
1222            // Loop if write() raises EINTR (PEP 475)
1223            let res = loop {
1224                let res = vm.call_method(self.check_init(raw, vm)?, "write", (mem_obj.clone(),));
1225                match trap_eintr(res, vm) {
1226                    Ok(Some(val)) => break Ok(val),
1227                    Ok(None) => continue,
1228                    Err(e) => break Err(e),
1229                }
1230            };
1231
1232            // Restore internal buffer if we borrowed it
1233            if let Some(wb) = write_buf {
1234                mem_obj.release();
1235                self.buffer = wb.take();
1236            }
1237
1238            let res = res?;
1239
1240            if vm.is_none(&res) {
1241                return Ok(None);
1242            }
1243            let n = isize::try_from_object(vm, res)?;
1244            if n < 0 || n as usize > len {
1245                return Err(vm.new_os_error(format!(
1246                    "raw write() returned invalid length {n} (should have been between 0 and {len})"
1247                )));
1248            }
1249            if self.abs_pos != -1 {
1250                self.abs_pos += n as Offset
1251            }
1252            Ok(Some(n as usize))
1253        }
1254
1255        fn write(
1256            &mut self,
1257            raw: &RawCell,
1258            obj: ArgBytesLike,
1259            vm: &VirtualMachine,
1260        ) -> PyResult<usize> {
1261            if !self.valid_read() && !self.valid_write() {
1262                self.pos = 0;
1263                self.raw_pos = 0;
1264            }
1265            let avail = self.buffer.len() - self.pos as usize;
1266            let buf_len;
1267            {
1268                let buf = obj.borrow_buf();
1269                buf_len = buf.len();
1270                if buf.len() <= avail {
1271                    self.buffer[self.pos as usize..][..buf.len()].copy_from_slice(&buf);
1272                    if !self.valid_write() || self.write_pos > self.pos {
1273                        self.write_pos = self.pos
1274                    }
1275                    self.adjust_position(self.pos + buf.len() as Offset);
1276                    if self.pos > self.write_end {
1277                        self.write_end = self.pos
1278                    }
1279                    return Ok(buf.len());
1280                }
1281            }
1282
1283            // if BlockingIOError, shift buffer
1284            // and try to buffer the new data; otherwise propagate the error
1285            match self.flush(raw, vm) {
1286                Ok(()) => {}
1287                Err(e) if e.fast_isinstance(vm.ctx.exceptions.blocking_io_error) => {
1288                    if self.readable() {
1289                        self.reset_read();
1290                    }
1291                    // Shift buffer and adjust positions
1292                    let shift = self.write_pos;
1293                    if shift > 0 {
1294                        self.buffer
1295                            .copy_within(shift as usize..self.write_end as usize, 0);
1296                        self.write_end -= shift;
1297                        self.raw_pos -= shift;
1298                        self.pos -= shift;
1299                        self.write_pos = 0;
1300                    }
1301                    let avail = self.buffer.len() - self.write_end as usize;
1302                    if buf_len <= avail {
1303                        // Everything can be buffered
1304                        let buf = obj.borrow_buf();
1305                        self.buffer[self.write_end as usize..][..buf_len].copy_from_slice(&buf);
1306                        self.write_end += buf_len as Offset;
1307                        self.pos += buf_len as Offset;
1308                        return Ok(buf_len);
1309                    }
1310                    // Buffer as much as possible and return BlockingIOError
1311                    let buf = obj.borrow_buf();
1312                    self.buffer[self.write_end as usize..][..avail].copy_from_slice(&buf[..avail]);
1313                    self.write_end += avail as Offset;
1314                    self.pos += avail as Offset;
1315                    return Err(vm
1316                        .new_payload_exception::<PyOSError>(
1317                            vm.ctx.exceptions.blocking_io_error.to_owned(),
1318                            vec![
1319                                vm.new_pyobj(EAGAIN),
1320                                vm.new_pyobj("write could not complete without blocking"),
1321                                vm.new_pyobj(avail),
1322                            ]
1323                            .into(),
1324                        )?
1325                        .upcast());
1326                }
1327                Err(e) => return Err(e),
1328            }
1329
1330            // Only reach here if flush succeeded
1331            let offset = self.raw_offset();
1332            if offset != 0 {
1333                self.raw_seek(raw, -offset, 1, vm)?;
1334                self.raw_pos -= offset;
1335            }
1336
1337            let mut remaining = buf_len;
1338            let mut written = 0;
1339            let buffer: PyBuffer = obj.into();
1340            while remaining > self.buffer.len() {
1341                let res = self.raw_write(raw, Some(buffer.clone()), written..buf_len, vm)?;
1342                match res {
1343                    Some(n) => {
1344                        written += n;
1345                        if let Some(r) = remaining.checked_sub(n) {
1346                            remaining = r
1347                        } else {
1348                            break;
1349                        }
1350                        vm.check_signals()?;
1351                    }
1352                    None if remaining > self.buffer.len() => {
1353                        // raw file is non-blocking
1354                        // can't buffer everything, buffer what we can and error
1355                        let buf = buffer.as_contiguous().unwrap();
1356                        let buffer_len = self.buffer.len();
1357                        self.buffer.copy_from_slice(&buf[written..][..buffer_len]);
1358                        self.raw_pos = 0;
1359                        let buffer_size = self.buffer.len() as _;
1360                        self.adjust_position(buffer_size);
1361                        self.write_end = buffer_size;
1362                        // BlockingIOError(errno, msg, characters_written)
1363                        let chars_written = written + buffer_len;
1364                        return Err(vm
1365                            .new_payload_exception::<PyOSError>(
1366                                vm.ctx.exceptions.blocking_io_error.to_owned(),
1367                                vec![
1368                                    vm.new_pyobj(EAGAIN),
1369                                    vm.new_pyobj("write could not complete without blocking"),
1370                                    vm.new_pyobj(chars_written),
1371                                ]
1372                                .into(),
1373                            )?
1374                            .upcast());
1375                    }
1376                    None => break,
1377                }
1378            }
1379
1380            if self.readable() {
1381                self.reset_read();
1382            }
1383
1384            if remaining > 0 {
1385                let buf = buffer.as_contiguous().unwrap();
1386                self.buffer[..remaining].copy_from_slice(&buf[written..][..remaining]);
1387                written += remaining;
1388            }
1389
1390            self.write_pos = 0;
1391            self.write_end = remaining as _;
1392            self.adjust_position(remaining as _);
1393            self.raw_pos = 0;
1394
1395            Ok(written)
1396        }
1397
1398        fn active_read_slice(&self) -> &[u8] {
1399            &self.buffer[self.pos as usize..][..self.readahead() as usize]
1400        }
1401
1402        fn read_fast(&mut self, n: usize) -> Option<Vec<u8>> {
1403            let ret = self.active_read_slice().get(..n)?.to_vec();
1404            self.pos += n as Offset;
1405            Some(ret)
1406        }
1407
1408        fn read_generic(
1409            &mut self,
1410            raw: &RawCell,
1411            n: usize,
1412            vm: &VirtualMachine,
1413        ) -> PyResult<Option<Vec<u8>>> {
1414            if let Some(fast) = self.read_fast(n) {
1415                return Ok(Some(fast));
1416            }
1417
1418            let current_size = self.readahead() as usize;
1419
1420            let mut out = vm.new_zeroed_bytes(n)?;
1421            let mut remaining = n;
1422            let mut written = 0;
1423            if current_size > 0 {
1424                let slice = self.active_read_slice();
1425                out[..slice.len()].copy_from_slice(slice);
1426                remaining -= current_size;
1427                written += current_size;
1428                self.pos += current_size as Offset;
1429            }
1430            if self.writable() {
1431                self.flush_rewind(raw, vm)?;
1432            }
1433            self.reset_read();
1434            macro_rules! handle_opt_read {
1435                ($x:expr) => {
1436                    match ($x, written > 0) {
1437                        (Some(0), _) | (None, true) => {
1438                            out.truncate(written);
1439                            return Ok(Some(out));
1440                        }
1441                        (Some(r), _) => r,
1442                        (None, _) => return Ok(None),
1443                    }
1444                };
1445            }
1446            while remaining > 0 && !self.buffer.is_empty() {
1447                // MINUS_LAST_BLOCK() in CPython
1448                let r = self.buffer.len() * (remaining / self.buffer.len());
1449                if r == 0 {
1450                    break;
1451                }
1452                let r = self.raw_read(raw, Either::A(Some(&mut out)), written..written + r, vm)?;
1453                let r = handle_opt_read!(r);
1454                remaining -= r;
1455                written += r;
1456            }
1457            self.pos = 0;
1458            self.raw_pos = 0;
1459            self.read_end = 0;
1460
1461            while remaining > 0 && (self.read_end as usize) < self.buffer.len() {
1462                let r = handle_opt_read!(self.fill_buffer(raw, vm)?);
1463                if remaining > r {
1464                    out[written..][..r].copy_from_slice(&self.buffer[self.pos as usize..][..r]);
1465                    written += r;
1466                    self.pos += r as Offset;
1467                    remaining -= r;
1468                } else if remaining > 0 {
1469                    out[written..][..remaining]
1470                        .copy_from_slice(&self.buffer[self.pos as usize..][..remaining]);
1471                    written += remaining;
1472                    self.pos += remaining as Offset;
1473                    remaining = 0;
1474                }
1475                if remaining == 0 {
1476                    break;
1477                }
1478            }
1479
1480            Ok(Some(out))
1481        }
1482
1483        fn fill_buffer(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult<Option<usize>> {
1484            let start = if self.valid_read() {
1485                self.read_end as usize
1486            } else {
1487                0
1488            };
1489            let buf_end = self.buffer.len();
1490            let res = self.raw_read(raw, Either::A(None), start..buf_end, vm)?;
1491            if let Some(n) = res.filter(|n| *n > 0) {
1492                let new_start = (start + n) as Offset;
1493                self.read_end = new_start;
1494                self.raw_pos = new_start;
1495            }
1496            Ok(res)
1497        }
1498
1499        fn raw_read(
1500            &mut self,
1501            raw: &RawCell,
1502            v: Either<Option<&mut Vec<u8>>, PyBuffer>,
1503            buf_range: Range<usize>,
1504            vm: &VirtualMachine,
1505        ) -> PyResult<Option<usize>> {
1506            let len = buf_range.len();
1507            let raw_obj = self.check_init(raw, vm)?;
1508            let res = match v {
1509                Either::A(v) => {
1510                    let v = v.unwrap_or(&mut self.buffer);
1511                    let read_buf = VecBuffer::from(core::mem::take(v)).into_ref(&vm.ctx);
1512                    let mem_obj = PyMemoryView::from_buffer_range(
1513                        read_buf.clone().into_pybuffer(false),
1514                        buf_range,
1515                        vm,
1516                    )?
1517                    .into_ref(&vm.ctx);
1518
1519                    // Loop if readinto() raises EINTR (PEP 475)
1520                    let res = loop {
1521                        let res = vm.call_method(raw_obj, "readinto", (mem_obj.clone(),));
1522                        match trap_eintr(res, vm) {
1523                            Ok(Some(val)) => break Ok(val),
1524                            Ok(None) => continue, // EINTR, retry
1525                            Err(e) => break Err(e),
1526                        }
1527                    };
1528
1529                    mem_obj.release();
1530                    // Always restore the buffer, even if an error occurred
1531                    *v = read_buf.take();
1532
1533                    res?
1534                }
1535                Either::B(buf) => {
1536                    let mem_obj =
1537                        PyMemoryView::from_buffer_range(buf, buf_range, vm)?.into_ref(&vm.ctx);
1538                    // Loop if readinto() raises EINTR (PEP 475)
1539                    loop {
1540                        let res = vm.call_method(raw_obj, "readinto", (mem_obj.clone(),));
1541                        match trap_eintr(res, vm)? {
1542                            Some(val) => break val,
1543                            None => continue,
1544                        }
1545                    }
1546                }
1547            };
1548
1549            if vm.is_none(&res) {
1550                return Ok(None);
1551            }
1552            // Try to convert to int; if it fails, treat as -1 and chain the TypeError
1553            let (n, type_error) = match isize::try_from_object(vm, res) {
1554                Ok(n) => (n, None),
1555                Err(e) => (-1, Some(e)),
1556            };
1557            if n < 0 || n as usize > len {
1558                let os_error = vm.new_os_error(format!(
1559                    "raw readinto() returned invalid length {n} (should have been between 0 and {len})"
1560                ));
1561                if let Some(cause) = type_error {
1562                    os_error.set_cause(Some(cause));
1563                }
1564                return Err(os_error);
1565            }
1566            if n > 0 && self.abs_pos != -1 {
1567                self.abs_pos += n as Offset
1568            }
1569            Ok(Some(n as usize))
1570        }
1571
1572        fn read_all(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult<Option<PyBytesRef>> {
1573            let buf = self.active_read_slice();
1574            let data = if buf.is_empty() {
1575                None
1576            } else {
1577                let b = buf.to_vec();
1578                self.pos += buf.len() as Offset;
1579                Some(b)
1580            };
1581
1582            if self.writable() {
1583                self.flush_rewind(raw, vm)?;
1584            }
1585
1586            let readall = vm
1587                .get_str_method(self.raw_owned(raw, vm)?, "readall")
1588                .transpose()?;
1589            if let Some(readall) = readall {
1590                let res = readall.call((), vm)?;
1591                let res = <Option<PyBytesRef>>::try_from_object(vm, res)?;
1592                let ret = if let Some(mut data) = data {
1593                    if let Some(bytes) = res {
1594                        data.extend_from_slice(bytes.as_bytes());
1595                    }
1596                    Some(PyBytes::from(data).into_ref(&vm.ctx))
1597                } else {
1598                    res
1599                };
1600                return Ok(ret);
1601            }
1602
1603            let mut chunks = Vec::new();
1604
1605            let mut read_size = 0;
1606            loop {
1607                // Loop with EINTR handling (PEP 475)
1608                let read_data = loop {
1609                    let res = vm.call_method(self.check_init(raw, vm)?, "read", ());
1610                    match trap_eintr(res, vm)? {
1611                        Some(val) => break val,
1612                        None => continue,
1613                    }
1614                };
1615                let read_data = <Option<PyBytesRef>>::try_from_object(vm, read_data)?;
1616
1617                match read_data {
1618                    Some(b) if !b.as_bytes().is_empty() => {
1619                        let l = b.as_bytes().len();
1620                        read_size += l;
1621                        if self.abs_pos != -1 {
1622                            self.abs_pos += l as Offset;
1623                        }
1624                        chunks.push(b);
1625                    }
1626                    read_data => {
1627                        let ret = if data.is_none() && read_size == 0 {
1628                            read_data
1629                        } else {
1630                            let mut data = data.unwrap_or_default();
1631                            data.reserve(read_size);
1632                            for bytes in &chunks {
1633                                data.extend_from_slice(bytes.as_bytes())
1634                            }
1635                            Some(PyBytes::from(data).into_ref(&vm.ctx))
1636                        };
1637                        break Ok(ret);
1638                    }
1639                }
1640            }
1641        }
1642
1643        const fn adjust_position(&mut self, new_pos: Offset) {
1644            self.pos = new_pos;
1645            if self.valid_read() && self.read_end < self.pos {
1646                self.read_end = self.pos
1647            }
1648        }
1649
1650        fn peek(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
1651            let have = self.readahead();
1652            let slice = if have > 0 {
1653                &self.buffer[self.pos as usize..][..have as usize]
1654            } else {
1655                self.reset_read();
1656                let r = self.fill_buffer(raw, vm)?.unwrap_or(0);
1657                self.pos = 0;
1658                &self.buffer[..r]
1659            };
1660            Ok(slice.to_vec())
1661        }
1662
1663        fn readinto_generic(
1664            &mut self,
1665            raw: &RawCell,
1666            buf: PyBuffer,
1667            readinto1: bool,
1668            vm: &VirtualMachine,
1669        ) -> PyResult<Option<usize>> {
1670            let mut written = 0;
1671            let n = self.readahead();
1672            let buf_len;
1673            {
1674                let mut b = buf.as_contiguous_mut().unwrap();
1675                buf_len = b.len();
1676                if n > 0 {
1677                    if n as usize >= b.len() {
1678                        b.copy_from_slice(&self.buffer[self.pos as usize..][..buf_len]);
1679                        self.pos += buf_len as Offset;
1680                        return Ok(Some(buf_len));
1681                    }
1682                    b[..n as usize]
1683                        .copy_from_slice(&self.buffer[self.pos as usize..][..n as usize]);
1684                    self.pos += n;
1685                    written = n as usize;
1686                }
1687            }
1688            if self.writable() {
1689                self.flush_rewind(raw, vm)?;
1690            }
1691            self.reset_read();
1692            self.pos = 0;
1693
1694            let mut remaining = buf_len - written;
1695            while remaining > 0 {
1696                let n = if remaining > self.buffer.len() {
1697                    self.raw_read(
1698                        raw,
1699                        Either::B(buf.clone()),
1700                        written..written + remaining,
1701                        vm,
1702                    )?
1703                } else if !(readinto1 && written != 0) {
1704                    let n = self.fill_buffer(raw, vm)?;
1705                    if let Some(n) = n.filter(|&n| n > 0) {
1706                        let n = core::cmp::min(n, remaining);
1707                        buf.as_contiguous_mut().unwrap()[written..][..n]
1708                            .copy_from_slice(&self.buffer[self.pos as usize..][..n]);
1709                        self.pos += n as Offset;
1710                        written += n;
1711                        remaining -= n;
1712                        continue;
1713                    }
1714                    n
1715                } else {
1716                    break;
1717                };
1718                let n = match n {
1719                    Some(0) => break,
1720                    None if written > 0 => break,
1721                    None => return Ok(None),
1722                    Some(n) => n,
1723                };
1724
1725                if readinto1 {
1726                    written += n;
1727                    break;
1728                }
1729                written += n;
1730                remaining -= n;
1731            }
1732
1733            Ok(Some(written))
1734        }
1735    }
1736
1737    pub(super) fn get_offset(obj: &PyObject, vm: &VirtualMachine) -> PyResult<Offset> {
1738        let int = obj.try_index(vm)?;
1739        int.as_bigint().try_into().map_err(|_| {
1740            vm.new_value_error(format!(
1741                "cannot fit '{}' into an offset-sized integer",
1742                obj.class().name()
1743            ))
1744        })
1745    }
1746
1747    pub(super) fn repr_file_obj_name(
1748        obj: &PyObject,
1749        vm: &VirtualMachine,
1750    ) -> PyResult<Option<PyStrRef>> {
1751        let name = match obj.get_attr("name", vm) {
1752            Ok(name) => Some(name),
1753            Err(e)
1754                if e.fast_isinstance(vm.ctx.exceptions.attribute_error)
1755                    || e.fast_isinstance(vm.ctx.exceptions.value_error) =>
1756            {
1757                None
1758            }
1759            Err(e) => return Err(e),
1760        };
1761        match name {
1762            Some(name) => {
1763                if let Some(_guard) = ReprGuard::enter(vm, obj) {
1764                    name.repr(vm).map(Some)
1765                } else {
1766                    Err(vm.new_runtime_error(format!(
1767                        "reentrant call inside {}.__repr__",
1768                        obj.class().slot_name()
1769                    )))
1770                }
1771            }
1772            None => Ok(None),
1773        }
1774    }
1775
1776    #[pyclass]
1777    trait BufferedMixin: PyPayload + StaticType {
1778        const CLASS_NAME: &'static str;
1779        const READABLE: bool;
1780        const WRITABLE: bool;
1781        const SEEKABLE: bool = false;
1782
1783        fn data(&self) -> &PyThreadMutex<BufferedData>;
1784        fn raw_cell(&self) -> &RawCell;
1785        fn closing(&self) -> &AtomicBool;
1786        fn finalizing(&self) -> &AtomicBool;
1787
1788        fn lock(&self, vm: &VirtualMachine) -> PyResult<PyThreadMutexGuard<'_, BufferedData>> {
1789            self.data()
1790                .lock_wrapped(|do_lock| vm.allow_threads(do_lock))
1791                .ok_or_else(|| vm.new_runtime_error("reentrant call inside buffered io"))
1792        }
1793
1794        #[pyslot]
1795        fn slot_init(zelf: &PyObject, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
1796            let zelf: &Py<Self> = zelf.try_to_ref(vm)?;
1797            let (raw, BufferSize { buffer_size }): (PyObjectRef, _) =
1798                args.bind(vm).map_err(|e| {
1799                    let str_repr = e.__str__(vm).as_wtf8().to_owned();
1800                    let msg = format!("{}() {}", Self::CLASS_NAME, str_repr);
1801                    vm.new_exception_msg(e.class().to_owned(), msg.into())
1802                })?;
1803            zelf.init(raw, BufferSize { buffer_size }, vm)
1804        }
1805
1806        fn init(
1807            &self,
1808            raw: PyObjectRef,
1809            BufferSize { buffer_size }: BufferSize,
1810            vm: &VirtualMachine,
1811        ) -> PyResult<()> {
1812            let mut data = self.lock(vm)?;
1813            self.raw_cell().store(None);
1814            data.flags.remove(BufferedFlags::DETACHED);
1815
1816            if buffer_size <= 0 {
1817                return Err(vm.new_value_error("buffer size must be strictly positive"));
1818            }
1819            let buffer_size = buffer_size as usize;
1820
1821            if Self::SEEKABLE {
1822                check_seekable(&raw, vm)?;
1823            }
1824            if Self::READABLE {
1825                data.flags.insert(BufferedFlags::READABLE);
1826                check_readable(&raw, vm)?;
1827            }
1828            if Self::WRITABLE {
1829                data.flags.insert(BufferedFlags::WRITABLE);
1830                check_writable(&raw, vm)?;
1831            }
1832
1833            data.buffer = vm.new_zeroed_bytes(buffer_size)?;
1834
1835            if Self::READABLE {
1836                data.reset_read();
1837            }
1838            if Self::WRITABLE {
1839                data.reset_write();
1840            }
1841            if Self::SEEKABLE {
1842                data.pos = 0;
1843            }
1844
1845            self.raw_cell().store(Some(raw));
1846
1847            Ok(())
1848        }
1849
1850        #[pymethod]
1851        fn seek(
1852            zelf: &Py<Self>,
1853            target: PyObjectRef,
1854            whence: WhenceArg,
1855            vm: &VirtualMachine,
1856        ) -> PyResult<Offset> {
1857            let whence = whence.whence;
1858            if !validate_whence(whence) {
1859                return Err(vm.new_value_error(format!("whence value {whence} unsupported")));
1860            }
1861            let mut data = zelf.lock(vm)?;
1862            let raw_obj = data.check_init(zelf.raw_cell(), vm)?;
1863            ensure_unclosed(raw_obj, "seek of closed file", vm)?;
1864            check_seekable(raw_obj, vm)?;
1865            let target = get_offset(&target, vm)?;
1866            data.seek(zelf.raw_cell(), target, whence, vm)
1867        }
1868
1869        #[pymethod]
1870        fn tell(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<Offset> {
1871            let mut data = zelf.lock(vm)?;
1872            let raw_tell = data.raw_tell(zelf.raw_cell(), vm)?;
1873            let raw_offset = data.raw_offset();
1874            let mut pos = raw_tell - raw_offset;
1875            // GH-95782
1876            if pos < 0 {
1877                pos = 0;
1878            }
1879            Ok(pos)
1880        }
1881
1882        #[pymethod]
1883        fn truncate(zelf: PyRef<Self>, pos: ObjPos, vm: &VirtualMachine) -> PyResult {
1884            let pos = pos.pos.unwrap_or_else(|| vm.ctx.none());
1885            let mut data = zelf.lock(vm)?;
1886            let raw_obj = data.check_init(zelf.raw_cell(), vm)?;
1887            if !data.writable() {
1888                return Err(new_unsupported_operation("truncate", vm));
1889            }
1890            data.flush_rewind(zelf.raw_cell(), vm)?;
1891            let res = vm.call_method(raw_obj, "truncate", (pos,))?;
1892            let _ = data.raw_tell(zelf.raw_cell(), vm);
1893            Ok(res)
1894        }
1895        #[pymethod]
1896        fn detach(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult {
1897            vm.call_method(zelf.as_object(), "flush", ())?;
1898            let mut data = zelf.lock(vm)?;
1899            data.flags.insert(BufferedFlags::DETACHED);
1900            zelf.raw_cell()
1901                .store(None)
1902                .ok_or_else(|| vm.new_value_error("raw stream has been detached"))
1903        }
1904
1905        #[pymethod]
1906        fn seekable(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult {
1907            let data = zelf.lock(vm)?;
1908            vm.call_method(data.check_init(zelf.raw_cell(), vm)?, "seekable", ())
1909        }
1910
1911        /// Get raw stream without holding the lock (for calling Python code safely)
1912        fn get_raw_unlocked(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
1913            let data = self.lock(vm)?;
1914            data.raw_owned(self.raw_cell(), vm)
1915        }
1916
1917        // Payload helper. BufferedRWPair calls this on its embedded writer.
1918        fn closed(&self, vm: &VirtualMachine) -> PyResult {
1919            self.get_raw_unlocked(vm)?.get_attr("closed", vm)
1920        }
1921
1922        #[pygetset(name = "closed")]
1923        fn py_closed(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult {
1924            zelf.closed(vm)
1925        }
1926
1927        #[pygetset]
1928        fn name(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult {
1929            zelf.get_raw_unlocked(vm)?.get_attr("name", vm)
1930        }
1931
1932        #[pygetset]
1933        fn mode(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult {
1934            zelf.get_raw_unlocked(vm)?.get_attr("mode", vm)
1935        }
1936
1937        #[pymethod]
1938        fn fileno(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult {
1939            let data = zelf.lock(vm)?;
1940            vm.call_method(data.check_init(zelf.raw_cell(), vm)?, "fileno", ())
1941        }
1942
1943        // Payload helper. BufferedRWPair calls this on its embedded reader and writer.
1944        fn isatty(&self, vm: &VirtualMachine) -> PyResult {
1945            let data = self.lock(vm)?;
1946            vm.call_method(data.check_init(self.raw_cell(), vm)?, "isatty", ())
1947        }
1948
1949        #[pymethod(name = "isatty")]
1950        fn py_isatty(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult {
1951            zelf.isatty(vm)
1952        }
1953
1954        #[pyslot]
1955        fn slot_repr(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyRef<PyStr>> {
1956            let name_repr = repr_file_obj_name(zelf, vm)?;
1957            let cls = zelf.class();
1958            let slot_name = cls.slot_name();
1959            let repr = if let Some(name_repr) = name_repr {
1960                format!("<{slot_name} name={name_repr}>")
1961            } else {
1962                format!("<{slot_name}>")
1963            };
1964            Ok(vm.ctx.new_str(repr))
1965        }
1966
1967        #[pymethod]
1968        fn __repr__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyRef<PyStr>> {
1969            Self::slot_repr(&zelf, vm)
1970        }
1971
1972        fn close_strict(&self, vm: &VirtualMachine) -> PyResult {
1973            let mut data = self.lock(vm)?;
1974            let raw = data.check_init(self.raw_cell(), vm)?;
1975            if file_closed(raw, vm)? {
1976                return Ok(vm.ctx.none());
1977            }
1978            let flush_res = data.flush(self.raw_cell(), vm);
1979            let close_res = vm.call_method(raw, "close", ());
1980            exception_chain(flush_res, close_res)
1981        }
1982
1983        #[pymethod]
1984        fn close(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult {
1985            // Don't hold the lock while calling Python code to avoid reentrant lock issues
1986            let raw = {
1987                let data = zelf.lock(vm)?;
1988                let raw = data.raw_owned(zelf.raw_cell(), vm)?;
1989                if file_closed(&raw, vm)? {
1990                    return Ok(vm.ctx.none());
1991                }
1992                raw
1993            };
1994            if zelf.finalizing().load(Ordering::Relaxed) {
1995                // _dealloc_warn: delegate to raw._dealloc_warn(source)
1996                let _ = vm.call_method(&raw, "_dealloc_warn", (zelf.as_object().to_owned(),));
1997            }
1998            // Set closing flag so that concurrent write() calls will fail
1999            zelf.closing().store(true, Ordering::Release);
2000            let flush_res = vm.call_method(zelf.as_object(), "flush", ()).map(drop);
2001            let close_res = vm.call_method(&raw, "close", ());
2002            exception_chain(flush_res, close_res)
2003        }
2004
2005        #[pymethod]
2006        fn readable(_zelf: &Py<Self>) -> bool {
2007            Self::READABLE
2008        }
2009
2010        #[pymethod]
2011        fn writable(_zelf: &Py<Self>) -> bool {
2012            Self::WRITABLE
2013        }
2014
2015        #[pymethod]
2016        fn __getstate__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
2017            Err(vm.new_type_error(format!("cannot pickle '{}' instances", zelf.class().name())))
2018        }
2019
2020        #[pymethod]
2021        fn __reduce_ex__(zelf: PyObjectRef, proto: usize, vm: &VirtualMachine) -> PyResult {
2022            if zelf.class().is(Self::static_type()) {
2023                return Err(
2024                    vm.new_type_error(format!("cannot pickle '{}' object", zelf.class().name()))
2025                );
2026            }
2027            let _ = proto;
2028            reduce_ex_for_subclass(&zelf, vm)
2029        }
2030
2031        #[pymethod]
2032        fn _dealloc_warn(
2033            zelf: PyRef<Self>,
2034            source: PyObjectRef,
2035            vm: &VirtualMachine,
2036        ) -> PyResult<()> {
2037            // Get raw reference and release lock before calling downstream
2038            let raw = {
2039                let _data = zelf.lock(vm)?;
2040                zelf.raw_cell().load_owned()
2041            };
2042            if let Some(raw) = raw {
2043                let _ = vm.call_method(&raw, "_dealloc_warn", (source,));
2044            }
2045            Ok(())
2046        }
2047    }
2048
2049    #[pyclass]
2050    trait BufferedReadable: PyPayload {
2051        type Reader: BufferedMixin;
2052
2053        fn reader(&self) -> &Self::Reader;
2054
2055        #[pymethod]
2056        fn read(
2057            zelf: &Py<Self>,
2058            size: OptionalSize,
2059            vm: &VirtualMachine,
2060        ) -> PyResult<Option<PyBytesRef>> {
2061            let mut data = zelf.reader().lock(vm)?;
2062            let raw_cell = zelf.reader().raw_cell();
2063            let raw = data.check_init(raw_cell, vm)?;
2064            let n = size.size.unwrap_or(-1);
2065            if n < -1 {
2066                return Err(vm.new_value_error("read length must be non-negative or -1"));
2067            }
2068            ensure_unclosed(raw, "read of closed file", vm)?;
2069            match n.to_usize() {
2070                Some(n) => data
2071                    .read_generic(raw_cell, n, vm)
2072                    .map(|x| x.map(|b| PyBytes::from(b).into_ref(&vm.ctx))),
2073                None => data.read_all(raw_cell, vm),
2074            }
2075        }
2076
2077        #[pymethod]
2078        fn peek(zelf: &Py<Self>, _size: PeekSize, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
2079            let mut data = zelf.reader().lock(vm)?;
2080            let raw_cell = zelf.reader().raw_cell();
2081            let raw = data.check_init(raw_cell, vm)?;
2082            ensure_unclosed(raw, "peek of closed file", vm)?;
2083
2084            if data.writable() {
2085                let _ = data.flush_rewind(raw_cell, vm);
2086            }
2087            data.peek(raw_cell, vm)
2088        }
2089
2090        #[pymethod]
2091        fn read1(zelf: &Py<Self>, size: OptionalSize, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
2092            let mut data = zelf.reader().lock(vm)?;
2093            let raw_cell = zelf.reader().raw_cell();
2094            let raw = data.check_init(raw_cell, vm)?;
2095            ensure_unclosed(raw, "read of closed file", vm)?;
2096            let n = size.to_usize().unwrap_or(data.buffer.len());
2097            if n == 0 {
2098                return Ok(Vec::new());
2099            }
2100            let have = data.readahead();
2101            if have > 0 {
2102                let n = core::cmp::min(have as usize, n);
2103                return Ok(data.read_fast(n).unwrap());
2104            }
2105            // Flush write buffer before reading
2106            if data.writable() {
2107                data.flush_rewind(raw_cell, vm)?;
2108            }
2109            let mut v = vm.new_zeroed_bytes(n)?;
2110            data.reset_read();
2111            let r = data
2112                .raw_read(raw_cell, Either::A(Some(&mut v)), 0..n, vm)?
2113                .unwrap_or(0);
2114            v.truncate(r);
2115            v.shrink_to_fit();
2116            Ok(v)
2117        }
2118
2119        #[pymethod]
2120        fn readinto(
2121            zelf: &Py<Self>,
2122            buffer: ArgMemoryBuffer,
2123            vm: &VirtualMachine,
2124        ) -> PyResult<Option<usize>> {
2125            let mut data = zelf.reader().lock(vm)?;
2126            let raw_cell = zelf.reader().raw_cell();
2127            let raw = data.check_init(raw_cell, vm)?;
2128            ensure_unclosed(raw, "readinto of closed file", vm)?;
2129            data.readinto_generic(raw_cell, buffer.into(), false, vm)
2130        }
2131
2132        #[pymethod]
2133        fn readinto1(
2134            zelf: &Py<Self>,
2135            buffer: ArgMemoryBuffer,
2136            vm: &VirtualMachine,
2137        ) -> PyResult<Option<usize>> {
2138            let mut data = zelf.reader().lock(vm)?;
2139            let raw_cell = zelf.reader().raw_cell();
2140            let raw = data.check_init(raw_cell, vm)?;
2141            ensure_unclosed(raw, "readinto of closed file", vm)?;
2142            data.readinto_generic(raw_cell, buffer.into(), true, vm)
2143        }
2144
2145        #[pymethod]
2146        fn flush(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<()> {
2147            // For read-only buffers, flush just calls raw.flush()
2148            // Don't hold the lock while calling Python code to avoid reentrant lock issues
2149            let raw = {
2150                let data = zelf.reader().lock(vm)?;
2151                data.raw_owned(zelf.reader().raw_cell(), vm)?
2152            };
2153            ensure_unclosed(&raw, "flush of closed file", vm)?;
2154            vm.call_method(&raw, "flush", ())?;
2155            Ok(())
2156        }
2157    }
2158
2159    fn exception_chain<T>(e1: PyResult<()>, e2: PyResult<T>) -> PyResult<T> {
2160        match (e1, e2) {
2161            (Err(e1), Err(e)) => {
2162                e.set_context(Some(e1));
2163                Err(e)
2164            }
2165            (Err(e), Ok(_)) | (Ok(()), Err(e)) => Err(e),
2166            (Ok(()), Ok(close_res)) => Ok(close_res),
2167        }
2168    }
2169
2170    #[pyattr]
2171    #[pyclass(name = "BufferedReader", base = _BufferedIOBase)]
2172    #[derive(Debug)]
2173    struct BufferedReader {
2174        _base: _BufferedIOBase,
2175        #[pymember]
2176        raw: RawCell,
2177        data: PyThreadMutex<BufferedData>,
2178        closing: AtomicBool,
2179        finalizing: AtomicBool,
2180    }
2181
2182    impl Default for BufferedReader {
2183        fn default() -> Self {
2184            Self {
2185                _base: _BufferedIOBase::default(),
2186                raw: RawCell::new_empty(),
2187                data: PyThreadMutex::default(),
2188                closing: AtomicBool::new(false),
2189                finalizing: AtomicBool::new(false),
2190            }
2191        }
2192    }
2193
2194    impl BufferedMixin for BufferedReader {
2195        const CLASS_NAME: &'static str = "BufferedReader";
2196        const READABLE: bool = true;
2197        const WRITABLE: bool = false;
2198
2199        fn data(&self) -> &PyThreadMutex<BufferedData> {
2200            &self.data
2201        }
2202
2203        fn raw_cell(&self) -> &RawCell {
2204            &self.raw
2205        }
2206
2207        fn closing(&self) -> &AtomicBool {
2208            &self.closing
2209        }
2210
2211        fn finalizing(&self) -> &AtomicBool {
2212            &self.finalizing
2213        }
2214    }
2215
2216    impl BufferedReadable for BufferedReader {
2217        type Reader = Self;
2218
2219        fn reader(&self) -> &Self::Reader {
2220            self
2221        }
2222    }
2223
2224    #[pyclass(
2225        with(Constructor, Initializer, BufferedMixin, BufferedReadable, Destructor),
2226        flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
2227    )]
2228    impl BufferedReader {}
2229
2230    impl Destructor for BufferedReader {
2231        fn slot_del(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
2232            if let Some(buf) = zelf.downcast_ref::<Self>() {
2233                buf.finalizing.store(true, Ordering::Relaxed);
2234            }
2235            iobase_finalize(zelf, vm);
2236            Ok(())
2237        }
2238
2239        #[cold]
2240        fn del(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<()> {
2241            unreachable!("slot_del is implemented")
2242        }
2243    }
2244
2245    impl DefaultConstructor for BufferedReader {}
2246
2247    impl Initializer for BufferedReader {
2248        type Args = BufferedInitArgs;
2249
2250        fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
2251            zelf.init(
2252                args.raw,
2253                BufferSize {
2254                    buffer_size: args.buffer_size,
2255                },
2256                vm,
2257            )
2258        }
2259    }
2260
2261    #[pyclass]
2262    trait BufferedWritable: PyPayload {
2263        type Writer: BufferedMixin;
2264
2265        fn writer(&self) -> &Self::Writer;
2266
2267        #[pymethod]
2268        fn write(zelf: &Py<Self>, buffer: ArgBytesLike, vm: &VirtualMachine) -> PyResult<usize> {
2269            // Check if close() is in progress (Issue #31976)
2270            // If closing, wait for close() to complete by spinning until raw is closed.
2271            // Note: This spin-wait has no timeout because close() is expected to always
2272            // complete (flush + fd close).
2273            if zelf.writer().closing().load(Ordering::Acquire) {
2274                loop {
2275                    let raw = {
2276                        let _data = zelf.writer().lock(vm)?;
2277                        match zelf.writer().raw_cell().load_owned() {
2278                            Some(raw) => raw,
2279                            None => break, // detached
2280                        }
2281                    };
2282                    if file_closed(&raw, vm)? {
2283                        break;
2284                    }
2285                    // Yield to other threads
2286                    std::thread::yield_now();
2287                }
2288                return Err(vm.new_value_error("write to closed file"));
2289            }
2290            let mut data = zelf.writer().lock(vm)?;
2291            let raw_cell = zelf.writer().raw_cell();
2292            let raw = data.check_init(raw_cell, vm)?;
2293            ensure_unclosed(raw, "write to closed file", vm)?;
2294
2295            data.write(raw_cell, buffer, vm)
2296        }
2297
2298        // Payload helper. BufferedRWPair calls this on its embedded writer.
2299        fn flush(&self, vm: &VirtualMachine) -> PyResult<()> {
2300            let mut data = self.writer().lock(vm)?;
2301            let raw_cell = self.writer().raw_cell();
2302            let raw = data.check_init(raw_cell, vm)?;
2303            ensure_unclosed(raw, "flush of closed file", vm)?;
2304            data.flush_rewind(raw_cell, vm)
2305        }
2306
2307        #[pymethod(name = "flush")]
2308        fn py_flush(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<()> {
2309            zelf.flush(vm)
2310        }
2311    }
2312
2313    #[pyattr]
2314    #[pyclass(name = "BufferedWriter", base = _BufferedIOBase)]
2315    #[derive(Debug)]
2316    struct BufferedWriter {
2317        _base: _BufferedIOBase,
2318        #[pymember]
2319        raw: RawCell,
2320        data: PyThreadMutex<BufferedData>,
2321        closing: AtomicBool,
2322        finalizing: AtomicBool,
2323    }
2324
2325    impl Default for BufferedWriter {
2326        fn default() -> Self {
2327            Self {
2328                _base: _BufferedIOBase::default(),
2329                raw: RawCell::new_empty(),
2330                data: PyThreadMutex::default(),
2331                closing: AtomicBool::new(false),
2332                finalizing: AtomicBool::new(false),
2333            }
2334        }
2335    }
2336
2337    impl BufferedMixin for BufferedWriter {
2338        const CLASS_NAME: &'static str = "BufferedWriter";
2339        const READABLE: bool = false;
2340        const WRITABLE: bool = true;
2341
2342        fn data(&self) -> &PyThreadMutex<BufferedData> {
2343            &self.data
2344        }
2345
2346        fn raw_cell(&self) -> &RawCell {
2347            &self.raw
2348        }
2349
2350        fn closing(&self) -> &AtomicBool {
2351            &self.closing
2352        }
2353
2354        fn finalizing(&self) -> &AtomicBool {
2355            &self.finalizing
2356        }
2357    }
2358
2359    impl BufferedWritable for BufferedWriter {
2360        type Writer = Self;
2361
2362        fn writer(&self) -> &Self::Writer {
2363            self
2364        }
2365    }
2366
2367    #[pyclass(
2368        with(Constructor, Initializer, BufferedMixin, BufferedWritable, Destructor),
2369        flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
2370    )]
2371    impl BufferedWriter {}
2372
2373    impl Destructor for BufferedWriter {
2374        fn slot_del(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
2375            if let Some(buf) = zelf.downcast_ref::<Self>() {
2376                buf.finalizing.store(true, Ordering::Relaxed);
2377            }
2378            iobase_finalize(zelf, vm);
2379            Ok(())
2380        }
2381
2382        #[cold]
2383        fn del(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<()> {
2384            unreachable!("slot_del is implemented")
2385        }
2386    }
2387
2388    impl DefaultConstructor for BufferedWriter {}
2389
2390    impl Initializer for BufferedWriter {
2391        type Args = BufferedInitArgs;
2392
2393        fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
2394            zelf.init(
2395                args.raw,
2396                BufferSize {
2397                    buffer_size: args.buffer_size,
2398                },
2399                vm,
2400            )
2401        }
2402    }
2403
2404    #[pyattr]
2405    #[pyclass(name = "BufferedRandom", base = _BufferedIOBase)]
2406    #[derive(Debug)]
2407    struct BufferedRandom {
2408        _base: _BufferedIOBase,
2409        #[pymember]
2410        raw: RawCell,
2411        data: PyThreadMutex<BufferedData>,
2412        closing: AtomicBool,
2413        finalizing: AtomicBool,
2414    }
2415
2416    impl Default for BufferedRandom {
2417        fn default() -> Self {
2418            Self {
2419                _base: _BufferedIOBase::default(),
2420                raw: RawCell::new_empty(),
2421                data: PyThreadMutex::default(),
2422                closing: AtomicBool::new(false),
2423                finalizing: AtomicBool::new(false),
2424            }
2425        }
2426    }
2427
2428    impl BufferedMixin for BufferedRandom {
2429        const CLASS_NAME: &'static str = "BufferedRandom";
2430        const READABLE: bool = true;
2431        const WRITABLE: bool = true;
2432        const SEEKABLE: bool = true;
2433
2434        fn data(&self) -> &PyThreadMutex<BufferedData> {
2435            &self.data
2436        }
2437
2438        fn raw_cell(&self) -> &RawCell {
2439            &self.raw
2440        }
2441
2442        fn closing(&self) -> &AtomicBool {
2443            &self.closing
2444        }
2445
2446        fn finalizing(&self) -> &AtomicBool {
2447            &self.finalizing
2448        }
2449    }
2450
2451    impl BufferedReadable for BufferedRandom {
2452        type Reader = Self;
2453
2454        fn reader(&self) -> &Self::Reader {
2455            self
2456        }
2457    }
2458
2459    impl BufferedWritable for BufferedRandom {
2460        type Writer = Self;
2461
2462        fn writer(&self) -> &Self::Writer {
2463            self
2464        }
2465    }
2466
2467    #[pyclass(
2468        with(
2469            Constructor,
2470            Initializer,
2471            BufferedMixin,
2472            BufferedReadable,
2473            BufferedWritable,
2474            Destructor
2475        ),
2476        flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
2477    )]
2478    impl BufferedRandom {}
2479
2480    impl Destructor for BufferedRandom {
2481        fn slot_del(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
2482            if let Some(buf) = zelf.downcast_ref::<Self>() {
2483                buf.finalizing.store(true, Ordering::Relaxed);
2484            }
2485            iobase_finalize(zelf, vm);
2486            Ok(())
2487        }
2488
2489        #[cold]
2490        fn del(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<()> {
2491            unreachable!("slot_del is implemented")
2492        }
2493    }
2494
2495    impl DefaultConstructor for BufferedRandom {}
2496
2497    impl Initializer for BufferedRandom {
2498        type Args = BufferedInitArgs;
2499
2500        fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
2501            zelf.init(
2502                args.raw,
2503                BufferSize {
2504                    buffer_size: args.buffer_size,
2505                },
2506                vm,
2507            )
2508        }
2509    }
2510
2511    #[pyattr]
2512    #[pyclass(name = "BufferedRWPair", base = _BufferedIOBase)]
2513    #[derive(Debug, Default)]
2514    struct BufferedRWPair {
2515        _base: _BufferedIOBase,
2516        read: BufferedReader,
2517        write: BufferedWriter,
2518    }
2519
2520    impl BufferedReadable for BufferedRWPair {
2521        type Reader = BufferedReader;
2522
2523        fn reader(&self) -> &Self::Reader {
2524            &self.read
2525        }
2526    }
2527
2528    impl BufferedWritable for BufferedRWPair {
2529        type Writer = BufferedWriter;
2530
2531        fn writer(&self) -> &Self::Writer {
2532            &self.write
2533        }
2534    }
2535
2536    impl DefaultConstructor for BufferedRWPair {}
2537
2538    impl Initializer for BufferedRWPair {
2539        type Args = BufferedRWPairArgs;
2540
2541        fn init(
2542            zelf: &Py<Self>,
2543            BufferedRWPairArgs {
2544                reader,
2545                writer,
2546                buffer_size,
2547            }: Self::Args,
2548            vm: &VirtualMachine,
2549        ) -> PyResult<()> {
2550            let size = BufferSize { buffer_size };
2551            zelf.read.init(reader, size.clone(), vm)?;
2552            zelf.write.init(writer, size, vm)?;
2553            Ok(())
2554        }
2555    }
2556
2557    #[pyclass(
2558        with(
2559            Constructor,
2560            Initializer,
2561            BufferedReadable,
2562            BufferedWritable,
2563            Destructor
2564        ),
2565        flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
2566    )]
2567    impl Py<BufferedRWPair> {
2568        #[pymethod]
2569        fn flush(&self, vm: &VirtualMachine) -> PyResult<()> {
2570            self.write.flush(vm)
2571        }
2572
2573        #[pymethod]
2574        const fn readable(&self) -> bool {
2575            true
2576        }
2577        #[pymethod]
2578        const fn writable(&self) -> bool {
2579            true
2580        }
2581
2582        #[pygetset]
2583        fn closed(&self, vm: &VirtualMachine) -> PyResult {
2584            self.write.closed(vm)
2585        }
2586
2587        #[pymethod]
2588        fn isatty(&self, vm: &VirtualMachine) -> PyResult {
2589            // read.isatty() or write.isatty()
2590            let res = self.read.isatty(vm)?;
2591            if res.try_to_bool(vm)? {
2592                Ok(res)
2593            } else {
2594                self.write.isatty(vm)
2595            }
2596        }
2597
2598        #[pymethod]
2599        fn close(&self, vm: &VirtualMachine) -> PyResult {
2600            let write_res = self.write.close_strict(vm).map(drop);
2601            let read_res = self.read.close_strict(vm);
2602            exception_chain(write_res, read_res)
2603        }
2604    }
2605
2606    impl Destructor for BufferedRWPair {
2607        fn slot_del(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
2608            iobase_finalize(zelf, vm);
2609            Ok(())
2610        }
2611
2612        #[cold]
2613        fn del(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<()> {
2614            unreachable!("slot_del is implemented")
2615        }
2616    }
2617
2618    #[derive(FromArgs)]
2619    struct TextIOWrapperArgs {
2620        #[pyarg(any, optional)]
2621        encoding: Option<PyUtf8StrRef>,
2622        #[pyarg(any, optional)]
2623        errors: Option<PyUtf8StrRef>,
2624        // None means universal newlines.
2625        #[pyarg(any, optional)]
2626        newline: Option<Newlines>,
2627        // None is false.
2628        #[pyarg(any, optional, py_default = "False")]
2629        line_buffering: Option<PyObjectRef>,
2630        // None is false.
2631        #[pyarg(any, optional, py_default = "False")]
2632        write_through: Option<PyObjectRef>,
2633    }
2634
2635    #[derive(FromArgs)]
2636    struct TextIOWrapperReconfigureArgs {
2637        #[pyarg(named, optional)]
2638        encoding: Option<PyUtf8StrRef>,
2639        #[pyarg(named, optional)]
2640        errors: Option<PyUtf8StrRef>,
2641        // Omitted leaves newline unchanged. None selects universal newlines.
2642        #[pyarg(named, optional, py_default = "None")]
2643        newline: OptionalOption<Newlines>,
2644        #[pyarg(named, optional)]
2645        line_buffering: Option<PyObjectRef>,
2646        #[pyarg(named, optional)]
2647        write_through: Option<PyObjectRef>,
2648    }
2649
2650    #[derive(Debug, Copy, Clone, Default, PartialEq)]
2651    enum Newlines {
2652        #[default]
2653        Universal,
2654        Passthrough,
2655        Lf,
2656        Cr,
2657        Crlf,
2658    }
2659
2660    impl Newlines {
2661        /// returns position where the new line starts if found, otherwise position at which to
2662        /// continue the search after more is read into the buffer
2663        fn find_newline(self, s: &Wtf8) -> Result<usize, usize> {
2664            let len = s.len();
2665            match self {
2666                Self::Universal | Self::Lf => s.find("\n".as_ref()).map(|p| p + 1).ok_or(len),
2667                Self::Passthrough => {
2668                    let bytes = s.as_bytes();
2669                    memchr::memchr2(b'\n', b'\r', bytes)
2670                        .map(|p| {
2671                            let nl_len =
2672                                if bytes[p] == b'\r' && bytes.get(p + 1).copied() == Some(b'\n') {
2673                                    2
2674                                } else {
2675                                    1
2676                                };
2677                            p + nl_len
2678                        })
2679                        .ok_or(len)
2680                }
2681                Self::Cr => s.find("\r".as_ref()).map(|p| p + 1).ok_or(len),
2682                Self::Crlf => {
2683                    // s[searched..] == remaining
2684                    let mut searched = 0;
2685                    let mut remaining = s.as_bytes();
2686                    loop {
2687                        match memchr::memchr(b'\r', remaining) {
2688                            Some(p) => match remaining.get(p + 1) {
2689                                Some(&ch_after_cr) => {
2690                                    if ch_after_cr == b'\n' {
2691                                        break Ok(searched + p + 2);
2692                                    }
2693                                    let pos_after = p + 1;
2694                                    searched += pos_after;
2695                                    remaining = &remaining[pos_after..];
2696                                    continue;
2697                                }
2698                                None => break Err(searched + p),
2699                            },
2700                            None => break Err(len),
2701                        }
2702                    }
2703                }
2704            }
2705        }
2706    }
2707
2708    impl TryFromObject for Newlines {
2709        fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
2710            let nl = if vm.is_none(&obj) {
2711                Self::Universal
2712            } else {
2713                let s = obj.downcast::<PyStr>().map_err(|obj| {
2714                    vm.new_type_error(format!(
2715                        "newline argument must be str or None, not {}",
2716                        obj.class().name()
2717                    ))
2718                })?;
2719                let wtf8 = s.as_wtf8();
2720                if !wtf8.is_utf8() {
2721                    let repr = s.repr(vm)?.as_str().to_owned();
2722                    return Err(vm.new_value_error(format!("illegal newline value: {repr}")));
2723                }
2724                let s_str = wtf8.as_str().expect("checked utf8");
2725                match s_str {
2726                    "" => Self::Passthrough,
2727                    "\n" => Self::Lf,
2728                    "\r" => Self::Cr,
2729                    "\r\n" => Self::Crlf,
2730                    _ => return Err(vm.new_value_error(format!("illegal newline value: {s}"))),
2731                }
2732            };
2733            Ok(nl)
2734        }
2735    }
2736
2737    fn reduce_ex_for_subclass(zelf: &PyObject, vm: &VirtualMachine) -> PyResult {
2738        let cls = zelf.class();
2739        let new = vm
2740            .get_attribute_opt(cls.as_object(), "__new__")?
2741            .ok_or_else(|| vm.new_attribute_error("type has no attribute '__new__'"))?;
2742        let args = vm.ctx.new_tuple(vec![cls.to_owned().into()]);
2743        let state = if let Some(getstate) = vm.get_attribute_opt(zelf, "__getstate__")? {
2744            getstate.call((), vm)?
2745        } else if let Ok(dict) = zelf.get_attr("__dict__", vm) {
2746            dict
2747        } else {
2748            vm.ctx.none()
2749        };
2750        Ok(vm.ctx.new_tuple(vec![new, args.into(), state]).into())
2751    }
2752
2753    /// A length of or index into a UTF-8 string, measured in both chars and bytes
2754    #[derive(Debug, Default, Copy, Clone)]
2755    struct Utf8size {
2756        bytes: usize,
2757        chars: usize,
2758    }
2759
2760    impl Utf8size {
2761        fn len_pystr(s: &Py<PyStr>) -> Self {
2762            Self {
2763                bytes: s.byte_len(),
2764                chars: s.char_len(),
2765            }
2766        }
2767
2768        fn len_str(s: &Wtf8) -> Self {
2769            Self {
2770                bytes: s.len(),
2771                chars: s.code_points().count(),
2772            }
2773        }
2774    }
2775
2776    impl core::ops::Add for Utf8size {
2777        type Output = Self;
2778
2779        #[inline]
2780        fn add(mut self, rhs: Self) -> Self {
2781            self += rhs;
2782            self
2783        }
2784    }
2785
2786    impl core::ops::AddAssign for Utf8size {
2787        #[inline]
2788        fn add_assign(&mut self, rhs: Self) {
2789            self.bytes += rhs.bytes;
2790            self.chars += rhs.chars;
2791        }
2792    }
2793
2794    impl core::ops::Sub for Utf8size {
2795        type Output = Self;
2796
2797        #[inline]
2798        fn sub(mut self, rhs: Self) -> Self {
2799            self -= rhs;
2800            self
2801        }
2802    }
2803
2804    impl core::ops::SubAssign for Utf8size {
2805        #[inline]
2806        fn sub_assign(&mut self, rhs: Self) {
2807            self.bytes -= rhs.bytes;
2808            self.chars -= rhs.chars;
2809        }
2810    }
2811
2812    // TODO: implement legit fast-paths for other encodings
2813    type EncodeFunc = fn(PyStrRef) -> PendingWrite;
2814    const fn textio_encode_utf8(s: PyStrRef) -> PendingWrite {
2815        PendingWrite::Utf8(s)
2816    }
2817
2818    #[derive(Debug)]
2819    struct TextIOData {
2820        encoder: Option<(PyObjectRef, Option<EncodeFunc>)>,
2821        decoder: Option<PyObjectRef>,
2822        errors: PyUtf8StrRef,
2823        newline: Newlines,
2824        chunk_size: usize,
2825        seekable: bool,
2826        has_read1: bool,
2827        // these are more state than configuration
2828        pending: PendingWrites,
2829        telling: bool,
2830        snapshot: Option<(i32, PyBytesRef)>,
2831        decoded_chars: Option<PyStrRef>,
2832        // number of characters we've consumed from decoded_chars
2833        decoded_chars_used: Utf8size,
2834        b2cratio: f64,
2835    }
2836
2837    #[derive(Debug, Default)]
2838    struct PendingWrites {
2839        num_bytes: usize,
2840        data: PendingWritesData,
2841    }
2842
2843    #[derive(Debug, Default)]
2844    enum PendingWritesData {
2845        #[default]
2846        None,
2847        One(PendingWrite),
2848        Many(Vec<PendingWrite>),
2849    }
2850
2851    #[derive(Debug)]
2852    enum PendingWrite {
2853        Utf8(PyStrRef),
2854        Bytes(PyBytesRef),
2855    }
2856
2857    impl PendingWrite {
2858        fn as_bytes(&self) -> &[u8] {
2859            match self {
2860                Self::Utf8(s) => s.as_bytes(),
2861                Self::Bytes(b) => b.as_bytes(),
2862            }
2863        }
2864    }
2865
2866    impl PendingWrites {
2867        fn push(&mut self, write: PendingWrite) {
2868            self.num_bytes += write.as_bytes().len();
2869            self.data = match core::mem::take(&mut self.data) {
2870                PendingWritesData::None => PendingWritesData::One(write),
2871                PendingWritesData::One(write1) => PendingWritesData::Many(vec![write1, write]),
2872                PendingWritesData::Many(mut v) => {
2873                    v.push(write);
2874                    PendingWritesData::Many(v)
2875                }
2876            }
2877        }
2878        fn take(&mut self, vm: &VirtualMachine) -> PyBytesRef {
2879            let Self { num_bytes, data } = core::mem::take(self);
2880            if let PendingWritesData::One(PendingWrite::Bytes(b)) = data {
2881                return b;
2882            }
2883            let writes_iter = match data {
2884                PendingWritesData::None => itertools::Either::Left(vec![].into_iter()),
2885                PendingWritesData::One(write) => itertools::Either::Right(core::iter::once(write)),
2886                PendingWritesData::Many(writes) => itertools::Either::Left(writes.into_iter()),
2887            };
2888            let mut buf = Vec::with_capacity(num_bytes);
2889            writes_iter.for_each(|chunk| buf.extend_from_slice(chunk.as_bytes()));
2890            PyBytes::from(buf).into_ref(&vm.ctx)
2891        }
2892    }
2893
2894    #[derive(Default, Debug)]
2895    struct TextIOCookie {
2896        start_pos: Offset,
2897        dec_flags: i32,
2898        bytes_to_feed: i32,
2899        chars_to_skip: i32,
2900        need_eof: bool,
2901        // chars_to_skip but utf8 bytes
2902        bytes_to_skip: i32,
2903    }
2904
2905    impl TextIOCookie {
2906        const START_POS_OFF: usize = 0;
2907        const DEC_FLAGS_OFF: usize = Self::START_POS_OFF + core::mem::size_of::<Offset>();
2908        const BYTES_TO_FEED_OFF: usize = Self::DEC_FLAGS_OFF + 4;
2909        const CHARS_TO_SKIP_OFF: usize = Self::BYTES_TO_FEED_OFF + 4;
2910        const NEED_EOF_OFF: usize = Self::CHARS_TO_SKIP_OFF + 4;
2911        const BYTES_TO_SKIP_OFF: usize = Self::NEED_EOF_OFF + 1;
2912        const BYTE_LEN: usize = Self::BYTES_TO_SKIP_OFF + 4;
2913
2914        fn parse(cookie: &BigInt) -> Option<Self> {
2915            let (_, mut buf) = cookie.to_bytes_le();
2916            if buf.len() > Self::BYTE_LEN {
2917                return None;
2918            }
2919            buf.resize(Self::BYTE_LEN, 0);
2920            let buf: &[u8; Self::BYTE_LEN] = buf.as_array()?;
2921            macro_rules! get_field {
2922                ($t:ty, $off:ident) => {
2923                    <$t>::from_ne_bytes(*buf[Self::$off..].first_chunk().unwrap())
2924                };
2925            }
2926            Some(Self {
2927                start_pos: get_field!(Offset, START_POS_OFF),
2928                dec_flags: get_field!(i32, DEC_FLAGS_OFF),
2929                bytes_to_feed: get_field!(i32, BYTES_TO_FEED_OFF),
2930                chars_to_skip: get_field!(i32, CHARS_TO_SKIP_OFF),
2931                need_eof: get_field!(u8, NEED_EOF_OFF) != 0,
2932                bytes_to_skip: get_field!(i32, BYTES_TO_SKIP_OFF),
2933            })
2934        }
2935
2936        fn build(&self) -> BigInt {
2937            let mut buf = [0; Self::BYTE_LEN];
2938            macro_rules! set_field {
2939                ($field:expr, $off:ident) => {{
2940                    let field = $field;
2941                    buf[Self::$off..][..core::mem::size_of_val(&field)]
2942                        .copy_from_slice(&field.to_ne_bytes())
2943                }};
2944            }
2945            set_field!(self.start_pos, START_POS_OFF);
2946            set_field!(self.dec_flags, DEC_FLAGS_OFF);
2947            set_field!(self.bytes_to_feed, BYTES_TO_FEED_OFF);
2948            set_field!(self.chars_to_skip, CHARS_TO_SKIP_OFF);
2949            set_field!(self.need_eof as u8, NEED_EOF_OFF);
2950            set_field!(self.bytes_to_skip, BYTES_TO_SKIP_OFF);
2951            BigInt::from_signed_bytes_le(&buf)
2952        }
2953
2954        fn set_decoder_state(&self, decoder: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
2955            if self.start_pos == 0 && self.dec_flags == 0 {
2956                vm.call_method(decoder, "reset", ())?;
2957            } else {
2958                vm.call_method(
2959                    decoder,
2960                    "setstate",
2961                    ((vm.ctx.new_bytes(vec![]), self.dec_flags),),
2962                )?;
2963            }
2964            Ok(())
2965        }
2966
2967        const fn num_to_skip(&self) -> Utf8size {
2968            Utf8size {
2969                bytes: self.bytes_to_skip as usize,
2970                chars: self.chars_to_skip as usize,
2971            }
2972        }
2973
2974        const fn set_num_to_skip(&mut self, num: Utf8size) {
2975            self.bytes_to_skip = num.bytes as i32;
2976            self.chars_to_skip = num.chars as i32;
2977        }
2978    }
2979
2980    #[pyclass(module = "_io", name, no_attr)]
2981    #[derive(Debug, PyPayload)]
2982    struct StatelessIncrementalEncoder {
2983        encode: PyObjectRef,
2984        errors: Option<PyStrRef>,
2985        name: Option<PyStrRef>,
2986    }
2987
2988    #[pyclass(with(Py))]
2989    impl StatelessIncrementalEncoder {}
2990
2991    #[pyclass]
2992    impl Py<StatelessIncrementalEncoder> {
2993        #[pymethod]
2994        fn encode(
2995            &self,
2996            input: PyObjectRef,
2997            _final: OptionalArg<bool>,
2998            vm: &VirtualMachine,
2999        ) -> PyResult {
3000            let mut args: Vec<PyObjectRef> = vec![input];
3001            if let Some(errors) = &self.errors {
3002                args.push(errors.to_owned().into());
3003            }
3004            let res = self.encode.call(args, vm)?;
3005            let tuple: PyTupleRef = res.try_into_value(vm)?;
3006            if tuple.as_slice().len() != 2 {
3007                return Err(vm.new_type_error("encoder must return a tuple (object, integer)"));
3008            }
3009            Ok(tuple.as_slice()[0].clone())
3010        }
3011
3012        #[pymethod]
3013        fn reset(&self) {}
3014
3015        #[pymethod]
3016        fn setstate(&self, _state: PyObjectRef) {}
3017
3018        #[pymethod]
3019        fn getstate(&self, vm: &VirtualMachine) -> PyObjectRef {
3020            vm.ctx.new_int(0).into()
3021        }
3022
3023        #[pygetset]
3024        fn name(&self) -> Option<PyStrRef> {
3025            self.name.clone()
3026        }
3027    }
3028
3029    #[pyclass(module = "_io", name, no_attr)]
3030    #[derive(Debug, PyPayload)]
3031    struct StatelessIncrementalDecoder {
3032        decode: PyObjectRef,
3033        errors: Option<PyStrRef>,
3034    }
3035
3036    #[pyclass(with(Py))]
3037    impl StatelessIncrementalDecoder {}
3038
3039    #[pyclass]
3040    impl Py<StatelessIncrementalDecoder> {
3041        #[pymethod]
3042        fn decode(
3043            &self,
3044            input: PyObjectRef,
3045            _final: OptionalArg<bool>,
3046            vm: &VirtualMachine,
3047        ) -> PyResult {
3048            let mut args: Vec<PyObjectRef> = vec![input];
3049            if let Some(errors) = &self.errors {
3050                args.push(errors.to_owned().into());
3051            }
3052            let res = self.decode.call(args, vm)?;
3053            let tuple: PyTupleRef = res.try_into_value(vm)?;
3054            if tuple.as_slice().len() != 2 {
3055                return Err(vm.new_type_error("decoder must return a tuple (object, integer)"));
3056            }
3057            Ok(tuple.as_slice()[0].clone())
3058        }
3059
3060        #[pymethod]
3061        fn getstate(&self, vm: &VirtualMachine) -> (PyBytesRef, u64) {
3062            (vm.ctx.empty_bytes.to_owned(), 0)
3063        }
3064
3065        #[pymethod]
3066        fn setstate(&self, _state: PyTupleRef, _vm: &VirtualMachine) {}
3067
3068        #[pymethod]
3069        fn reset(&self) {}
3070    }
3071
3072    #[pyattr]
3073    #[pyclass(name = "TextIOWrapper", base = _TextIOBase)]
3074    #[derive(Debug)]
3075    struct TextIOWrapper {
3076        _base: _TextIOBase,
3077        #[pymember]
3078        buffer: RawCell,
3079        #[pymember]
3080        encoding: RawCell,
3081        #[pymember]
3082        line_buffering: AtomicBool,
3083        #[pymember]
3084        write_through: AtomicBool,
3085        data: PyThreadMutex<Option<TextIOData>>,
3086        finalizing: AtomicBool,
3087    }
3088
3089    impl Default for TextIOWrapper {
3090        fn default() -> Self {
3091            Self {
3092                _base: _TextIOBase::default(),
3093                buffer: RawCell::new_empty(),
3094                encoding: RawCell::new_empty(),
3095                line_buffering: AtomicBool::new(false),
3096                write_through: AtomicBool::new(false),
3097                data: PyThreadMutex::default(),
3098                finalizing: AtomicBool::new(false),
3099            }
3100        }
3101    }
3102
3103    impl DefaultConstructor for TextIOWrapper {}
3104
3105    #[derive(FromArgs)]
3106    struct TextIOWrapperInitArgs {
3107        #[pyarg(any)]
3108        buffer: PyObjectRef,
3109        #[pyarg(flatten)]
3110        args: TextIOWrapperArgs,
3111    }
3112
3113    impl Initializer for TextIOWrapper {
3114        type Args = TextIOWrapperInitArgs;
3115
3116        fn init(
3117            zelf: &Py<Self>,
3118            TextIOWrapperInitArgs { buffer, args }: Self::Args,
3119            vm: &VirtualMachine,
3120        ) -> PyResult<()> {
3121            zelf.buffer.store(None);
3122            zelf.encoding.store(None);
3123            zelf.line_buffering.store(false, Ordering::Relaxed);
3124            zelf.write_through.store(false, Ordering::Relaxed);
3125            let mut data = zelf.lock_opt(vm)?;
3126            *data = None;
3127
3128            let encoding = Self::resolve_encoding(args.encoding, vm)?;
3129
3130            let errors = args.errors.unwrap_or_else(|| vm.ctx.new_utf8_str("strict"));
3131            Self::validate_errors(&errors, vm)?;
3132
3133            let has_read1 = vm.get_attribute_opt(&buffer, "read1")?.is_some();
3134            let seekable = vm.call_method(&buffer, "seekable", ())?.try_to_bool(vm)?;
3135
3136            let newline = args.newline.unwrap_or_default();
3137            let (encoder, decoder) =
3138                Self::find_coder(&buffer, encoding.as_str(), &errors, newline, vm)?;
3139            if let Some((encoder, _)) = &encoder {
3140                Self::adjust_encoder_state_for_bom(encoder, encoding.as_str(), &buffer, vm)?;
3141            }
3142
3143            let line_buffering = match args.line_buffering {
3144                Some(value) => value.try_to_bool(vm)?,
3145                None => false,
3146            };
3147            let write_through = match args.write_through {
3148                Some(value) => value.try_to_bool(vm)?,
3149                None => false,
3150            };
3151
3152            zelf.buffer.store(Some(buffer));
3153            zelf.encoding.store(Some(encoding.into()));
3154            zelf.line_buffering.store(line_buffering, Ordering::Relaxed);
3155            zelf.write_through.store(write_through, Ordering::Relaxed);
3156
3157            *data = Some(TextIOData {
3158                encoder,
3159                decoder,
3160                errors,
3161                newline,
3162                chunk_size: 8192,
3163                seekable,
3164                has_read1,
3165
3166                pending: PendingWrites::default(),
3167                telling: seekable,
3168                snapshot: None,
3169                decoded_chars: None,
3170                decoded_chars_used: Utf8size::default(),
3171                b2cratio: 0.0,
3172            });
3173
3174            Ok(())
3175        }
3176
3177        fn slot_init(zelf: &PyObject, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
3178            let zelf_ref: &Py<Self> = zelf.try_to_ref(vm)?;
3179            {
3180                zelf_ref.buffer.store(None);
3181                zelf_ref.encoding.store(None);
3182                zelf_ref.line_buffering.store(false, Ordering::Relaxed);
3183                zelf_ref.write_through.store(false, Ordering::Relaxed);
3184                let mut data = zelf_ref.lock_opt(vm)?;
3185                *data = None;
3186            }
3187            let parsed = args.bind_for(vm, Self::NAME)?;
3188            Self::init(zelf_ref, parsed, vm)
3189        }
3190    }
3191
3192    impl TextIOWrapper {
3193        fn lock_opt(
3194            &self,
3195            vm: &VirtualMachine,
3196        ) -> PyResult<PyThreadMutexGuard<'_, Option<TextIOData>>> {
3197            self.data
3198                .lock_wrapped(|do_lock| vm.allow_threads(do_lock))
3199                .ok_or_else(|| vm.new_runtime_error("reentrant call inside textio"))
3200        }
3201
3202        fn lock(&self, vm: &VirtualMachine) -> PyResult<PyMappedThreadMutexGuard<'_, TextIOData>> {
3203            let lock = self.lock_opt(vm)?;
3204            PyThreadMutexGuard::try_map(lock, |x| x.as_mut())
3205                .map_err(|_| vm.new_value_error("I/O operation on uninitialized object"))
3206        }
3207
3208        fn validate_errors(errors: &Py<PyUtf8Str>, vm: &VirtualMachine) -> PyResult<()> {
3209            if errors.as_pystr().contains_nuls() {
3210                cold_path();
3211                return Err(nul_char_error(vm));
3212            }
3213            vm.state
3214                .codec_registry
3215                .lookup_error(errors.as_str(), vm)
3216                .map(drop)
3217        }
3218
3219        fn bool_from_index(value: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
3220            let int = value.try_index(vm)?;
3221            let value: i32 = int.try_to_primitive(vm)?;
3222            Ok(value != 0)
3223        }
3224
3225        fn resolve_encoding(
3226            encoding: Option<PyUtf8StrRef>,
3227            vm: &VirtualMachine,
3228        ) -> PyResult<PyUtf8StrRef> {
3229            // Note: Do not issue EncodingWarning here. The warning should only
3230            // be issued by io.text_encoding(), the public API. This function
3231            // is used internally (e.g., for stdin/stdout/stderr initialization)
3232            // where no warning should be emitted.
3233            let encoding = match encoding {
3234                None if vm.state.config.settings.utf8_mode > 0 => {
3235                    identifier_utf8!(vm, utf_8).to_owned()
3236                }
3237                Some(enc) if enc.as_str() == "locale" => match vm.import("locale", 0) {
3238                    Ok(locale) => locale
3239                        .get_attr("getencoding", vm)?
3240                        .call((), vm)?
3241                        .try_into_value(vm)?,
3242                    Err(err)
3243                        if err.fast_isinstance(vm.ctx.exceptions.import_error)
3244                            || err.fast_isinstance(vm.ctx.exceptions.module_not_found_error) =>
3245                    {
3246                        identifier_utf8!(vm, utf_8).to_owned()
3247                    }
3248                    Err(err) => return Err(err),
3249                },
3250                Some(enc) => enc,
3251                _ => match vm.import("locale", 0) {
3252                    Ok(locale) => locale
3253                        .get_attr("getencoding", vm)?
3254                        .call((), vm)?
3255                        .try_into_value(vm)?,
3256                    Err(err)
3257                        if err.fast_isinstance(vm.ctx.exceptions.import_error)
3258                            || err.fast_isinstance(vm.ctx.exceptions.module_not_found_error) =>
3259                    {
3260                        identifier_utf8!(vm, utf_8).to_owned()
3261                    }
3262                    Err(err) => return Err(err),
3263                },
3264            };
3265            if encoding.as_pystr().contains_nuls() {
3266                cold_path();
3267                return Err(nul_char_error(vm));
3268            }
3269            Ok(encoding)
3270        }
3271
3272        fn adjust_encoder_state_for_bom(
3273            encoder: &PyObject,
3274            encoding: &str,
3275            buffer: &PyObject,
3276            vm: &VirtualMachine,
3277        ) -> PyResult<()> {
3278            let needs_bom = matches!(encoding, "utf-8-sig" | "utf-16" | "utf-32");
3279            if !needs_bom {
3280                return Ok(());
3281            }
3282            let seekable = vm.call_method(buffer, "seekable", ())?.try_to_bool(vm)?;
3283            if !seekable {
3284                return Ok(());
3285            }
3286            let pos = vm.call_method(buffer, "tell", ())?;
3287            if vm.bool_eq(&pos, vm.ctx.new_int(0).as_ref())? {
3288                return Ok(());
3289            }
3290            if let Err(err) = vm.call_method(encoder, "setstate", (0,))
3291                && !err.fast_isinstance(vm.ctx.exceptions.attribute_error)
3292            {
3293                return Err(err);
3294            }
3295            Ok(())
3296        }
3297
3298        #[expect(clippy::type_complexity, reason = "ignore warning for now")]
3299        fn find_coder(
3300            buffer: &PyObject,
3301            encoding: &str,
3302            errors: &Py<PyUtf8Str>,
3303            newline: Newlines,
3304            vm: &VirtualMachine,
3305        ) -> PyResult<(
3306            Option<(PyObjectRef, Option<EncodeFunc>)>,
3307            Option<PyObjectRef>,
3308        )> {
3309            let codec = vm.state.codec_registry.lookup(encoding, vm)?;
3310            if !codec.is_text_codec(vm)? {
3311                return Err(vm.new_lookup_error(format!(
3312                    "'{encoding}' is not a text encoding; use codecs.open() to handle arbitrary codecs"
3313                )));
3314            }
3315            let errors = errors.to_owned().into_wtf8();
3316
3317            let encoder = if vm.call_method(buffer, "writable", ())?.try_to_bool(vm)? {
3318                let incremental_encoder =
3319                    match codec.get_incremental_encoder(Some(errors.clone()), vm) {
3320                        Ok(encoder) => encoder,
3321                        Err(err)
3322                            if err.fast_isinstance(vm.ctx.exceptions.type_error)
3323                                || err.fast_isinstance(vm.ctx.exceptions.attribute_error) =>
3324                        {
3325                            let name = vm
3326                                .get_attribute_opt(codec.as_tuple().as_object(), "name")?
3327                                .and_then(|obj| obj.downcast::<PyStr>().ok());
3328                            StatelessIncrementalEncoder {
3329                                encode: codec.get_encode_func().to_owned(),
3330                                errors: Some(errors.clone()),
3331                                name,
3332                            }
3333                            .into_ref(&vm.ctx)
3334                            .into()
3335                        }
3336                        Err(err) => return Err(err),
3337                    };
3338                let encoding_name = vm.get_attribute_opt(&incremental_encoder, "name")?;
3339                let encode_func = encoding_name.and_then(|name| {
3340                    let name = name.downcast_ref::<PyStr>()?;
3341                    match name.to_str()? {
3342                        "utf-8" => Some(textio_encode_utf8 as EncodeFunc),
3343                        _ => None,
3344                    }
3345                });
3346                Some((incremental_encoder, encode_func))
3347            } else {
3348                None
3349            };
3350
3351            let decoder = if vm.call_method(buffer, "readable", ())?.try_to_bool(vm)? {
3352                let decoder = match codec.get_incremental_decoder(Some(errors.clone()), vm) {
3353                    Ok(decoder) => decoder,
3354                    Err(err)
3355                        if err.fast_isinstance(vm.ctx.exceptions.type_error)
3356                            || err.fast_isinstance(vm.ctx.exceptions.attribute_error) =>
3357                    {
3358                        StatelessIncrementalDecoder {
3359                            decode: codec.get_decode_func().to_owned(),
3360                            errors: Some(errors),
3361                        }
3362                        .into_ref(&vm.ctx)
3363                        .into()
3364                    }
3365                    Err(err) => return Err(err),
3366                };
3367                if let Newlines::Universal | Newlines::Passthrough = newline {
3368                    let args = IncrementalNewlineDecoderArgs {
3369                        decoder,
3370                        translate: matches!(newline, Newlines::Universal),
3371                        errors: None,
3372                    };
3373                    Some(IncrementalNewlineDecoder::construct_and_init(args, vm)?.into())
3374                } else {
3375                    Some(decoder)
3376                }
3377            } else {
3378                None
3379            };
3380            Ok((encoder, decoder))
3381        }
3382    }
3383
3384    #[inline]
3385    fn flush_inner(textio: &mut TextIOData, buffer: &PyObject, vm: &VirtualMachine) -> PyResult {
3386        textio.check_closed(buffer, vm)?;
3387        textio.telling = textio.seekable;
3388        textio.write_pending(buffer, vm)?;
3389        vm.call_method(buffer, "flush", ())
3390    }
3391
3392    fn text_buffer(cell: &RawCell, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
3393        cell.load_owned()
3394            .ok_or_else(|| vm.new_value_error("underlying buffer has been detached"))
3395    }
3396
3397    fn textio_encoding(cell: &RawCell, vm: &VirtualMachine) -> PyResult<PyUtf8StrRef> {
3398        let obj = cell
3399            .load_owned()
3400            .ok_or_else(|| vm.new_value_error("I/O operation on uninitialized object"))?;
3401        obj.downcast::<PyUtf8Str>().map_err(|obj| {
3402            vm.new_type_error(format!(
3403                "encoding must be a str, not '{}'",
3404                obj.class().name()
3405            ))
3406        })
3407    }
3408
3409    #[pyclass(
3410        with(
3411            Constructor,
3412            Initializer,
3413            Destructor,
3414            Iterable,
3415            IterNext,
3416            Representable
3417        ),
3418        flags(BASETYPE, HAS_WEAKREF)
3419    )]
3420    impl Py<TextIOWrapper> {
3421        #[pymethod]
3422        fn reconfigure(
3423            &self,
3424            args: TextIOWrapperReconfigureArgs,
3425            vm: &VirtualMachine,
3426        ) -> PyResult<()> {
3427            let mut data = self.lock(vm)?;
3428            let buffer = text_buffer(&self.buffer, vm)?;
3429            data.check_closed(&buffer, vm)?;
3430
3431            let mut encoding = textio_encoding(&self.encoding, vm)?;
3432            let mut errors = data.errors.clone();
3433            let mut newline = data.newline;
3434            let mut encoding_changed = false;
3435            let mut errors_changed = false;
3436            let mut newline_changed = false;
3437            let mut line_buffering = None;
3438            let mut write_through = None;
3439
3440            if let Some(enc) = args.encoding {
3441                if enc.as_pystr().contains_nuls() && enc.as_str().starts_with("locale") {
3442                    cold_path();
3443                    return Err(vm.new_lookup_error(format!("unknown encoding: {enc}")));
3444                }
3445                let resolved = TextIOWrapper::resolve_encoding(Some(enc), vm)?;
3446                encoding_changed = resolved.as_str() != encoding.as_str();
3447                encoding = resolved;
3448            }
3449
3450            if let Some(errs) = args.errors {
3451                TextIOWrapper::validate_errors(&errs, vm)?;
3452                errors_changed = errs.as_str() != errors.as_str();
3453                errors = errs;
3454            } else if encoding_changed {
3455                errors = identifier_utf8!(vm, strict).to_owned();
3456                errors_changed = true;
3457            }
3458
3459            if let OptionalArg::Present(nl) = args.newline {
3460                let nl = nl.unwrap_or_default();
3461                newline_changed = nl != newline;
3462                newline = nl;
3463            }
3464
3465            if let Some(value) = args.line_buffering {
3466                line_buffering = Some(TextIOWrapper::bool_from_index(&value, vm)?);
3467            }
3468            if let Some(value) = args.write_through {
3469                write_through = Some(TextIOWrapper::bool_from_index(&value, vm)?);
3470            }
3471
3472            if (encoding_changed || newline_changed)
3473                && data.decoder.is_some()
3474                && (data.decoded_chars.is_some()
3475                    || data.snapshot.is_some()
3476                    || data.decoded_chars_used.chars != 0)
3477            {
3478                return Err(new_unsupported_operation(
3479                    "cannot reconfigure encoding or newline after reading from the stream",
3480                    vm,
3481                ));
3482            }
3483
3484            if data.pending.num_bytes > 0 {
3485                data.write_pending(&buffer, vm)?;
3486            }
3487            vm.call_method(&buffer, "flush", ())?;
3488
3489            if encoding_changed || errors_changed || newline_changed {
3490                if data.pending.num_bytes > 0 {
3491                    data.write_pending(&buffer, vm)?;
3492                }
3493                let (encoder, decoder) =
3494                    TextIOWrapper::find_coder(&buffer, encoding.as_str(), &errors, newline, vm)?;
3495                self.encoding.store(Some(encoding.clone().into()));
3496                data.errors = errors;
3497                data.newline = newline;
3498                data.encoder = encoder;
3499                data.decoder = decoder;
3500                data.set_decoded_chars(None);
3501                data.snapshot = None;
3502                data.decoded_chars_used = Utf8size::default();
3503                if let Some((encoder, _)) = &data.encoder {
3504                    TextIOWrapper::adjust_encoder_state_for_bom(
3505                        encoder,
3506                        encoding.as_str(),
3507                        &buffer,
3508                        vm,
3509                    )?;
3510                }
3511            }
3512
3513            if let Some(line_buffering) = line_buffering {
3514                self.line_buffering.store(line_buffering, Ordering::Relaxed);
3515            }
3516            if let Some(write_through) = write_through {
3517                self.write_through.store(write_through, Ordering::Relaxed);
3518            }
3519            Ok(())
3520        }
3521
3522        #[pymethod]
3523        fn detach(zelf: PyRef<TextIOWrapper>, vm: &VirtualMachine) -> PyResult {
3524            let mut textio = zelf.lock(vm)?;
3525            let buffer = text_buffer(&zelf.buffer, vm)?;
3526            flush_inner(&mut textio, &buffer, vm)?;
3527            zelf.buffer.store(None);
3528            Ok(buffer)
3529        }
3530
3531        #[pymethod]
3532        fn seekable(&self, vm: &VirtualMachine) -> PyResult {
3533            let textio = self.lock(vm)?;
3534            let buffer = text_buffer(&self.buffer, vm)?;
3535            drop(textio);
3536            vm.call_method(&buffer, "seekable", ())
3537        }
3538
3539        #[pymethod]
3540        fn readable(&self, vm: &VirtualMachine) -> PyResult {
3541            let textio = self.lock(vm)?;
3542            let buffer = text_buffer(&self.buffer, vm)?;
3543            drop(textio);
3544            vm.call_method(&buffer, "readable", ())
3545        }
3546
3547        #[pymethod]
3548        fn writable(&self, vm: &VirtualMachine) -> PyResult {
3549            let textio = self.lock(vm)?;
3550            let buffer = text_buffer(&self.buffer, vm)?;
3551            drop(textio);
3552            vm.call_method(&buffer, "writable", ())
3553        }
3554
3555        #[pygetset]
3556        fn newlines(&self, vm: &VirtualMachine) -> PyResult<Option<PyObjectRef>> {
3557            let data = self.lock(vm)?;
3558            let Some(decoder) = &data.decoder else {
3559                return Ok(None);
3560            };
3561            vm.get_attribute_opt(decoder, "newlines")
3562        }
3563
3564        #[pygetset(name = "_CHUNK_SIZE")]
3565        fn chunksize(&self, vm: &VirtualMachine) -> PyResult<usize> {
3566            Ok(self.lock(vm)?.chunk_size)
3567        }
3568
3569        #[pygetset(setter, name = "_CHUNK_SIZE")]
3570        fn set_chunksize(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
3571            {
3572                let textio = self.lock(vm)?;
3573                if self.buffer.deref().is_none() {
3574                    return Err(vm.new_value_error("underlying buffer has been detached"));
3575                }
3576                drop(textio);
3577            }
3578
3579            let chunk_size: isize = match value {
3580                PySetterValue::Assign(object_value) => {
3581                    let integer = object_value.try_index(vm)?;
3582
3583                    integer.try_to_primitive::<isize>(vm).map_err(|_| {
3584                        let class = object_value.class();
3585                        let type_name = class.name();
3586                        let mut end = type_name.len().min(200);
3587                        while !type_name.is_char_boundary(end) {
3588                            end -= 1;
3589                        }
3590                        vm.new_value_error(format!(
3591                            "cannot fit '{}' into an index-sized integer",
3592                            &type_name[..end]
3593                        ))
3594                    })?
3595                }
3596                PySetterValue::Delete => {
3597                    return Err(vm.new_attribute_error("cannot delete attribute"));
3598                }
3599            };
3600
3601            if chunk_size <= 0 {
3602                return Err(vm.new_value_error("a strictly positive integer is required"));
3603            }
3604
3605            let chunk_size = usize::try_from(chunk_size)
3606                .map_err(|_| vm.new_value_error("a strictly positive integer is required"))?;
3607
3608            let mut textio = self.lock(vm)?;
3609            textio.chunk_size = chunk_size;
3610            Ok(())
3611        }
3612
3613        #[pymethod]
3614        fn seek(
3615            zelf: PyRef<TextIOWrapper>,
3616            cookie: PyObjectRef,
3617            whence: HowArg,
3618            vm: &VirtualMachine,
3619        ) -> PyResult {
3620            let how = whence.whence;
3621
3622            let reset_encoder = |encoder, start_of_stream| {
3623                if start_of_stream {
3624                    vm.call_method(encoder, "reset", ())
3625                } else {
3626                    vm.call_method(encoder, "setstate", (0,))
3627                }
3628            };
3629
3630            let textio = zelf.lock(vm)?;
3631
3632            if !textio.seekable {
3633                return Err(new_unsupported_operation(
3634                    "underlying stream is not seekable",
3635                    vm,
3636                ));
3637            }
3638
3639            let cookie = match how {
3640                // SEEK_SET
3641                0 => cookie,
3642                // SEEK_CUR
3643                1 => {
3644                    if vm.bool_eq(&cookie, vm.ctx.new_int(0).as_ref())? {
3645                        let buffer = text_buffer(&zelf.buffer, vm)?;
3646                        vm.call_method(&buffer, "tell", ())?
3647                    } else {
3648                        return Err(new_unsupported_operation(
3649                            "can't do nonzero cur-relative seeks",
3650                            vm,
3651                        ));
3652                    }
3653                }
3654                // SEEK_END
3655                2 => {
3656                    if vm.bool_eq(&cookie, vm.ctx.new_int(0).as_ref())? {
3657                        drop(textio);
3658                        vm.call_method(zelf.as_object(), "flush", ())?;
3659                        let mut textio = zelf.lock(vm)?;
3660                        textio.set_decoded_chars(None);
3661                        textio.snapshot = None;
3662                        if let Some(decoder) = &textio.decoder {
3663                            vm.call_method(decoder, "reset", ())?;
3664                        }
3665                        let buffer = text_buffer(&zelf.buffer, vm)?;
3666                        let res = vm.call_method(&buffer, "seek", (0, 2))?;
3667                        if let Some((encoder, _)) = &textio.encoder {
3668                            let start_of_stream = vm.bool_eq(&res, vm.ctx.new_int(0).as_ref())?;
3669                            reset_encoder(encoder, start_of_stream)?;
3670                        }
3671                        return Ok(res);
3672                    }
3673                    return Err(new_unsupported_operation(
3674                        "can't do nonzero end-relative seeks",
3675                        vm,
3676                    ));
3677                }
3678                _ => {
3679                    return Err(
3680                        vm.new_value_error(format!("invalid whence ({how}, should be 0, 1 or 2)"))
3681                    );
3682                }
3683            };
3684            use crate::types::PyComparisonOp;
3685            if cookie.rich_compare_bool(vm.ctx.new_int(0).as_ref(), PyComparisonOp::Lt, vm)? {
3686                return Err(
3687                    vm.new_value_error(format!("negative seek position {}", cookie.repr(vm)?))
3688                );
3689            }
3690            drop(textio);
3691            vm.call_method(zelf.as_object(), "flush", ())?;
3692            let cookie_obj = crate::builtins::PyIntRef::try_from_object(vm, cookie)?;
3693            let cookie = TextIOCookie::parse(cookie_obj.as_bigint())
3694                .ok_or_else(|| vm.new_value_error("invalid cookie"))?;
3695            let mut textio = zelf.lock(vm)?;
3696            let buffer = text_buffer(&zelf.buffer, vm)?;
3697            vm.call_method(&buffer, "seek", (cookie.start_pos,))?;
3698            textio.set_decoded_chars(None);
3699            textio.snapshot = None;
3700            if let Some(decoder) = &textio.decoder {
3701                cookie.set_decoder_state(decoder, vm)?;
3702            }
3703            if cookie.chars_to_skip != 0 {
3704                let decoder = textio
3705                    .decoder
3706                    .clone()
3707                    .ok_or_else(|| vm.new_value_error("invalid cookie"))?;
3708                let input_chunk = vm.call_method(&buffer, "read", (cookie.bytes_to_feed,))?;
3709                let input_chunk: PyBytesRef = input_chunk.downcast().map_err(|obj| {
3710                    vm.new_type_error(format!(
3711                        "underlying read() should have returned a bytes object, not '{}'",
3712                        obj.class().name()
3713                    ))
3714                })?;
3715                textio.snapshot = Some((cookie.dec_flags, input_chunk.clone()));
3716                let decoded = vm.call_method(&decoder, "decode", (input_chunk, cookie.need_eof))?;
3717                let decoded = check_decoded(decoded, vm)?;
3718                // The position is stored both as a count of characters and as
3719                // an offset in bytes, so both have to land inside what was
3720                // just decoded: everything read back from here indexes it.
3721                let num_to_skip = cookie.num_to_skip();
3722                let pos_is_valid = num_to_skip.chars <= decoded.char_len()
3723                    && decoded.as_wtf8().is_code_point_boundary(num_to_skip.bytes);
3724                textio.set_decoded_chars(Some(decoded));
3725                if !pos_is_valid {
3726                    return Err(vm.new_os_error("can't restore logical file position"));
3727                }
3728                textio.decoded_chars_used = num_to_skip;
3729            } else {
3730                textio.snapshot = Some((cookie.dec_flags, PyBytes::from(vec![]).into_ref(&vm.ctx)))
3731            }
3732            if let Some((encoder, _)) = &textio.encoder {
3733                let start_of_stream = cookie.start_pos == 0 && cookie.dec_flags == 0;
3734                reset_encoder(encoder, start_of_stream)?;
3735            }
3736            Ok(cookie_obj.into())
3737        }
3738
3739        #[pymethod]
3740        fn tell(zelf: PyRef<TextIOWrapper>, vm: &VirtualMachine) -> PyResult {
3741            let mut textio = zelf.lock(vm)?;
3742            if !textio.seekable {
3743                return Err(new_unsupported_operation(
3744                    "underlying stream is not seekable",
3745                    vm,
3746                ));
3747            }
3748            if !textio.telling {
3749                return Err(vm.new_os_error("telling position disabled by next() call"));
3750            }
3751            let buffer = text_buffer(&zelf.buffer, vm)?;
3752            textio.write_pending(&buffer, vm)?;
3753            drop(textio);
3754            vm.call_method(zelf.as_object(), "flush", ())?;
3755            let textio = zelf.lock(vm)?;
3756            let buffer = text_buffer(&zelf.buffer, vm)?;
3757            let pos = vm.call_method(&buffer, "tell", ())?;
3758            let (decoder, (dec_flags, next_input)) = match (&textio.decoder, &textio.snapshot) {
3759                (Some(d), Some(s)) => (d, s),
3760                _ => return Ok(pos),
3761            };
3762            let pos = Offset::try_from_object(vm, pos)?;
3763            let mut cookie = TextIOCookie {
3764                start_pos: pos - next_input.as_bytes().len() as Offset,
3765                dec_flags: *dec_flags,
3766                ..Default::default()
3767            };
3768            if textio.decoded_chars_used.bytes == 0 {
3769                return Ok(cookie.build().to_pyobject(vm));
3770            }
3771            let decoder_getstate = || {
3772                let state = vm.call_method(decoder, "getstate", ())?;
3773                parse_decoder_state(state, vm)
3774            };
3775            let decoder_decode = |b: &[u8]| {
3776                let decoded = vm.call_method(decoder, "decode", (vm.ctx.new_bytes(b.to_vec()),))?;
3777                let decoded = check_decoded(decoded, vm)?;
3778                Ok(Utf8size::len_pystr(&decoded))
3779            };
3780            let saved_state = vm.call_method(decoder, "getstate", ())?;
3781            let mut num_to_skip = textio.decoded_chars_used;
3782            let mut skip_bytes = (textio.b2cratio * num_to_skip.chars as f64) as isize;
3783            let mut skip_back = 1;
3784            while skip_bytes > 0 {
3785                cookie.set_decoder_state(decoder, vm)?;
3786                let input = &next_input.as_bytes()[..skip_bytes as usize];
3787                let n_decoded = decoder_decode(input)?;
3788                if n_decoded.chars <= num_to_skip.chars {
3789                    let (dec_buffer, dec_flags) = decoder_getstate()?;
3790                    if dec_buffer.as_bytes().is_empty() {
3791                        cookie.dec_flags = dec_flags;
3792                        num_to_skip -= n_decoded;
3793                        break;
3794                    }
3795                    skip_bytes -= dec_buffer.as_bytes().len() as isize;
3796                    skip_back = 1;
3797                } else {
3798                    skip_bytes -= skip_back;
3799                    skip_back *= 2;
3800                }
3801            }
3802            if skip_bytes <= 0 {
3803                skip_bytes = 0;
3804                cookie.set_decoder_state(decoder, vm)?;
3805            }
3806            let skip_bytes = skip_bytes as usize;
3807
3808            cookie.start_pos += skip_bytes as Offset;
3809            cookie.set_num_to_skip(num_to_skip);
3810
3811            if num_to_skip.chars != 0 {
3812                let mut n_decoded = Utf8size::default();
3813                let mut input = next_input.as_bytes();
3814                input = &input[skip_bytes..];
3815                while !input.is_empty() {
3816                    let (byte1, rest) = input.split_at(1);
3817                    let n = decoder_decode(byte1)?;
3818                    n_decoded += n;
3819                    cookie.bytes_to_feed += 1;
3820                    let (dec_buffer, dec_flags) = decoder_getstate()?;
3821                    if dec_buffer.as_bytes().is_empty() && n_decoded.chars <= num_to_skip.chars {
3822                        cookie.start_pos += cookie.bytes_to_feed as Offset;
3823                        num_to_skip -= n_decoded;
3824                        cookie.dec_flags = dec_flags;
3825                        cookie.bytes_to_feed = 0;
3826                        n_decoded = Utf8size::default();
3827                    }
3828                    if n_decoded.chars >= num_to_skip.chars {
3829                        break;
3830                    }
3831                    input = rest;
3832                }
3833                if input.is_empty() {
3834                    let decoded =
3835                        vm.call_method(decoder, "decode", (vm.ctx.new_bytes(vec![]), true))?;
3836                    let decoded = check_decoded(decoded, vm)?;
3837                    let final_decoded_chars = n_decoded.chars + decoded.char_len();
3838                    cookie.need_eof = true;
3839                    if final_decoded_chars < num_to_skip.chars {
3840                        return Err(vm.new_os_error("can't reconstruct logical file position"));
3841                    }
3842                }
3843            }
3844            vm.call_method(decoder, "setstate", (saved_state,))?;
3845            cookie.set_num_to_skip(num_to_skip);
3846            Ok(cookie.build().to_pyobject(vm))
3847        }
3848
3849        #[pygetset]
3850        fn name(&self, vm: &VirtualMachine) -> PyResult {
3851            let _textio = self.lock(vm)?;
3852            text_buffer(&self.buffer, vm)?.get_attr("name", vm)
3853        }
3854
3855        #[pygetset]
3856        fn errors(&self, vm: &VirtualMachine) -> PyResult<PyUtf8StrRef> {
3857            Ok(self.lock(vm)?.errors.clone())
3858        }
3859
3860        #[pymethod]
3861        fn fileno(&self, vm: &VirtualMachine) -> PyResult {
3862            let _textio = self.lock(vm)?;
3863            let buffer = text_buffer(&self.buffer, vm)?;
3864            vm.call_method(&buffer, "fileno", ())
3865        }
3866
3867        #[pymethod]
3868        fn read(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult<PyStrRef> {
3869            let mut textio = self.lock(vm)?;
3870            let buffer = text_buffer(&self.buffer, vm)?;
3871            textio.check_closed(&buffer, vm)?;
3872            let decoder = textio
3873                .decoder
3874                .clone()
3875                .ok_or_else(|| new_unsupported_operation("not readable", vm))?;
3876
3877            textio.write_pending(&buffer, vm)?;
3878
3879            let s = if let Some(mut remaining) = size.to_usize() {
3880                let mut chunks = Vec::new();
3881                let mut chunks_bytes = 0;
3882                loop {
3883                    if let Some((s, char_len)) = textio.get_decoded_chars(remaining, vm) {
3884                        chunks_bytes += s.byte_len();
3885                        chunks.push(s);
3886                        remaining = remaining.saturating_sub(char_len);
3887                    }
3888                    if remaining == 0 {
3889                        break;
3890                    }
3891                    let eof = textio.read_chunk(&buffer, remaining, vm)?;
3892                    if eof {
3893                        break;
3894                    }
3895                }
3896                if chunks.is_empty() {
3897                    vm.ctx.empty_str.to_owned()
3898                } else if chunks.len() == 1 {
3899                    chunks.pop().unwrap()
3900                } else {
3901                    let mut ret = Wtf8Buf::with_capacity(chunks_bytes);
3902                    for chunk in chunks {
3903                        ret.push_wtf8(chunk.as_wtf8())
3904                    }
3905                    PyStr::from(ret).into_ref(&vm.ctx)
3906                }
3907            } else {
3908                let bytes = vm.call_method(&buffer, "read", ())?;
3909                let decoded = vm.call_method(&decoder, "decode", (bytes, true))?;
3910                let decoded = check_decoded(decoded, vm)?;
3911                let ret = textio.take_decoded_chars(Some(decoded), vm);
3912                textio.snapshot = None;
3913                ret
3914            };
3915            Ok(s)
3916        }
3917
3918        #[pymethod]
3919        fn write(&self, text: PyStrRef, vm: &VirtualMachine) -> PyResult<usize> {
3920            let mut textio = self.lock(vm)?;
3921            let buffer = text_buffer(&self.buffer, vm)?;
3922            textio.check_closed(&buffer, vm)?;
3923
3924            let (encoder, encode_func) = textio
3925                .encoder
3926                .as_ref()
3927                .ok_or_else(|| new_unsupported_operation("not writable", vm))?;
3928
3929            let char_len = text.char_len();
3930
3931            let data = text.as_wtf8();
3932
3933            let replace_nl = match textio.newline {
3934                Newlines::Lf => Some("\n"),
3935                Newlines::Cr => Some("\r"),
3936                Newlines::Crlf => Some("\r\n"),
3937                Newlines::Universal if cfg!(windows) => Some("\r\n"),
3938                _ => None,
3939            };
3940            let line_buffering = self.line_buffering.load(Ordering::Relaxed);
3941            let has_lf =
3942                (replace_nl.is_some() || line_buffering) && data.contains_code_point('\n'.into());
3943            let flush = line_buffering && (has_lf || data.contains_code_point('\r'.into()));
3944            let chunk = if let Some(replace_nl) = replace_nl {
3945                if has_lf {
3946                    PyStr::from(data.replace("\n".as_ref(), replace_nl.as_ref())).into_ref(&vm.ctx)
3947                } else {
3948                    text
3949                }
3950            } else {
3951                text
3952            };
3953            let chunk = if let Some(encode_func) = *encode_func {
3954                encode_func(chunk)
3955            } else {
3956                let b = vm.call_method(encoder, "encode", (chunk.clone(),))?;
3957                b.downcast::<PyBytes>()
3958                    .map(PendingWrite::Bytes)
3959                    .or_else(|obj| {
3960                        // TODO: not sure if encode() returning the str it was passed is officially
3961                        // supported or just a quirk of how the CPython code is written
3962                        if obj.is(&chunk) {
3963                            Ok(PendingWrite::Utf8(chunk))
3964                        } else {
3965                            Err(vm.new_type_error(format!(
3966                                "encoder should return a bytes object, not '{}'",
3967                                obj.class().name()
3968                            )))
3969                        }
3970                    })?
3971            };
3972            if textio.pending.num_bytes > 0
3973                && textio.pending.num_bytes + chunk.as_bytes().len() > textio.chunk_size
3974            {
3975                let pending = textio.pending.take(vm);
3976                drop(textio);
3977                vm.call_method(&buffer, "write", (pending,))?;
3978                textio = self.lock(vm)?;
3979                let buffer = text_buffer(&self.buffer, vm)?;
3980                textio.check_closed(&buffer, vm)?;
3981                if textio.pending.num_bytes > 0 {
3982                    let pending = textio.pending.take(vm);
3983                    drop(textio);
3984                    vm.call_method(&buffer, "write", (pending,))?;
3985                    textio = self.lock(vm)?;
3986                    let buffer = text_buffer(&self.buffer, vm)?;
3987                    textio.check_closed(&buffer, vm)?;
3988                }
3989            }
3990            textio.pending.push(chunk);
3991            if textio.pending.num_bytes > 0
3992                && (flush
3993                    || self.write_through.load(Ordering::Relaxed)
3994                    || textio.pending.num_bytes >= textio.chunk_size)
3995            {
3996                let buffer = text_buffer(&self.buffer, vm)?;
3997                let pending = textio.pending.take(vm);
3998                drop(textio);
3999                vm.call_method(&buffer, "write", (pending,))?;
4000                textio = self.lock(vm)?;
4001                let buffer = text_buffer(&self.buffer, vm)?;
4002                textio.check_closed(&buffer, vm)?;
4003            }
4004            if flush {
4005                let buffer = text_buffer(&self.buffer, vm)?;
4006                let _ = vm.call_method(&buffer, "flush", ());
4007            }
4008
4009            Ok(char_len)
4010        }
4011
4012        #[pymethod]
4013        fn flush(&self, vm: &VirtualMachine) -> PyResult {
4014            let mut textio = self.lock(vm)?;
4015            let buffer = text_buffer(&self.buffer, vm)?;
4016            flush_inner(&mut textio, &buffer, vm)
4017        }
4018
4019        #[pymethod]
4020        fn truncate(zelf: PyRef<TextIOWrapper>, pos: KeepNonePos, vm: &VirtualMachine) -> PyResult {
4021            // Implementation follows _pyio.py TextIOWrapper.truncate
4022            let mut textio = zelf.lock(vm)?;
4023            let buffer = text_buffer(&zelf.buffer, vm)?;
4024            flush_inner(&mut textio, &buffer, vm)?;
4025            drop(textio);
4026
4027            let pos = match pos.pos.into_option() {
4028                Some(p) => p,
4029                None => vm.call_method(zelf.as_object(), "tell", ())?,
4030            };
4031            vm.call_method(&buffer, "truncate", (pos,))
4032        }
4033
4034        #[pymethod]
4035        fn isatty(&self, vm: &VirtualMachine) -> PyResult {
4036            let textio = self.lock(vm)?;
4037            let buffer = text_buffer(&self.buffer, vm)?;
4038            textio.check_closed(&buffer, vm)?;
4039            vm.call_method(&buffer, "isatty", ())
4040        }
4041
4042        #[pymethod]
4043        fn readline(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult<PyStrRef> {
4044            let limit = size.to_usize();
4045
4046            let mut textio = self.lock(vm)?;
4047            let buffer = text_buffer(&self.buffer, vm)?;
4048            check_closed(&buffer, vm)?;
4049
4050            textio.write_pending(&buffer, vm)?;
4051
4052            #[derive(Clone)]
4053            struct SlicedStr(PyStrRef, Range<usize>);
4054
4055            impl SlicedStr {
4056                #[inline]
4057                fn byte_len(&self) -> usize {
4058                    self.1.len()
4059                }
4060
4061                #[inline]
4062                fn char_len(&self) -> usize {
4063                    if self.is_full_slice() {
4064                        self.0.char_len()
4065                    } else {
4066                        self.slice().code_points().count()
4067                    }
4068                }
4069
4070                #[inline]
4071                fn is_full_slice(&self) -> bool {
4072                    self.1.len() >= self.0.byte_len()
4073                }
4074
4075                #[inline]
4076                fn slice(&self) -> &Wtf8 {
4077                    &self.0.as_wtf8()[self.1.clone()]
4078                }
4079
4080                #[inline]
4081                fn slice_pystr(self, vm: &VirtualMachine) -> PyStrRef {
4082                    if self.is_full_slice() {
4083                        self.0
4084                    } else {
4085                        // TODO: try to use Arc::get_mut() on the str?
4086                        PyStr::from(self.slice()).into_ref(&vm.ctx)
4087                    }
4088                }
4089
4090                fn utf8_len(&self) -> Utf8size {
4091                    Utf8size {
4092                        bytes: self.byte_len(),
4093                        chars: self.char_len(),
4094                    }
4095                }
4096            }
4097
4098            let mut start;
4099            let mut end_pos;
4100            let mut offset_to_buffer;
4101            let mut chunked = Utf8size::default();
4102            let mut remaining: Option<SlicedStr> = None;
4103            let mut chunks = Vec::new();
4104
4105            let cur_line = 'outer: loop {
4106                let decoded_chars = loop {
4107                    match textio.decoded_chars.as_ref() {
4108                        Some(s) if !s.is_empty() => break s,
4109                        _ => {}
4110                    }
4111                    let eof = textio.read_chunk(&buffer, 0, vm)?;
4112                    if eof {
4113                        textio.set_decoded_chars(None);
4114                        textio.snapshot = None;
4115                        start = Utf8size::default();
4116                        end_pos = Utf8size::default();
4117                        offset_to_buffer = Utf8size::default();
4118                        break 'outer None;
4119                    }
4120                };
4121                let line = match remaining.take() {
4122                    None => {
4123                        start = textio.decoded_chars_used;
4124                        offset_to_buffer = Utf8size::default();
4125                        decoded_chars.clone()
4126                    }
4127                    Some(remaining) => {
4128                        assert_eq!(textio.decoded_chars_used.bytes, 0);
4129                        offset_to_buffer = remaining.utf8_len();
4130                        let decoded_chars = decoded_chars.as_wtf8();
4131                        let line = if remaining.is_full_slice() {
4132                            let mut line = remaining.0;
4133                            line.concat_in_place(decoded_chars, vm);
4134                            line
4135                        } else {
4136                            let remaining = remaining.slice();
4137                            let mut s =
4138                                Wtf8Buf::with_capacity(remaining.len() + decoded_chars.len());
4139                            s.push_wtf8(remaining);
4140                            s.push_wtf8(decoded_chars);
4141                            PyStr::from(s).into_ref(&vm.ctx)
4142                        };
4143                        start = Utf8size::default();
4144                        line
4145                    }
4146                };
4147                let line_from_start = &line.as_wtf8()[start.bytes..];
4148                let nl_res = textio.newline.find_newline(line_from_start);
4149                match nl_res {
4150                    Ok(p) | Err(p) => {
4151                        end_pos = start + Utf8size::len_str(&line_from_start[..p]);
4152                        if let Some(limit) = limit {
4153                            // original CPython logic: end_pos = start + limit - chunked
4154                            if chunked.chars + end_pos.chars >= limit {
4155                                end_pos = start
4156                                    + Utf8size {
4157                                        chars: limit - chunked.chars,
4158                                        bytes: crate::common::str::codepoint_range_end(
4159                                            line_from_start,
4160                                            limit - chunked.chars,
4161                                        )
4162                                        .unwrap(),
4163                                    };
4164                                break Some(line);
4165                            }
4166                        }
4167                    }
4168                }
4169                if nl_res.is_ok() {
4170                    break Some(line);
4171                }
4172                if end_pos.bytes > start.bytes {
4173                    let chunk = SlicedStr(line.clone(), start.bytes..end_pos.bytes);
4174                    chunked += chunk.utf8_len();
4175                    chunks.push(chunk);
4176                }
4177                let line_len = line.byte_len();
4178                if end_pos.bytes < line_len {
4179                    remaining = Some(SlicedStr(line, end_pos.bytes..line_len));
4180                }
4181                textio.set_decoded_chars(None);
4182            };
4183
4184            let cur_line = cur_line.map(|line| {
4185                textio.decoded_chars_used = end_pos - offset_to_buffer;
4186                SlicedStr(line, start.bytes..end_pos.bytes)
4187            });
4188            // don't need to care about chunked.chars anymore
4189            let mut chunked = chunked.bytes;
4190            if let Some(remaining) = remaining {
4191                chunked += remaining.byte_len();
4192                chunks.push(remaining);
4193            }
4194            let line = if !chunks.is_empty() {
4195                if let Some(cur_line) = cur_line {
4196                    chunked += cur_line.byte_len();
4197                    chunks.push(cur_line);
4198                }
4199                let mut s = Wtf8Buf::with_capacity(chunked);
4200                for chunk in chunks {
4201                    s.push_wtf8(chunk.slice())
4202                }
4203                PyStr::from(s).into_ref(&vm.ctx)
4204            } else if let Some(cur_line) = cur_line {
4205                cur_line.slice_pystr(vm)
4206            } else {
4207                vm.ctx.empty_str.to_owned()
4208            };
4209            Ok(line)
4210        }
4211
4212        #[pymethod]
4213        fn close(zelf: PyRef<TextIOWrapper>, vm: &VirtualMachine) -> PyResult<()> {
4214            let _textio = zelf.lock(vm)?;
4215            let Some(buffer) = zelf.buffer.load_owned() else {
4216                return Err(vm.new_value_error("underlying buffer has been detached"));
4217            };
4218            drop(_textio);
4219            if file_closed(&buffer, vm)? {
4220                return Ok(());
4221            }
4222            // https://github.com/python/cpython/issues/142594
4223            // The file_closed() check above may have triggered a reentrant
4224            // call to detach() via a custom `closed` property.
4225            // If so, the buffer is now detached and we should return early.
4226            if zelf.buffer.deref().is_none() {
4227                return Ok(());
4228            }
4229            if zelf.finalizing.load(Ordering::Relaxed) {
4230                // _dealloc_warn: delegate to buffer._dealloc_warn(source)
4231                let _ = vm.call_method(&buffer, "_dealloc_warn", (zelf.as_object().to_owned(),));
4232            }
4233            let flush_res = vm.call_method(zelf.as_object(), "flush", ()).map(drop);
4234            let close_res = vm.call_method(&buffer, "close", ()).map(drop);
4235            exception_chain(flush_res, close_res)
4236        }
4237
4238        #[pygetset]
4239        fn closed(&self, vm: &VirtualMachine) -> PyResult {
4240            let _textio = self.lock(vm)?;
4241            text_buffer(&self.buffer, vm)?.get_attr("closed", vm)
4242        }
4243
4244        #[pymethod]
4245        fn __getstate__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
4246            Err(vm.new_type_error(format!("cannot pickle '{}' instances", zelf.class().name())))
4247        }
4248
4249        #[pymethod]
4250        fn __reduce_ex__(zelf: PyObjectRef, proto: usize, vm: &VirtualMachine) -> PyResult {
4251            if zelf.class().is(TextIOWrapper::static_type()) {
4252                return Err(
4253                    vm.new_type_error(format!("cannot pickle '{}' object", zelf.class().name()))
4254                );
4255            }
4256            let _ = proto;
4257            reduce_ex_for_subclass(&zelf, vm)
4258        }
4259    }
4260
4261    fn parse_decoder_state(state: PyObjectRef, vm: &VirtualMachine) -> PyResult<(PyBytesRef, i32)> {
4262        use crate::builtins::{PyTuple, int};
4263        let state_err = || vm.new_type_error("illegal decoder state");
4264        let state = state.downcast::<PyTuple>().map_err(|_| state_err())?;
4265        match state.as_slice() {
4266            [buf, flags] => {
4267                let buf = buf.clone().downcast::<PyBytes>().map_err(|obj| {
4268                    vm.new_type_error(format!(
4269                        "illegal decoder state: the first item should be a bytes object, not '{}'",
4270                        obj.class().name()
4271                    ))
4272                })?;
4273                let flags = flags.downcast_ref::<int::PyInt>().ok_or_else(state_err)?;
4274                let flags = flags.try_to_primitive(vm)?;
4275                Ok((buf, flags))
4276            }
4277            _ => Err(state_err()),
4278        }
4279    }
4280
4281    impl TextIOData {
4282        fn write_pending(&mut self, buffer: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
4283            if self.pending.num_bytes == 0 {
4284                return Ok(());
4285            }
4286            let data = self.pending.take(vm);
4287            vm.call_method(buffer, "write", (data,))?;
4288            Ok(())
4289        }
4290
4291        /// returns true on EOF
4292        fn read_chunk(
4293            &mut self,
4294            buffer: &PyObject,
4295            size_hint: usize,
4296            vm: &VirtualMachine,
4297        ) -> PyResult<bool> {
4298            let decoder = self
4299                .decoder
4300                .as_ref()
4301                .ok_or_else(|| new_unsupported_operation("not readable", vm))?;
4302
4303            let dec_state = if self.telling {
4304                let state = vm.call_method(decoder, "getstate", ())?;
4305                Some(parse_decoder_state(state, vm)?)
4306            } else {
4307                None
4308            };
4309
4310            let method = if self.has_read1 { "read1" } else { "read" };
4311            let size_hint = if size_hint > 0 {
4312                (self.b2cratio.max(1.0) * size_hint as f64) as usize
4313            } else {
4314                size_hint
4315            };
4316            let chunk_size = core::cmp::max(self.chunk_size, size_hint);
4317            let input_chunk = vm.call_method(buffer, method, (chunk_size,))?;
4318
4319            let buf = ArgBytesLike::try_from_borrowed_object(vm, &input_chunk).map_err(|_| {
4320                vm.new_type_error(format!(
4321                    "underlying {}() should have returned a bytes-like object, not '{}'",
4322                    method,
4323                    input_chunk.class().name()
4324                ))
4325            })?;
4326            let nbytes = buf.borrow_buf().len();
4327            let eof = nbytes == 0;
4328            let decoded = vm.call_method(decoder, "decode", (input_chunk, eof))?;
4329            let decoded = check_decoded(decoded, vm)?;
4330
4331            let char_len = decoded.char_len();
4332            self.b2cratio = if char_len > 0 {
4333                nbytes as f64 / char_len as f64
4334            } else {
4335                0.0
4336            };
4337            let eof = if char_len > 0 { false } else { eof };
4338            self.set_decoded_chars(Some(decoded));
4339
4340            if let Some((dec_buffer, dec_flags)) = dec_state {
4341                // TODO: inplace append to bytes when refcount == 1
4342                let mut next_input = dec_buffer.as_bytes().to_vec();
4343                next_input.extend_from_slice(&buf.borrow_buf());
4344                self.snapshot = Some((dec_flags, PyBytes::from(next_input).into_ref(&vm.ctx)));
4345            }
4346
4347            Ok(eof)
4348        }
4349
4350        fn check_closed(&self, buffer: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
4351            check_closed(buffer, vm)
4352        }
4353
4354        /// returns str, str.char_len() (it might not be cached in the str yet but we calculate it
4355        /// anyway in this method)
4356        fn get_decoded_chars(
4357            &mut self,
4358            n: usize,
4359            vm: &VirtualMachine,
4360        ) -> Option<(PyStrRef, usize)> {
4361            if n == 0 {
4362                return None;
4363            }
4364            let decoded_chars = self.decoded_chars.as_ref()?;
4365            let avail = &decoded_chars.as_wtf8()[self.decoded_chars_used.bytes..];
4366            if avail.is_empty() {
4367                return None;
4368            }
4369            let avail_chars = decoded_chars.char_len() - self.decoded_chars_used.chars;
4370            let (chars, chars_used) = if n >= avail_chars {
4371                if self.decoded_chars_used.bytes == 0 {
4372                    (decoded_chars.clone(), avail_chars)
4373                } else {
4374                    (PyStr::from(avail).into_ref(&vm.ctx), avail_chars)
4375                }
4376            } else {
4377                let s = crate::common::str::get_codepoints(avail, 0..n);
4378                (PyStr::from(s).into_ref(&vm.ctx), n)
4379            };
4380            self.decoded_chars_used += Utf8size {
4381                bytes: chars.byte_len(),
4382                chars: chars_used,
4383            };
4384            Some((chars, chars_used))
4385        }
4386
4387        fn set_decoded_chars(&mut self, s: Option<PyStrRef>) {
4388            self.decoded_chars = s;
4389            self.decoded_chars_used = Utf8size::default();
4390        }
4391
4392        fn take_decoded_chars(
4393            &mut self,
4394            append: Option<PyStrRef>,
4395            vm: &VirtualMachine,
4396        ) -> PyStrRef {
4397            let empty_str = || vm.ctx.empty_str.to_owned();
4398            let chars_pos = core::mem::take(&mut self.decoded_chars_used).bytes;
4399            let decoded_chars = match core::mem::take(&mut self.decoded_chars) {
4400                None => return append.unwrap_or_else(empty_str),
4401                Some(s) if s.is_empty() => return append.unwrap_or_else(empty_str),
4402                Some(s) => s,
4403            };
4404            let append_len = append.as_ref().map_or(0, |s| s.byte_len());
4405            if append_len == 0 && chars_pos == 0 {
4406                return decoded_chars;
4407            }
4408            // TODO: in-place editing of `str` when refcount == 1
4409            let decoded_chars_unused = &decoded_chars.as_wtf8()[chars_pos..];
4410            let mut s = Wtf8Buf::with_capacity(decoded_chars_unused.len() + append_len);
4411            s.push_wtf8(decoded_chars_unused);
4412            if let Some(append) = append {
4413                s.push_wtf8(append.as_wtf8())
4414            }
4415            PyStr::from(s).into_ref(&vm.ctx)
4416        }
4417    }
4418
4419    impl Destructor for TextIOWrapper {
4420        fn slot_del(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
4421            if let Some(wrapper) = zelf.downcast_ref::<Self>() {
4422                wrapper.finalizing.store(true, Ordering::Relaxed);
4423            }
4424            iobase_finalize(zelf, vm);
4425            Ok(())
4426        }
4427
4428        #[cold]
4429        fn del(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<()> {
4430            unreachable!("slot_del is implemented")
4431        }
4432    }
4433
4434    impl Representable for TextIOWrapper {
4435        #[inline]
4436        fn repr(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyRef<PyStr>> {
4437            let type_name = zelf.class().slot_name();
4438            let Some(_guard) = ReprGuard::enter(vm, zelf.as_object()) else {
4439                return Err(
4440                    vm.new_runtime_error(format!("reentrant call inside {type_name}.__repr__"))
4441                );
4442            };
4443            // Detach while blocked, like `lock_opt`: another thread can be
4444            // stopped holding this mutex, and blocking on it while attached
4445            // would leave no safepoint for that stop to complete at.
4446            let Some(data) = zelf.data.lock_wrapped(|do_lock| vm.allow_threads(do_lock)) else {
4447                // Reentrant call
4448                return Ok(vm.ctx.new_str(Wtf8Buf::from(format!("<{type_name}>"))));
4449            };
4450            if data.is_none() {
4451                return Err(vm.new_value_error("I/O operation on uninitialized object"));
4452            }
4453
4454            let mut result = Wtf8Buf::from(format!("<{type_name}"));
4455            let buffer = zelf.buffer.load_owned();
4456
4457            // Add name if present
4458            if let Some(buffer) = &buffer
4459                && let Ok(Some(name)) = vm.get_attribute_opt(buffer, "name")
4460            {
4461                let name_repr = name.repr(vm)?;
4462                result.push_wtf8(" name=".as_ref());
4463                result.push_wtf8(name_repr.as_wtf8());
4464            }
4465
4466            // Add mode if present (prefer the wrapper's attribute)
4467            let mode_obj = match vm.get_attribute_opt(zelf.as_object(), "mode") {
4468                Ok(Some(mode)) => Some(mode),
4469                Ok(None) | Err(_) => match buffer.as_ref() {
4470                    Some(buffer) => match vm.get_attribute_opt(buffer, "mode") {
4471                        Ok(Some(mode)) => Some(mode),
4472                        _ => None,
4473                    },
4474                    None => None,
4475                },
4476            };
4477            if let Some(mode) = mode_obj {
4478                let mode_repr = mode.repr(vm)?;
4479                result.push_wtf8(" mode=".as_ref());
4480                result.push_wtf8(mode_repr.as_wtf8());
4481            }
4482
4483            // Add encoding (always valid UTF-8)
4484            let encoding = textio_encoding(&zelf.encoding, vm)?;
4485            result.push_wtf8(" encoding='".as_ref());
4486            result.push_wtf8(encoding.as_str().as_ref());
4487            result.push_wtf8("'>".as_ref());
4488
4489            Ok(vm.ctx.new_str(result))
4490        }
4491
4492        fn repr_str(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
4493            unreachable!("repr() is overridden directly")
4494        }
4495    }
4496
4497    impl Iterable for TextIOWrapper {
4498        fn slot_iter(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
4499            check_closed(&zelf, vm)?;
4500            Ok(zelf)
4501        }
4502
4503        fn iter(_zelf: PyRef<Self>, _vm: &VirtualMachine) -> PyResult {
4504            unreachable!("slot_iter is implemented")
4505        }
4506    }
4507
4508    impl IterNext for TextIOWrapper {
4509        fn slot_iternext(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
4510            // Set telling = false during iteration (matches CPython behavior)
4511            let textio_ref: PyRef<Self> = zelf.downcast_ref::<Self>().unwrap().to_owned();
4512            {
4513                let mut textio = textio_ref.lock(vm)?;
4514                textio.telling = false;
4515            }
4516
4517            let line = vm.call_method(zelf, "readline", ())?;
4518
4519            if !line.try_to_bool(vm)? {
4520                // Restore telling on StopIteration
4521                let mut textio = textio_ref.lock(vm)?;
4522                textio.snapshot = None;
4523                textio.telling = textio.seekable;
4524                Ok(PyIterReturn::StopIteration(None))
4525            } else {
4526                Ok(PyIterReturn::Return(line))
4527            }
4528        }
4529
4530        fn next(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyIterReturn> {
4531            unreachable!("slot_iternext is implemented")
4532        }
4533    }
4534
4535    #[pyattr]
4536    #[pyclass(name)]
4537    #[derive(Debug, PyPayload, Default)]
4538    struct IncrementalNewlineDecoder {
4539        // TODO: Traverse
4540        data: PyThreadMutex<Option<IncrementalNewlineDecoderData>>,
4541    }
4542
4543    #[derive(Debug)]
4544    struct IncrementalNewlineDecoderData {
4545        decoder: PyObjectRef,
4546        // currently this is used for nothing
4547        // errors: PyObjectRef,
4548        pendingcr: bool,
4549        translate: bool,
4550        seennl: SeenNewline,
4551    }
4552
4553    bitflags! {
4554        #[derive(Debug, PartialEq, Eq, Copy, Clone)]
4555        struct SeenNewline: u8 {
4556            const LF = 1;
4557            const CR = 2;
4558            const CRLF = 4;
4559        }
4560    }
4561
4562    impl SeenNewline {
4563        fn observe(&mut self, text: &Wtf8) {
4564            let bytes = text.as_bytes();
4565            let mut matches = memchr::memchr2_iter(b'\r', b'\n', bytes);
4566            while !self.is_all() {
4567                let Some(i) = matches.next() else { break };
4568                match bytes[i] {
4569                    b'\n' => self.insert(Self::LF),
4570                    _ if bytes.get(i + 1) == Some(&b'\n') => {
4571                        matches.next();
4572                        self.insert(Self::CRLF);
4573                    }
4574                    _ => self.insert(Self::CR),
4575                }
4576            }
4577        }
4578
4579        fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
4580            match self.bits() {
4581                1 => "\n".to_pyobject(vm),
4582                2 => "\r".to_pyobject(vm),
4583                3 => ("\r", "\n").to_pyobject(vm),
4584                4 => "\r\n".to_pyobject(vm),
4585                5 => ("\n", "\r\n").to_pyobject(vm),
4586                6 => ("\r", "\r\n").to_pyobject(vm),
4587                7 => ("\r", "\n", "\r\n").to_pyobject(vm),
4588                _ => vm.ctx.none(),
4589            }
4590        }
4591    }
4592
4593    impl DefaultConstructor for IncrementalNewlineDecoder {}
4594
4595    #[derive(FromArgs)]
4596    struct IncrementalNewlineDecoderArgs {
4597        #[pyarg(any)]
4598        decoder: PyObjectRef,
4599        #[pyarg(any)]
4600        translate: bool,
4601        // Accepted and ignored. strict is the reported default.
4602        #[pyarg(any, default, py_default = "'strict'")]
4603        errors: Option<PyObjectRef>,
4604    }
4605
4606    impl Initializer for IncrementalNewlineDecoder {
4607        type Args = IncrementalNewlineDecoderArgs;
4608        fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
4609            let _ = args.errors;
4610            let mut data = zelf.lock_opt(vm)?;
4611            *data = Some(IncrementalNewlineDecoderData {
4612                decoder: args.decoder,
4613                translate: args.translate,
4614                pendingcr: false,
4615                seennl: SeenNewline::empty(),
4616            });
4617            Ok(())
4618        }
4619    }
4620
4621    impl IncrementalNewlineDecoder {
4622        fn lock_opt(
4623            &self,
4624            vm: &VirtualMachine,
4625        ) -> PyResult<PyThreadMutexGuard<'_, Option<IncrementalNewlineDecoderData>>> {
4626            self.data
4627                .lock_wrapped(|do_lock| vm.allow_threads(do_lock))
4628                .ok_or_else(|| vm.new_runtime_error("reentrant call inside nldecoder"))
4629        }
4630
4631        fn lock(
4632            &self,
4633            vm: &VirtualMachine,
4634        ) -> PyResult<PyMappedThreadMutexGuard<'_, IncrementalNewlineDecoderData>> {
4635            let lock = self.lock_opt(vm)?;
4636            PyThreadMutexGuard::try_map(lock, |x| x.as_mut())
4637                .map_err(|_| vm.new_value_error("I/O operation on uninitialized nldecoder"))
4638        }
4639    }
4640
4641    #[pyclass(with(Constructor, Initializer))]
4642    impl Py<IncrementalNewlineDecoder> {
4643        #[pymethod]
4644        fn decode(&self, args: NewlineDecodeArgs, vm: &VirtualMachine) -> PyResult<PyStrRef> {
4645            self.lock(vm)?.decode(args.input, args.r#final, vm)
4646        }
4647
4648        #[pymethod]
4649        fn getstate(&self, vm: &VirtualMachine) -> PyResult<(PyObjectRef, u64)> {
4650            let data = self.lock(vm)?;
4651            let (buffer, flag) = if vm.is_none(&data.decoder) {
4652                (vm.ctx.new_bytes(vec![]).into(), 0)
4653            } else {
4654                vm.call_method(&data.decoder, "getstate", ())?
4655                    .try_to_ref::<PyTuple>(vm)?
4656                    .extract_tuple::<(PyObjectRef, u64)>(vm)?
4657            };
4658            let flag = (flag << 1) | (data.pendingcr as u64);
4659            Ok((buffer, flag))
4660        }
4661
4662        #[pymethod]
4663        fn setstate(&self, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> {
4664            let mut data = self.lock(vm)?;
4665            let (buffer, flag) = state.extract_tuple::<(PyObjectRef, u64)>(vm)?;
4666            data.pendingcr = flag & 1 != 0;
4667            if !vm.is_none(&data.decoder) {
4668                vm.call_method(&data.decoder, "setstate", ((buffer, flag >> 1),))?;
4669            }
4670            Ok(())
4671        }
4672
4673        #[pymethod]
4674        fn reset(&self, vm: &VirtualMachine) -> PyResult<()> {
4675            let mut data = self.lock(vm)?;
4676            data.seennl = SeenNewline::empty();
4677            data.pendingcr = false;
4678            if !vm.is_none(&data.decoder) {
4679                vm.call_method(&data.decoder, "reset", ())?;
4680            }
4681            Ok(())
4682        }
4683
4684        #[pygetset]
4685        fn newlines(&self, vm: &VirtualMachine) -> PyResult {
4686            let data = self.lock(vm)?;
4687            Ok(data.seennl.to_pyobject(vm))
4688        }
4689    }
4690
4691    #[derive(FromArgs)]
4692    struct NewlineDecodeArgs {
4693        #[pyarg(any)]
4694        input: PyObjectRef,
4695        #[pyarg(any, default)]
4696        r#final: bool,
4697    }
4698
4699    impl IncrementalNewlineDecoderData {
4700        fn decode(
4701            &mut self,
4702            input: PyObjectRef,
4703            final_: bool,
4704            vm: &VirtualMachine,
4705        ) -> PyResult<PyStrRef> {
4706            let output = if vm.is_none(&self.decoder) {
4707                input
4708            } else {
4709                vm.call_method(&self.decoder, "decode", (input, final_))?
4710            };
4711            let orig_output: PyStrRef = output.try_into_value(vm)?;
4712            // this being Cow::Owned means we need to allocate a new string
4713            let mut output = Cow::Borrowed(orig_output.as_wtf8());
4714            if self.pendingcr && (final_ || !output.is_empty()) {
4715                output.to_mut().insert(0, '\r'.into());
4716                self.pendingcr = false;
4717            }
4718            if !final_ && let Some(s) = output.strip_suffix("\r") {
4719                output = Cow::Owned(s.to_owned());
4720                self.pendingcr = true;
4721            }
4722
4723            if output.is_empty() {
4724                return Ok(vm.ctx.empty_str.to_owned());
4725            }
4726
4727            if (self.seennl == SeenNewline::LF || self.seennl.is_empty())
4728                && !output.contains_code_point('\r'.into())
4729            {
4730                if self.seennl.is_empty() && output.contains_code_point('\n'.into()) {
4731                    self.seennl.insert(SeenNewline::LF);
4732                }
4733            } else if !self.translate {
4734                self.seennl.observe(&output);
4735            } else {
4736                let bytes = output.as_bytes();
4737                let mut matches = memchr::memchr2_iter(b'\r', b'\n', bytes);
4738                let mut new_string = Wtf8Buf::with_capacity(output.len());
4739                let mut last_modification_index = 0;
4740                while let Some(cr_index) = matches.next() {
4741                    if bytes[cr_index] == b'\r' {
4742                        // skip copying the CR
4743                        let mut next_chunk_index = cr_index + 1;
4744                        if bytes.get(cr_index + 1) == Some(&b'\n') {
4745                            matches.next();
4746                            self.seennl.insert(SeenNewline::CRLF);
4747                            // skip the LF too
4748                            next_chunk_index += 1;
4749                        } else {
4750                            self.seennl.insert(SeenNewline::CR);
4751                        }
4752                        new_string.push_wtf8(&output[last_modification_index..cr_index]);
4753                        new_string.push_char('\n');
4754                        last_modification_index = next_chunk_index;
4755                    } else {
4756                        self.seennl.insert(SeenNewline::LF);
4757                    }
4758                }
4759                new_string.push_wtf8(&output[last_modification_index..]);
4760                output = Cow::Owned(new_string);
4761            }
4762
4763            Ok(match output {
4764                Cow::Borrowed(_) => orig_output,
4765                Cow::Owned(s) => vm.ctx.new_str(s),
4766            })
4767        }
4768    }
4769
4770    #[pyattr]
4771    #[pyclass(name = "StringIO", base = _TextIOBase)]
4772    #[derive(Debug)]
4773    struct StringIO {
4774        _base: _TextIOBase,
4775        buffer: PyRwLock<BufferedIO>,
4776        newline: AtomicCell<Newlines>,
4777        seennl: AtomicCell<SeenNewline>,
4778        closed: AtomicCell<bool>,
4779    }
4780
4781    #[derive(FromArgs)]
4782    struct StringIONewArgs {
4783        // None means an empty buffer.
4784        #[pyarg(any, optional, py_default = "''")]
4785        initial_value: Option<PyStrRef>,
4786
4787        // Omitted newline is \n. None selects universal newlines.
4788        #[pyarg(any, default, py_default = "'\\n'")]
4789        newline: OptionalOption<Newlines>,
4790    }
4791
4792    impl Constructor for StringIO {
4793        type Args = FuncArgs;
4794
4795        fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
4796            Ok(Self {
4797                _base: Default::default(),
4798                buffer: PyRwLock::new(BufferedIO::new(Cursor::new(Vec::new()))),
4799                newline: AtomicCell::new(Newlines::Lf),
4800                seennl: AtomicCell::new(SeenNewline::empty()),
4801                closed: AtomicCell::new(false),
4802            })
4803        }
4804    }
4805
4806    impl Initializer for StringIO {
4807        type Args = StringIONewArgs;
4808
4809        fn init(
4810            zelf: &Py<Self>,
4811            Self::Args {
4812                initial_value,
4813                newline,
4814            }: Self::Args,
4815            _vm: &VirtualMachine,
4816        ) -> PyResult<()> {
4817            let newline = match newline {
4818                OptionalArg::Missing => Newlines::Lf,
4819                OptionalArg::Present(None) => Newlines::Universal,
4820                OptionalArg::Present(Some(newline)) => newline,
4821            };
4822            let raw_bytes = initial_value.as_ref().map_or_else(Vec::new, |v| {
4823                Self::translate_newlines(v.as_wtf8(), newline).into_bytes()
4824            });
4825            *zelf.buffer.write() = BufferedIO::new(Cursor::new(raw_bytes));
4826            zelf.newline.store(newline);
4827            zelf.seennl.store(SeenNewline::empty());
4828            if let Some(initial_value) = initial_value {
4829                zelf.observe_newlines(initial_value.as_wtf8(), newline);
4830            }
4831            Ok(())
4832        }
4833    }
4834
4835    impl StringIO {
4836        fn buffer(&self, vm: &VirtualMachine) -> PyResult<PyRwLockWriteGuard<'_, BufferedIO>> {
4837            if !self.closed.load() {
4838                Ok(self.buffer.write())
4839            } else {
4840                Err(io_closed_error(vm))
4841            }
4842        }
4843
4844        fn translate_newlines(data: &Wtf8, newline: Newlines) -> Wtf8Buf {
4845            match newline {
4846                Newlines::Universal => data
4847                    .replace("\r\n".as_ref(), "\n".as_ref())
4848                    .replace("\r".as_ref(), "\n".as_ref()),
4849                Newlines::Cr => data.replace("\n".as_ref(), "\r".as_ref()),
4850                Newlines::Crlf => data.replace("\n".as_ref(), "\r\n".as_ref()),
4851                Newlines::Passthrough | Newlines::Lf => data.to_owned(),
4852            }
4853        }
4854
4855        fn observe_newlines(&self, data: &Wtf8, newline: Newlines) {
4856            if matches!(newline, Newlines::Universal | Newlines::Passthrough) {
4857                let mut seennl = self.seennl.load();
4858                seennl.observe(data);
4859                self.seennl.store(seennl);
4860            }
4861        }
4862
4863        fn text(bytes: &[u8]) -> &Wtf8 {
4864            // SAFETY: StringIO is populated only from PyStr values, which are valid WTF-8.
4865            unsafe { Wtf8::from_bytes_unchecked(bytes) }
4866        }
4867
4868        fn char_offset_to_byte(bytes: &[u8], char_offset: usize) -> usize {
4869            let text = Self::text(bytes);
4870            crate::common::str::codepoint_range_end(text, char_offset)
4871                .unwrap_or_else(|| bytes.len() + (char_offset - text.code_points().count()))
4872        }
4873
4874        fn byte_offset_to_char(bytes: &[u8], byte_offset: usize) -> usize {
4875            let content_len = bytes.len();
4876            let in_content = byte_offset.min(content_len);
4877            Self::text(&bytes[..in_content]).code_points().count()
4878                + byte_offset.saturating_sub(content_len)
4879        }
4880
4881        fn read_size(buffer: &BufferedIO, size: Option<usize>, newline: Option<Newlines>) -> usize {
4882            let position = buffer.tell() as usize;
4883            let bytes = buffer.cursor.get_ref().get(position..).unwrap_or_default();
4884            let size_end = size
4885                .and_then(|size| crate::common::str::codepoint_range_end(Self::text(bytes), size))
4886                .unwrap_or(bytes.len());
4887            newline
4888                .and_then(|newline| newline.find_newline(Self::text(&bytes[..size_end])).ok())
4889                .unwrap_or(size_end)
4890        }
4891    }
4892
4893    #[pyclass(flags(BASETYPE, HAS_DICT, HAS_WEAKREF), with(Constructor, Initializer))]
4894    impl Py<StringIO> {
4895        #[pymethod]
4896        const fn readable(&self) -> bool {
4897            true
4898        }
4899
4900        #[pymethod]
4901        const fn writable(&self) -> bool {
4902            true
4903        }
4904
4905        #[pymethod]
4906        const fn seekable(&self) -> bool {
4907            true
4908        }
4909
4910        #[pygetset]
4911        fn closed(&self) -> bool {
4912            self.closed.load()
4913        }
4914
4915        #[pygetset]
4916        fn newlines(&self, vm: &VirtualMachine) -> PyResult {
4917            if self.closed.load() {
4918                Err(io_closed_error(vm))
4919            } else {
4920                Ok(self.seennl.load().to_pyobject(vm))
4921            }
4922        }
4923
4924        #[pymethod]
4925        fn close(&self) {
4926            self.closed.store(true);
4927        }
4928
4929        // write string to underlying vector
4930        #[pymethod]
4931        fn write(&self, s: PyStrRef, vm: &VirtualMachine) -> PyResult<u64> {
4932            let newline = self.newline.load();
4933            let bytes = StringIO::translate_newlines(s.as_wtf8(), newline).into_bytes();
4934            let mut buffer = self.buffer(vm)?;
4935            self.observe_newlines(s.as_wtf8(), newline);
4936            buffer
4937                .write(&bytes)
4938                .ok_or_else(|| vm.new_type_error("Error Writing String"))?;
4939            Ok(s.char_len() as u64)
4940        }
4941
4942        // return the entire contents of the underlying
4943        #[pymethod]
4944        fn getvalue(&self, vm: &VirtualMachine) -> PyResult<Wtf8Buf> {
4945            let bytes = self.buffer(vm)?.getvalue();
4946            Wtf8Buf::from_bytes(bytes).map_err(|_| vm.new_value_error("Error Retrieving Value"))
4947        }
4948
4949        // skip to the jth position
4950        #[pymethod]
4951        fn seek(&self, pos: PyObjectRef, whence: HowArg, vm: &VirtualMachine) -> PyResult<u64> {
4952            let offset: isize = isize::try_from_object(vm, pos)?;
4953            let how = whence.whence;
4954            let mut buffer = self.buffer(vm)?;
4955            let char_offset = match how {
4956                0 if offset >= 0 => offset as usize,
4957                0 => return Err(vm.new_value_error(format!("negative seek position {offset}"))),
4958                1 | 2 if offset != 0 => {
4959                    let kind = if how == 1 { "cur" } else { "end" };
4960                    return Err(vm.new_os_error(format!("can't do nonzero {kind}-relative seeks")));
4961                }
4962                1 | 2 => {
4963                    let byte_offset = if how == 1 {
4964                        buffer.tell() as usize
4965                    } else {
4966                        buffer.cursor.get_ref().len()
4967                    };
4968                    StringIO::byte_offset_to_char(buffer.cursor.get_ref(), byte_offset)
4969                }
4970                _ => {
4971                    return Err(
4972                        vm.new_value_error(format!("invalid whence ({how}, should be 0, 1 or 2)"))
4973                    );
4974                }
4975            };
4976
4977            let byte_offset = StringIO::char_offset_to_byte(buffer.cursor.get_ref(), char_offset);
4978            buffer
4979                .seek(SeekFrom::Start(byte_offset as u64))
4980                .map_err(|err| os_err(vm, err))?;
4981            Ok(char_offset as u64)
4982        }
4983
4984        // Read k bytes from the object and return.
4985        // If k is undefined || k == -1, then we read all bytes until the end of the file.
4986        // This also increments the stream position by the value of k
4987        #[pymethod]
4988        fn read(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult<Wtf8Buf> {
4989            let mut buffer = self.buffer(vm)?;
4990            let size = StringIO::read_size(&buffer, size.to_usize(), None);
4991            let data = buffer.read(Some(size)).unwrap_or_default();
4992
4993            let value = Wtf8Buf::from_bytes(data)
4994                .map_err(|_| vm.new_value_error("Error Retrieving Value"))?;
4995            Ok(value)
4996        }
4997
4998        #[pymethod]
4999        fn tell(&self, vm: &VirtualMachine) -> PyResult<u64> {
5000            let buffer = self.buffer(vm)?;
5001            Ok(
5002                StringIO::byte_offset_to_char(buffer.cursor.get_ref(), buffer.tell() as usize)
5003                    as u64,
5004            )
5005        }
5006
5007        #[pymethod]
5008        fn readline(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult<Wtf8Buf> {
5009            let mut buffer = self.buffer(vm)?;
5010            let size = StringIO::read_size(&buffer, size.to_usize(), Some(self.newline.load()));
5011            let input = buffer.read(Some(size)).unwrap_or_default();
5012            Wtf8Buf::from_bytes(input).map_err(|_| vm.new_value_error("Error Retrieving Value"))
5013        }
5014
5015        #[pymethod]
5016        fn truncate(&self, pos: StringIOPos, vm: &VirtualMachine) -> PyResult<usize> {
5017            let mut buffer = self.buffer(vm)?;
5018            let pos = match pos.as_optional().try_usize(vm)? {
5019                Some(pos) => pos,
5020                None => {
5021                    StringIO::byte_offset_to_char(buffer.cursor.get_ref(), buffer.tell() as usize)
5022                }
5023            };
5024            let byte_pos = StringIO::char_offset_to_byte(buffer.cursor.get_ref(), pos);
5025            buffer.truncate(Some(byte_pos));
5026            Ok(pos)
5027        }
5028
5029        #[pygetset]
5030        const fn line_buffering(&self) -> bool {
5031            false
5032        }
5033
5034        #[pymethod]
5035        fn __getstate__(zelf: PyRef<StringIO>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
5036            let buffer = zelf.buffer(vm)?;
5037            let content = Wtf8Buf::from_bytes(buffer.getvalue())
5038                .map_err(|_| vm.new_value_error("Error Retrieving Value"))?;
5039            let pos =
5040                StringIO::byte_offset_to_char(buffer.cursor.get_ref(), buffer.tell() as usize);
5041            drop(buffer);
5042
5043            // Get __dict__ if it exists and is non-empty
5044            let dict_obj: PyObjectRef = match zelf.as_object().dict() {
5045                Some(d) if !d.is_empty() => d.into(),
5046                _ => vm.ctx.none(),
5047            };
5048
5049            let newline = match zelf.newline.load() {
5050                Newlines::Universal => vm.ctx.none(),
5051                Newlines::Passthrough => vm.ctx.new_str("").into(),
5052                Newlines::Lf => vm.ctx.new_str("\n").into(),
5053                Newlines::Cr => vm.ctx.new_str("\r").into(),
5054                Newlines::Crlf => vm.ctx.new_str("\r\n").into(),
5055            };
5056
5057            // Return (content, newline, position, dict)
5058            Ok(vm.ctx.new_tuple(vec![
5059                vm.ctx.new_str(content).into(),
5060                newline,
5061                vm.ctx.new_int(pos).into(),
5062                dict_obj,
5063            ]))
5064        }
5065
5066        #[pymethod]
5067        fn __setstate__(
5068            zelf: PyRef<StringIO>,
5069            state: PyTupleRef,
5070            vm: &VirtualMachine,
5071        ) -> PyResult<()> {
5072            // Check closed state first (like CHECK_CLOSED)
5073            if zelf.closed.load() {
5074                return Err(vm.new_value_error("__setstate__ on closed file"));
5075            }
5076            if state.as_slice().len() != 4 {
5077                return Err(vm.new_type_error(format!(
5078                    "__setstate__ argument should be 4-tuple, got {}",
5079                    state.as_slice().len()
5080                )));
5081            }
5082
5083            let content: PyStrRef = state.as_slice()[0].clone().try_into_value(vm)?;
5084            let newline = Newlines::try_from_object(vm, state.as_slice()[1].clone())?;
5085            let pos: isize = isize::try_from_object(vm, state.as_slice()[2].clone())?;
5086            if pos < 0 {
5087                return Err(vm.new_value_error("negative seek position"));
5088            }
5089            let dict = &state.as_slice()[3];
5090
5091            // Set content and position
5092            let raw_bytes = content.as_bytes().to_vec();
5093            let mut buffer = zelf.buffer.write();
5094            *buffer = BufferedIO::new(Cursor::new(raw_bytes));
5095            let byte_pos = StringIO::char_offset_to_byte(buffer.cursor.get_ref(), pos as usize);
5096            buffer
5097                .seek(SeekFrom::Start(byte_pos as u64))
5098                .map_err(|err| os_err(vm, err))?;
5099            drop(buffer);
5100            zelf.newline.store(newline);
5101            let mut seennl = SeenNewline::empty();
5102            if matches!(newline, Newlines::Universal | Newlines::Passthrough) {
5103                seennl.observe(content.as_wtf8());
5104            }
5105            zelf.seennl.store(seennl);
5106
5107            // Set __dict__ if provided
5108            if !vm.is_none(dict) {
5109                let dict_ref: PyRef<PyDict> = dict.clone().try_into_value(vm)?;
5110                if let Some(obj_dict) = zelf.as_object().dict() {
5111                    obj_dict.clear();
5112                    for (key, value) in dict_ref {
5113                        obj_dict.set_item(&*key, value, vm)?;
5114                    }
5115                }
5116            }
5117
5118            Ok(())
5119        }
5120    }
5121
5122    #[derive(FromArgs)]
5123    struct BytesIOArgs {
5124        // None means an empty buffer.
5125        #[pyarg(any, optional, py_default = "b''")]
5126        initial_bytes: Option<ArgBytesLike>,
5127    }
5128
5129    #[pyattr]
5130    #[pyclass(name = "BytesIO", base = _BufferedIOBase)]
5131    #[derive(Debug)]
5132    struct BytesIO {
5133        _base: _BufferedIOBase,
5134        buffer: PyRwLock<BufferedIO>,
5135        closed: AtomicCell<bool>,
5136        exports: AtomicCell<usize>,
5137    }
5138
5139    impl Constructor for BytesIO {
5140        type Args = FuncArgs;
5141
5142        fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
5143            Ok(Self {
5144                _base: Default::default(),
5145                buffer: PyRwLock::new(BufferedIO::new(Cursor::new(Vec::new()))),
5146                closed: AtomicCell::new(false),
5147                exports: AtomicCell::new(0),
5148            })
5149        }
5150    }
5151
5152    impl Initializer for BytesIO {
5153        type Args = BytesIOArgs;
5154
5155        fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
5156            if zelf.exports.load() > 0 {
5157                return Err(
5158                    vm.new_buffer_error("Existing exports of data: object cannot be re-sized")
5159                );
5160            }
5161
5162            let raw_bytes = args
5163                .initial_bytes
5164                .map_or_else(Vec::new, |input| input.borrow_buf().to_vec());
5165            *zelf.buffer.write() = BufferedIO::new(Cursor::new(raw_bytes));
5166            Ok(())
5167        }
5168    }
5169
5170    impl BytesIO {
5171        fn buffer(&self, vm: &VirtualMachine) -> PyResult<PyRwLockWriteGuard<'_, BufferedIO>> {
5172            if !self.closed.load() {
5173                Ok(self.buffer.write())
5174            } else {
5175                Err(io_closed_error(vm))
5176            }
5177        }
5178    }
5179
5180    #[pyclass(
5181        flags(BASETYPE, HAS_DICT, HAS_WEAKREF),
5182        with(PyRef, Constructor, Initializer)
5183    )]
5184    impl Py<BytesIO> {
5185        #[pymethod]
5186        const fn readable(&self) -> bool {
5187            true
5188        }
5189
5190        #[pymethod]
5191        const fn writable(&self) -> bool {
5192            true
5193        }
5194
5195        #[pymethod]
5196        const fn seekable(&self) -> bool {
5197            true
5198        }
5199
5200        #[pymethod]
5201        fn flush(&self, vm: &VirtualMachine) -> PyResult<()> {
5202            if self.closed.load() {
5203                Err(io_closed_error(vm))
5204            } else {
5205                Ok(())
5206            }
5207        }
5208
5209        #[pymethod]
5210        fn write(&self, b: ArgContiguousBytesLike, vm: &VirtualMachine) -> PyResult<u64> {
5211            let mut buffer = self.try_resizable(vm)?;
5212            // Acquiring the buffer can run `__buffer__`, which may have closed us.
5213            if self.closed.load() {
5214                return Err(io_closed_error(vm));
5215            }
5216            b.with_ref(|bytes| buffer.write(bytes))
5217                .ok_or_else(|| vm.new_type_error("Error Writing Bytes"))
5218        }
5219
5220        // Retrieves the entire bytes object value from the underlying buffer
5221        #[pymethod]
5222        fn getvalue(&self, vm: &VirtualMachine) -> PyResult<PyBytesRef> {
5223            let bytes = self.buffer(vm)?.getvalue();
5224            Ok(vm.ctx.new_bytes(bytes))
5225        }
5226
5227        // Takes an integer k (bytes) and returns them from the underlying buffer
5228        // If k is undefined || k == -1, then we read all bytes until the end of the file.
5229        // This also increments the stream position by the value of k
5230        #[pymethod]
5231        #[pymethod(name = "read1")]
5232        fn read(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
5233            let buf = self.buffer(vm)?.read(size.to_usize()).unwrap_or_default();
5234            Ok(buf)
5235        }
5236
5237        #[pymethod]
5238        fn readinto(zelf: &Self, buffer: ArgMemoryBuffer, vm: &VirtualMachine) -> PyResult<usize> {
5239            // Reading locks this object, and a destination that views it locks
5240            // it too, so such a destination is filled after the read is done.
5241            if buffer.source_object().is(zelf.as_object()) {
5242                let mut data = vm.new_zeroed_bytes(buffer.len())?;
5243                let ret = zelf
5244                    .buffer(vm)?
5245                    .cursor
5246                    .read(&mut data)
5247                    .map_err(|_| vm.new_value_error("Error readinto from Take"))?;
5248                buffer.borrow_buf_mut()[..ret].copy_from_slice(&data[..ret]);
5249                return Ok(ret);
5250            }
5251            let mut buf = zelf.buffer(vm)?;
5252            let ret = buf
5253                .cursor
5254                .read(&mut buffer.borrow_buf_mut())
5255                .map_err(|_| vm.new_value_error("Error readinto from Take"))?;
5256
5257            Ok(ret)
5258        }
5259
5260        //skip to the jth position
5261        #[pymethod]
5262        fn seek(&self, pos: PyObjectRef, whence: HowArg, vm: &VirtualMachine) -> PyResult<u64> {
5263            let seek_from = seekfrom(vm, pos, whence.whence)?;
5264            let mut buffer = self.buffer(vm)?;
5265
5266            // Handle negative positions by clamping to 0
5267            match seek_from {
5268                SeekFrom::Current(offset) if offset < 0 => {
5269                    let current = buffer.tell();
5270                    let new_pos = current.saturating_add_signed(offset);
5271                    buffer
5272                        .seek(SeekFrom::Start(new_pos))
5273                        .map_err(|err| os_err(vm, err))
5274                }
5275                _ => buffer.seek(seek_from).map_err(|err| os_err(vm, err)),
5276            }
5277        }
5278
5279        #[pymethod]
5280        fn tell(&self, vm: &VirtualMachine) -> PyResult<u64> {
5281            Ok(self.buffer(vm)?.tell())
5282        }
5283
5284        #[pymethod]
5285        fn readline(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
5286            self.buffer(vm)?.readline(size.to_usize(), vm)
5287        }
5288
5289        #[pymethod]
5290        fn truncate(&self, pos: OptionalPos, vm: &VirtualMachine) -> PyResult<usize> {
5291            if self.closed.load() {
5292                return Err(io_closed_error(vm));
5293            }
5294            let mut buffer = self.try_resizable(vm)?;
5295            let pos = pos.try_usize(vm)?;
5296            Ok(buffer.truncate(pos))
5297        }
5298
5299        #[pygetset]
5300        fn closed(&self) -> bool {
5301            self.closed.load()
5302        }
5303
5304        #[pymethod]
5305        fn close(&self, vm: &VirtualMachine) -> PyResult<()> {
5306            if self.exports.load() > 0 {
5307                return Err(
5308                    vm.new_buffer_error("Existing exports of data: object cannot be closed")
5309                );
5310            }
5311            self.closed.store(true);
5312            Ok(())
5313        }
5314
5315        #[pymethod]
5316        fn __getstate__(zelf: PyRef<BytesIO>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
5317            let buffer = zelf.buffer(vm)?;
5318            let content = buffer.getvalue();
5319            let pos = buffer.tell();
5320            drop(buffer);
5321
5322            // Get __dict__ if it exists and is non-empty
5323            let dict_obj: PyObjectRef = match zelf.as_object().dict() {
5324                Some(d) if !d.is_empty() => d.into(),
5325                _ => vm.ctx.none(),
5326            };
5327
5328            // Return (content, position, dict)
5329            Ok(vm.ctx.new_tuple(vec![
5330                vm.ctx.new_bytes(content).into(),
5331                vm.ctx.new_int(pos).into(),
5332                dict_obj,
5333            ]))
5334        }
5335
5336        #[pymethod]
5337        fn __setstate__(
5338            zelf: PyRef<BytesIO>,
5339            object: PyTupleRef,
5340            vm: &VirtualMachine,
5341        ) -> PyResult<()> {
5342            if zelf.closed.load() {
5343                return Err(vm.new_value_error("__setstate__ on closed file"));
5344            }
5345            if object.as_slice().len() != 3 {
5346                return Err(vm.new_type_error(format!(
5347                    "__setstate__ argument should be 3-tuple, got {}",
5348                    object.as_slice().len()
5349                )));
5350            }
5351
5352            let content: PyBytesRef = object.as_slice()[0].clone().try_into_value(vm)?;
5353            let pos: u64 = object.as_slice()[1].clone().try_into_value(vm)?;
5354            let dict = &object.as_slice()[2];
5355
5356            // Check exports and set content (like CHECK_EXPORTS)
5357            let mut buffer = zelf.try_resizable(vm)?;
5358            *buffer = BufferedIO::new(Cursor::new(content.as_bytes().to_vec()));
5359            buffer
5360                .seek(SeekFrom::Start(pos))
5361                .map_err(|err| os_err(vm, err))?;
5362            drop(buffer);
5363
5364            // Set __dict__ if provided
5365            if !vm.is_none(dict) {
5366                let dict_ref: PyRef<PyDict> = dict.clone().try_into_value(vm)?;
5367                if let Some(obj_dict) = zelf.as_object().dict() {
5368                    obj_dict.clear();
5369                    for (key, value) in dict_ref {
5370                        obj_dict.set_item(&*key, value, vm)?;
5371                    }
5372                }
5373            }
5374
5375            Ok(())
5376        }
5377
5378        #[pymethod]
5379        fn isatty(&self, vm: &VirtualMachine) -> PyResult<bool> {
5380            if self.closed() {
5381                return Err(io_closed_error(vm));
5382            }
5383
5384            Ok(false)
5385        }
5386    }
5387
5388    #[pyclass]
5389    impl PyRef<BytesIO> {
5390        #[pymethod]
5391        fn getbuffer(self, vm: &VirtualMachine) -> PyResult<PyMemoryView> {
5392            if self.closed.load() {
5393                return Err(vm.new_value_error("I/O operation on closed file."));
5394            }
5395            let len = self.buffer.read().cursor.get_ref().len();
5396            let buffer = PyBuffer::new(
5397                self.into(),
5398                BufferDescriptor::simple(len, false),
5399                &BYTES_IO_BUFFER_METHODS,
5400            );
5401            let view = PyMemoryView::from_buffer(buffer, vm)?;
5402            Ok(view)
5403        }
5404    }
5405
5406    static BYTES_IO_BUFFER_METHODS: BufferMethods = BufferMethods {
5407        obj_bytes: |buffer| {
5408            let zelf = buffer.obj_as::<BytesIO>();
5409            PyRwLockReadGuard::map(zelf.buffer.read(), |x| x.cursor.get_ref().as_slice()).into()
5410        },
5411        obj_bytes_mut: |buffer| {
5412            let zelf = buffer.obj_as::<BytesIO>();
5413            PyRwLockWriteGuard::map(zelf.buffer.write(), |x| x.cursor.get_mut().as_mut_slice())
5414                .into()
5415        },
5416
5417        release: |buffer| {
5418            buffer.obj_as::<BytesIO>().exports.fetch_sub(1);
5419        },
5420
5421        retain: |buffer| {
5422            buffer.obj_as::<BytesIO>().exports.fetch_add(1);
5423        },
5424    };
5425
5426    impl BufferResizeGuard for BytesIO {
5427        type Resizable<'a> = PyRwLockWriteGuard<'a, BufferedIO>;
5428
5429        fn try_resizable_opt(&self) -> Option<Self::Resizable<'_>> {
5430            let w = self.buffer.write();
5431            (self.exports.load() == 0).then_some(w)
5432        }
5433    }
5434
5435    #[repr(u8)]
5436    #[derive(Debug)]
5437    enum FileMode {
5438        Read = b'r',
5439        Write = b'w',
5440        Exclusive = b'x',
5441        Append = b'a',
5442    }
5443
5444    #[repr(u8)]
5445    #[derive(Debug)]
5446    enum EncodeMode {
5447        Text = b't',
5448        Bytes = b'b',
5449    }
5450
5451    #[derive(Debug)]
5452    struct Mode {
5453        file: FileMode,
5454        encode: EncodeMode,
5455        plus: bool,
5456    }
5457
5458    impl core::str::FromStr for Mode {
5459        type Err = ParseModeError;
5460
5461        fn from_str(s: &str) -> Result<Self, Self::Err> {
5462            let mut file = None;
5463            let mut encode = None;
5464            let mut plus = false;
5465            macro_rules! set_mode {
5466                ($var:ident, $mode:path, $err:ident) => {{
5467                    match $var {
5468                        Some($mode) => return Err(ParseModeError::InvalidMode),
5469                        Some(_) => return Err(ParseModeError::$err),
5470                        None => $var = Some($mode),
5471                    }
5472                }};
5473            }
5474
5475            for ch in s.chars() {
5476                match ch {
5477                    '+' => {
5478                        if plus {
5479                            return Err(ParseModeError::InvalidMode);
5480                        }
5481                        plus = true
5482                    }
5483                    't' => set_mode!(encode, EncodeMode::Text, MultipleEncode),
5484                    'b' => set_mode!(encode, EncodeMode::Bytes, MultipleEncode),
5485                    'r' => set_mode!(file, FileMode::Read, MultipleFile),
5486                    'a' => set_mode!(file, FileMode::Append, MultipleFile),
5487                    'w' => set_mode!(file, FileMode::Write, MultipleFile),
5488                    'x' => set_mode!(file, FileMode::Exclusive, MultipleFile),
5489                    _ => return Err(ParseModeError::InvalidMode),
5490                }
5491            }
5492
5493            let file = file.ok_or(ParseModeError::NoFile)?;
5494            let encode = encode.unwrap_or(EncodeMode::Text);
5495
5496            Ok(Self { file, encode, plus })
5497        }
5498    }
5499
5500    impl Mode {
5501        const fn rawmode(&self) -> &'static str {
5502            match (&self.file, self.plus) {
5503                (FileMode::Read, true) => "rb+",
5504                (FileMode::Read, false) => "rb",
5505                (FileMode::Write, true) => "wb+",
5506                (FileMode::Write, false) => "wb",
5507                (FileMode::Exclusive, true) => "xb+",
5508                (FileMode::Exclusive, false) => "xb",
5509                (FileMode::Append, true) => "ab+",
5510                (FileMode::Append, false) => "ab",
5511            }
5512        }
5513    }
5514
5515    enum ParseModeError {
5516        InvalidMode,
5517        MultipleFile,
5518        MultipleEncode,
5519        NoFile,
5520    }
5521
5522    impl ParseModeError {
5523        fn error_msg(&self, mode_string: &str) -> String {
5524            match self {
5525                Self::InvalidMode => format!("invalid mode: '{mode_string}'"),
5526                Self::MultipleFile => {
5527                    "must have exactly one of create/read/write/append mode".to_owned()
5528                }
5529                Self::MultipleEncode => "can't have text and binary mode at once".to_owned(),
5530                Self::NoFile => {
5531                    "Must have exactly one of create/read/write/append mode and at most one plus"
5532                        .to_owned()
5533                }
5534            }
5535        }
5536    }
5537
5538    #[derive(FromArgs)]
5539    struct IoOpenArgs {
5540        file: PyObjectRef,
5541        #[pyarg(any, default = "r")]
5542        mode: PyUtf8StrRef,
5543        #[pyarg(flatten)]
5544        opts: OpenArgs,
5545    }
5546
5547    #[pyfunction]
5548    fn open(args: IoOpenArgs, vm: &VirtualMachine) -> PyResult {
5549        io_open(args.file, Some(args.mode.as_str()), args.opts, vm)
5550    }
5551
5552    #[derive(FromArgs)]
5553    struct OpenCodeArgs {
5554        #[pyarg(any)]
5555        path: PyObjectRef,
5556    }
5557
5558    #[pyfunction]
5559    fn open_code(args: OpenCodeArgs, vm: &VirtualMachine) -> PyResult {
5560        // TODO: lifecycle hooks or something?
5561        io_open(args.path, Some("rb"), OpenArgs::default(), vm)
5562    }
5563
5564    #[derive(FromArgs)]
5565    pub struct OpenArgs {
5566        #[pyarg(any, default = -1)]
5567        pub buffering: isize,
5568        #[pyarg(any, optional)]
5569        pub encoding: Option<PyUtf8StrRef>,
5570        #[pyarg(any, optional)]
5571        pub errors: Option<PyUtf8StrRef>,
5572        #[pyarg(any, optional)]
5573        pub newline: Option<PyUtf8StrRef>,
5574        #[pyarg(any, default = true)]
5575        pub closefd: bool,
5576        #[pyarg(any, optional)]
5577        pub opener: Option<PyObjectRef>,
5578    }
5579
5580    impl Default for OpenArgs {
5581        fn default() -> Self {
5582            Self {
5583                buffering: -1,
5584                encoding: None,
5585                errors: None,
5586                newline: None,
5587                closefd: true,
5588                opener: None,
5589            }
5590        }
5591    }
5592
5593    /// Reinit per-object IO buffer locks on std streams after `fork()`.
5594    ///
5595    /// # Safety
5596    ///
5597    /// Must only be called from the single-threaded child process immediately
5598    /// after `fork()`, before any other thread is created.
5599    #[cfg(all(unix, feature = "threading", feature = "host_env"))]
5600    pub(crate) unsafe fn reinit_std_streams_after_fork(vm: &VirtualMachine) {
5601        for name in ["stdin", "stdout", "stderr"] {
5602            let Ok(stream) = vm.sys_module.get_attr(name, vm) else {
5603                continue;
5604            };
5605            reinit_io_locks(&stream);
5606        }
5607    }
5608
5609    #[cfg(all(unix, feature = "threading", feature = "host_env"))]
5610    fn reinit_io_locks(obj: &PyObject) {
5611        use crate::common::lock::reinit_thread_mutex_after_fork;
5612
5613        if let Some(tio) = obj.downcast_ref::<TextIOWrapper>() {
5614            unsafe { reinit_thread_mutex_after_fork(&tio.data) };
5615            if let Some(guard) = tio.data.lock()
5616                && let Some(ref data) = *guard
5617            {
5618                if let Some(ref decoder) = data.decoder {
5619                    reinit_io_locks(decoder);
5620                }
5621                if let Some(buffer) = tio.buffer.load_owned() {
5622                    reinit_io_locks(&buffer);
5623                }
5624            }
5625            return;
5626        }
5627        if let Some(nl) = obj.downcast_ref::<IncrementalNewlineDecoder>() {
5628            unsafe { reinit_thread_mutex_after_fork(&nl.data) };
5629            return;
5630        }
5631        if let Some(br) = obj.downcast_ref::<BufferedReader>() {
5632            unsafe { reinit_thread_mutex_after_fork(&br.data) };
5633            return;
5634        }
5635        if let Some(bw) = obj.downcast_ref::<BufferedWriter>() {
5636            unsafe { reinit_thread_mutex_after_fork(&bw.data) };
5637            return;
5638        }
5639        if let Some(brw) = obj.downcast_ref::<BufferedRandom>() {
5640            unsafe { reinit_thread_mutex_after_fork(&brw.data) };
5641            return;
5642        }
5643        if let Some(brw) = obj.downcast_ref::<BufferedRWPair>() {
5644            unsafe { reinit_thread_mutex_after_fork(&brw.read.data) };
5645            unsafe { reinit_thread_mutex_after_fork(&brw.write.data) };
5646        }
5647    }
5648
5649    pub fn io_open(
5650        file: PyObjectRef,
5651        mode: Option<&str>,
5652        opts: OpenArgs,
5653        vm: &VirtualMachine,
5654    ) -> PyResult {
5655        // mode is optional: 'rt' is the default mode (open from reading text)
5656        let mode_string = mode.unwrap_or("r");
5657        let mode = mode_string
5658            .parse::<Mode>()
5659            .map_err(|e| vm.new_value_error(e.error_msg(mode_string)))?;
5660
5661        if let EncodeMode::Bytes = mode.encode {
5662            let msg = if opts.encoding.is_some() {
5663                Some("binary mode doesn't take an encoding argument")
5664            } else if opts.errors.is_some() {
5665                Some("binary mode doesn't take an errors argument")
5666            } else if opts.newline.is_some() {
5667                Some("binary mode doesn't take a newline argument")
5668            } else {
5669                None
5670            };
5671            if let Some(msg) = msg {
5672                return Err(vm.new_value_error(msg));
5673            }
5674        }
5675
5676        // Match CPython's _pyio.open (Lib/_pyio.py): resolve PathLike here so
5677        // _io.FileIO still receives (and preserves on .name) the raw argument
5678        // when called directly.
5679        let file = if file.fast_isinstance(vm.ctx.types.int_type) {
5680            file
5681        } else {
5682            FsPath::try_from_path_like(file, true, vm)?.to_pyobject(vm)
5683        };
5684
5685        // check file descriptor validity
5686        #[cfg(all(unix, feature = "host_env"))]
5687        if let Ok(crate::ospath::OsPathOrFd::Fd(fd)) = file.clone().try_into_value(vm) {
5688            rustpython_host_env::fcntl::validate_fd(fd.as_raw())
5689                .map_err(|_| vm.new_last_errno_error())?;
5690        }
5691
5692        // Construct a RawIO (subclass of RawIOBase)
5693        // On Windows, use _WindowsConsoleIO for console handles.
5694        // This is subsequently consumed by a Buffered Class.
5695        let is_console = cfg_select! {
5696            all(feature = "host_env", windows) => {
5697                super::winconsoleio::pyio_get_console_type(&file, vm) != '\0'
5698            }
5699            _ => false,
5700        };
5701
5702        let file_io_class: &Py<PyType> = cfg_select! {
5703            all(feature = "host_env", windows) => {
5704                if is_console {
5705                    Some(super::winconsoleio::WindowsConsoleIO::static_type())
5706                } else {
5707                    Some(super::fileio::FileIO::static_type())
5708                }
5709            }
5710            feature = "host_env" => Some(super::fileio::FileIO::static_type()),
5711            _ => None,
5712        }
5713        .ok_or_else(|| {
5714            new_unsupported_operation(
5715                "Couldn't get FileIO, io.open likely isn't supported on your platform",
5716                vm,
5717            )
5718        })?;
5719        let raw = PyType::call(
5720            file_io_class,
5721            (file, mode.rawmode(), opts.closefd, opts.opener).into_args(vm),
5722            vm,
5723        )?;
5724
5725        let isatty = opts.buffering < 0 && {
5726            let atty = vm.call_method(&raw, "isatty", ())?;
5727            bool::try_from_object(vm, atty)?
5728        };
5729
5730        // Warn if line buffering is requested in binary mode
5731        if opts.buffering == 1 && matches!(mode.encode, EncodeMode::Bytes) {
5732            crate::stdlib::_warnings::warn(
5733                vm.ctx.exceptions.runtime_warning,
5734                "line buffering (buffering=1) isn't supported in binary mode, the default buffer size will be used".to_owned(),
5735                1,
5736                vm,
5737            )?;
5738        }
5739
5740        let line_buffering = opts.buffering == 1 || isatty;
5741
5742        let buffering = if opts.buffering < 0 || opts.buffering == 1 {
5743            DEFAULT_BUFFER_SIZE
5744        } else {
5745            opts.buffering as usize
5746        };
5747
5748        if buffering == 0 {
5749            let ret = match mode.encode {
5750                EncodeMode::Text => {
5751                    let _ = vm.call_method(&raw, "close", ());
5752                    Err(vm.new_value_error("can't have unbuffered text I/O"))
5753                }
5754                EncodeMode::Bytes => Ok(raw),
5755            };
5756            return ret;
5757        }
5758
5759        let cls = if mode.plus {
5760            BufferedRandom::static_type()
5761        } else if let FileMode::Read = mode.file {
5762            BufferedReader::static_type()
5763        } else {
5764            BufferedWriter::static_type()
5765        };
5766        let buffered = PyType::call(cls, (raw, buffering).into_args(vm), vm)?;
5767
5768        match mode.encode {
5769            EncodeMode::Text => {
5770                let encoding = if is_console && opts.encoding.is_none() {
5771                    // Console IO always uses utf-8
5772                    Some(PyUtf8Str::from("utf-8").into_ref(&vm.ctx))
5773                } else {
5774                    match opts.encoding {
5775                        Some(enc) => Some(enc),
5776                        None => {
5777                            let encoding =
5778                                text_encoding(vm.ctx.none(), StackLevelArg { stacklevel: 2 }, vm)?;
5779                            Some(PyUtf8StrRef::try_from_object(vm, encoding.into())?)
5780                        }
5781                    }
5782                };
5783                let tio = TextIOWrapper::static_type();
5784                let wrapper = PyType::call(
5785                    tio,
5786                    (
5787                        buffered.clone(),
5788                        encoding,
5789                        opts.errors,
5790                        opts.newline,
5791                        line_buffering,
5792                    )
5793                        .into_args(vm),
5794                    vm,
5795                )
5796                .inspect_err(|_err| {
5797                    let _ = vm.call_method(&buffered, "close", ());
5798                })?;
5799                wrapper.set_attr("mode", vm.new_pyobj(mode_string), vm)?;
5800                Ok(wrapper)
5801            }
5802            EncodeMode::Bytes => Ok(buffered),
5803        }
5804    }
5805
5806    fn create_unsupported_operation(ctx: &Context) -> PyTypeRef {
5807        use crate::builtins::type_::PyAttributes;
5808        use crate::types::PyTypeSlots;
5809
5810        let mut attrs = PyAttributes::default();
5811        attrs.insert(identifier!(ctx, __module__), ctx.new_str("io").into());
5812
5813        PyType::new_heap(
5814            "UnsupportedOperation",
5815            vec![
5816                ctx.exceptions.os_error.to_owned(),
5817                ctx.exceptions.value_error.to_owned(),
5818            ],
5819            attrs,
5820            PyTypeSlots::heap_default(),
5821            ctx.types.type_type.to_owned(),
5822            ctx,
5823        )
5824        .unwrap()
5825    }
5826
5827    pub(super) fn unsupported_operation() -> &'static Py<PyType> {
5828        rustpython_common::static_cell! {
5829            static CELL: PyTypeRef;
5830        }
5831        CELL.get_or_init(|| create_unsupported_operation(Context::genesis()))
5832    }
5833
5834    #[pyfunction]
5835    fn text_encoding(
5836        encoding: PyObjectRef,
5837        stacklevel: StackLevelArg,
5838        vm: &VirtualMachine,
5839    ) -> PyResult<PyStrRef> {
5840        if vm.is_none(&encoding) {
5841            let encoding = if vm.state.config.settings.utf8_mode > 0 {
5842                "utf-8"
5843            } else {
5844                "locale"
5845            };
5846            if vm.state.config.settings.warn_default_encoding {
5847                let mut stacklevel = stacklevel.stacklevel;
5848                if stacklevel > 1
5849                    && let Some(code) = crate::frame::current_code()
5850                    && let Some(stdlib_dir) = vm.state.config.paths.stdlib_dir.as_deref()
5851                {
5852                    let path = code.source_path().as_str();
5853                    if !path.starts_with(stdlib_dir) {
5854                        stacklevel = stacklevel.saturating_sub(1);
5855                    }
5856                }
5857                let stacklevel = usize::try_from(stacklevel).unwrap_or(0);
5858                crate::stdlib::_warnings::warn(
5859                    vm.ctx.exceptions.encoding_warning,
5860                    "'encoding' argument not specified".to_owned(),
5861                    stacklevel,
5862                    vm,
5863                )?;
5864            }
5865            return Ok(vm.ctx.new_str(encoding));
5866        }
5867        encoding.try_into_value(vm)
5868    }
5869
5870    #[cfg(test)]
5871    mod tests {
5872        use super::*;
5873
5874        #[test]
5875        fn buffered_read() {
5876            let data = vec![1, 2, 3, 4];
5877            let bytes = None;
5878            let mut buffered = BufferedIO {
5879                cursor: Cursor::new(data.clone()),
5880            };
5881
5882            assert_eq!(buffered.read(bytes).unwrap(), data);
5883        }
5884
5885        #[test]
5886        fn buffered_seek() {
5887            let data = vec![1, 2, 3, 4];
5888            let count: u64 = 2;
5889            let mut buffered = BufferedIO {
5890                cursor: Cursor::new(data),
5891            };
5892
5893            assert_eq!(buffered.seek(SeekFrom::Start(count)).unwrap(), count);
5894            assert_eq!(buffered.read(Some(count as usize)).unwrap(), vec![3, 4]);
5895        }
5896
5897        #[test]
5898        fn buffered_value() {
5899            let data = vec![1, 2, 3, 4];
5900            let buffered = BufferedIO {
5901                cursor: Cursor::new(data.clone()),
5902            };
5903
5904            assert_eq!(buffered.getvalue(), data);
5905        }
5906    }
5907
5908    #[cfg_attr(
5909        not(feature = "host_env"),
5910        expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")
5911    )]
5912    pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py<PyModule>) -> PyResult<()> {
5913        // Call auto-generated initialization first
5914        __module_exec(vm, module);
5915
5916        // Initialize FileIO types (requires host_env for filesystem access)
5917        #[cfg(feature = "host_env")]
5918        super::fileio::module_exec(vm, module)?;
5919
5920        // Initialize WindowsConsoleIO type (Windows only)
5921        #[cfg(all(feature = "host_env", windows))]
5922        super::winconsoleio::module_exec(vm, module)?;
5923
5924        let unsupported_operation = unsupported_operation().to_owned();
5925        extend_module!(vm, module, {
5926            "UnsupportedOperation" => unsupported_operation,
5927            "BlockingIOError" => vm.ctx.exceptions.blocking_io_error.to_owned(),
5928        });
5929        Ok(())
5930    }
5931}
5932// FileIO requires host environment for filesystem access
5933#[cfg(feature = "host_env")]
5934#[pymodule]
5935mod fileio {
5936    use super::{_io::*, Offset, iobase_finalize};
5937    use crate::host_env::crt_fd;
5938    use crate::{
5939        AsObject, Py, PyObject, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine,
5940        builtins::{PyBaseExceptionRef, PyUtf8Str, PyUtf8StrRef},
5941        common::wtf8::Wtf8Buf,
5942        convert::{IntoPyException, ToPyException},
5943        exceptions::OSErrorBuilder,
5944        function::{ArgBytesLike, ArgMemoryBuffer},
5945        ospath::{OsPath, OsPathOrFd},
5946        stdlib::os,
5947        types::{Constructor, DefaultConstructor, Destructor, Initializer, Representable},
5948    };
5949    use crossbeam_utils::atomic::AtomicCell;
5950    use rustpython_host_env::io as host_io;
5951
5952    #[pyattr]
5953    #[pyclass(module = "_io", name, base = _RawIOBase)]
5954    #[derive(Debug)]
5955    pub(super) struct FileIO {
5956        _base: _RawIOBase,
5957        fd: AtomicCell<i32>,
5958        closefd: AtomicCell<bool>,
5959        mode: AtomicCell<host_io::FileMode>,
5960        seekable: AtomicCell<Option<bool>>,
5961        blksize: AtomicCell<i64>,
5962        finalizing: AtomicCell<bool>,
5963    }
5964
5965    #[derive(FromArgs)]
5966    pub(super) struct FileIOArgs {
5967        #[pyarg(any)]
5968        file: PyObjectRef,
5969        // Omitted mode is stored as rb.
5970        #[pyarg(any, default, py_default = "'r'")]
5971        mode: Option<PyUtf8StrRef>,
5972        #[pyarg(any, default = true)]
5973        closefd: bool,
5974        #[pyarg(any, optional)]
5975        opener: Option<PyObjectRef>,
5976    }
5977
5978    impl Default for FileIO {
5979        fn default() -> Self {
5980            Self {
5981                _base: Default::default(),
5982                fd: AtomicCell::new(-1),
5983                closefd: AtomicCell::new(true),
5984                mode: AtomicCell::new(host_io::FileMode::empty()),
5985                seekable: AtomicCell::new(None),
5986                blksize: AtomicCell::new(super::DEFAULT_BUFFER_SIZE as _),
5987                finalizing: AtomicCell::new(false),
5988            }
5989        }
5990    }
5991
5992    impl DefaultConstructor for FileIO {}
5993
5994    impl Initializer for FileIO {
5995        type Args = FileIOArgs;
5996
5997        fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
5998            // TODO: let atomic_flag_works
5999            let name = args.file;
6000            // Check if bool is used as file descriptor
6001            if name.class().is(vm.ctx.types.bool_type) {
6002                crate::stdlib::_warnings::warn(
6003                    vm.ctx.exceptions.runtime_warning,
6004                    "bool is used as a file descriptor".to_owned(),
6005                    1,
6006                    vm,
6007                )?;
6008            }
6009            let arg_fd = if let Some(i) = name.downcast_ref::<crate::builtins::PyInt>() {
6010                let fd = i.try_to_primitive(vm)?;
6011                if fd < 0 {
6012                    return Err(vm.new_value_error("negative file descriptor"));
6013                }
6014                Some(fd)
6015            } else {
6016                None
6017            };
6018
6019            let mode_obj = args
6020                .mode
6021                .unwrap_or_else(|| PyUtf8Str::from("rb").into_ref(&vm.ctx));
6022            let mode_str = mode_obj.as_str();
6023            let parsed = host_io::parse_fileio_mode(mode_str)
6024                .map_err(|e| vm.new_value_error(e.error_msg(mode_str)))?;
6025            let mode = parsed.mode;
6026            let flags = parsed.flags;
6027            zelf.mode.store(mode);
6028
6029            let (fd, filename) = if let Some(fd) = arg_fd {
6030                zelf.closefd.store(args.closefd);
6031                (fd, None)
6032            } else {
6033                zelf.closefd.store(true);
6034                if !args.closefd {
6035                    return Err(vm.new_value_error("Cannot use closefd=False with file name"));
6036                }
6037
6038                if let Some(opener) = args.opener {
6039                    let fd = opener.call((name.clone(), flags), vm)?;
6040                    if !fd.fast_isinstance(vm.ctx.types.int_type) {
6041                        return Err(vm.new_type_error("expected integer from opener"));
6042                    }
6043                    let fd = i32::try_from_object(vm, fd)?;
6044                    if fd < 0 {
6045                        return Err(vm.new_value_error(format!("opener returned {fd}")));
6046                    }
6047                    (fd, None)
6048                } else {
6049                    let path = OsPath::try_from_fspath(name.clone(), vm)?;
6050                    #[cfg(any(unix, target_os = "wasi"))]
6051                    let fd = host_io::open_path(&path.clone().into_cstring(vm)?, flags, 0o666);
6052                    #[cfg(windows)]
6053                    let fd = host_io::open_path(&path.to_wide_cstring(vm)?, flags, 0o666);
6054                    let filename = OsPathOrFd::Path(path);
6055                    match fd {
6056                        Ok(fd) => (fd.into_raw(), Some(filename)),
6057                        Err(e) => {
6058                            return Err(OSErrorBuilder::with_filename_from_errno(&e, filename, vm));
6059                        }
6060                    }
6061                }
6062            };
6063            let fd_is_own = arg_fd.is_none();
6064            zelf.fd.store(fd);
6065            let fd = unsafe { crt_fd::Borrowed::borrow_raw(fd) };
6066            let filename = filename.unwrap_or(OsPathOrFd::Fd(fd));
6067
6068            // TODO: _Py_set_inheritable
6069
6070            match host_io::inspect_file_target(fd) {
6071                Ok(info) => {
6072                    if let Some(blksize) = info.blksize {
6073                        zelf.blksize.store(blksize);
6074                    }
6075                }
6076                Err(err) => {
6077                    if host_io::should_forget_fd_after_inspect_error(&err, fd_is_own) {
6078                        zelf.fd.store(-1);
6079                    }
6080                    return Err(OSErrorBuilder::with_filename(&err, filename, vm));
6081                }
6082            }
6083
6084            #[cfg(windows)]
6085            crate::stdlib::msvcrt::setmode_binary(fd);
6086            if let Err(e) = zelf.as_object().set_attr("name", name, vm) {
6087                // If fd was passed by user, don't close it on error
6088                if !fd_is_own {
6089                    zelf.fd.store(-1);
6090                }
6091                return Err(e);
6092            }
6093
6094            if mode.is_superset(&host_io::FileMode::APPENDING) {
6095                let _ = host_io::seek_to_end(fd);
6096            }
6097
6098            Ok(())
6099        }
6100    }
6101
6102    impl Representable for FileIO {
6103        #[inline]
6104        fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
6105            let type_name = zelf.class().slot_name();
6106            let fd = zelf.fd.load();
6107            if fd < 0 {
6108                return Ok(format!("<{type_name} [closed]>"));
6109            }
6110            let name_repr = repr_file_obj_name(zelf.as_object(), vm)?;
6111            let mode = zelf.mode();
6112            let closefd = if zelf.closefd.load() { "True" } else { "False" };
6113            let repr = if let Some(name_repr) = name_repr {
6114                format!("<{type_name} name={name_repr} mode='{mode}' closefd={closefd}>")
6115            } else {
6116                format!("<{type_name} fd={fd} mode='{mode}' closefd={closefd}>")
6117            };
6118            Ok(repr)
6119        }
6120    }
6121
6122    impl FileIO {
6123        fn io_error(
6124            zelf: &Py<Self>,
6125            error: std::io::Error,
6126            vm: &VirtualMachine,
6127        ) -> PyBaseExceptionRef {
6128            let exc = error.to_pyexception(vm);
6129            if let Ok(name) = zelf.as_object().get_attr("name", vm) {
6130                exc.as_object()
6131                    .set_attr("filename", name, vm)
6132                    .expect("OSError.filename set must success");
6133            }
6134            exc
6135        }
6136
6137        fn fileno(&self, vm: &VirtualMachine) -> PyResult<i32> {
6138            let fd = self.fd.load();
6139            if fd >= 0 {
6140                Ok(fd)
6141            } else {
6142                Err(io_closed_error(vm))
6143            }
6144        }
6145
6146        fn get_fd(&self, vm: &VirtualMachine) -> PyResult<crt_fd::Borrowed<'_>> {
6147            self.fileno(vm)
6148                .map(|fd| unsafe { crt_fd::Borrowed::borrow_raw(fd) })
6149        }
6150
6151        /// One `read()` into `buf`, retried on EINTR (PEP 475). `None` on EAGAIN.
6152        fn read_once_into(
6153            zelf: &Py<Self>,
6154            handle: crt_fd::Borrowed<'_>,
6155            buf: &mut [u8],
6156            vm: &VirtualMachine,
6157        ) -> PyResult<Option<usize>> {
6158            loop {
6159                match vm.allow_threads(|| host_io::read_once(handle, buf)) {
6160                    Ok(n) => return Ok(Some(n)),
6161                    Err(e) if host_io::is_interrupted_error(&e) => {
6162                        vm.check_signals()?;
6163                    }
6164                    // Non-blocking mode: return None if EAGAIN
6165                    Err(e) if host_io::is_would_block_error(&e) => return Ok(None),
6166                    Err(e) => return Err(Self::io_error(zelf, e, vm)),
6167                }
6168            }
6169        }
6170    }
6171
6172    #[pyclass(
6173        with(Constructor, Initializer, Representable, Destructor),
6174        flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
6175    )]
6176    impl Py<FileIO> {
6177        #[pygetset]
6178        fn closed(&self) -> bool {
6179            self.fd.load() < 0
6180        }
6181
6182        #[pygetset]
6183        fn closefd(&self) -> bool {
6184            self.closefd.load()
6185        }
6186
6187        #[pygetset(name = "_blksize")]
6188        fn blksize(&self) -> i64 {
6189            self.blksize.load()
6190        }
6191
6192        #[pymethod]
6193        fn fileno(&self, vm: &VirtualMachine) -> PyResult<i32> {
6194            self.payload.fileno(vm)
6195        }
6196
6197        #[pymethod]
6198        fn readable(&self, vm: &VirtualMachine) -> PyResult<bool> {
6199            if self.fd.load() < 0 {
6200                return Err(io_closed_error(vm));
6201            }
6202            Ok(self.mode.load().is_superset(&host_io::FileMode::READABLE))
6203        }
6204
6205        #[pymethod]
6206        fn writable(&self, vm: &VirtualMachine) -> PyResult<bool> {
6207            if self.fd.load() < 0 {
6208                return Err(io_closed_error(vm));
6209            }
6210            Ok(self.mode.load().is_superset(&host_io::FileMode::WRITABLE))
6211        }
6212
6213        #[pygetset]
6214        fn mode(&self) -> &'static str {
6215            self.mode.load().raw_mode()
6216        }
6217
6218        #[pymethod]
6219        fn read(
6220            zelf: &Self,
6221            read_byte: OptionalSize,
6222            vm: &VirtualMachine,
6223        ) -> PyResult<Option<Vec<u8>>> {
6224            if !zelf.mode.load().is_superset(&host_io::FileMode::READABLE) {
6225                return Err(new_unsupported_operation(
6226                    "File or stream is not readable",
6227                    vm,
6228                ));
6229            }
6230            let handle = zelf.get_fd(vm)?;
6231            let bytes = if let Some(read_byte) = read_byte.to_usize() {
6232                let mut bytes = vm.new_zeroed_bytes(read_byte)?;
6233                // Loop on EINTR (PEP 475)
6234                let n = loop {
6235                    match vm.allow_threads(|| host_io::read_once(handle, &mut bytes)) {
6236                        Ok(n) => break n,
6237                        Err(e) if host_io::is_interrupted_error(&e) => {
6238                            vm.check_signals()?;
6239                            continue;
6240                        }
6241                        // Non-blocking mode: return None if EAGAIN
6242                        Err(e) if host_io::is_would_block_error(&e) => {
6243                            return Ok(None);
6244                        }
6245                        Err(e) => return Err(FileIO::io_error(zelf, e, vm)),
6246                    }
6247                };
6248                bytes.truncate(n);
6249                bytes
6250            } else {
6251                let mut bytes = vec![];
6252                // Loop on EINTR (PEP 475)
6253                loop {
6254                    match vm.allow_threads(|| host_io::read_all(handle, &mut bytes)) {
6255                        Ok(()) => break,
6256                        Err(e) if host_io::is_interrupted_error(&e) => {
6257                            vm.check_signals()?;
6258                            continue;
6259                        }
6260                        // Non-blocking mode: return None if EAGAIN (only if no data read yet)
6261                        Err(e) if host_io::is_would_block_error(&e) => {
6262                            if bytes.is_empty() {
6263                                return Ok(None);
6264                            }
6265                            break;
6266                        }
6267                        Err(e) => return Err(FileIO::io_error(zelf, e, vm)),
6268                    }
6269                }
6270                bytes
6271            };
6272
6273            Ok(Some(bytes))
6274        }
6275
6276        #[pymethod]
6277        fn readinto(
6278            zelf: &Self,
6279            buffer: ArgMemoryBuffer,
6280            vm: &VirtualMachine,
6281        ) -> PyResult<Option<usize>> {
6282            if !zelf.mode.load().is_superset(&host_io::FileMode::READABLE) {
6283                return Err(new_unsupported_operation(
6284                    "File or stream is not readable",
6285                    vm,
6286                ));
6287            }
6288
6289            let handle = zelf.get_fd(vm)?;
6290
6291            if host_io::reads_without_waiting(handle) {
6292                // The read answers from the file itself, so it returns without
6293                // waiting on anyone; write where the caller asked directly.
6294                // Seekability is not the question -- a pipe on Windows seeks.
6295                let mut buf = buffer.borrow_buf_mut();
6296                return FileIO::read_once_into(zelf, handle, &mut buf, vm);
6297            }
6298
6299            // A pipe, socket or terminal answers only when the other end
6300            // writes, which may be never. Holding the export for the whole
6301            // call is what keeps the target from being resized meanwhile, as a
6302            // Py_buffer does; but reaching its bytes takes a lock that every
6303            // other thread touching the same object waits on, and a thread
6304            // waiting on a lock never reaches a safepoint, so holding that one
6305            // across the wait stops the world from being stopped at all. Read
6306            // aside and take the lock for the copy.
6307            let mut scratch = vm.new_zeroed_bytes(buffer.len())?;
6308            let ret = FileIO::read_once_into(zelf, handle, &mut scratch, vm)?;
6309            if let Some(n) = ret {
6310                buffer.borrow_buf_mut()[..n].copy_from_slice(&scratch[..n]);
6311            }
6312            Ok(ret)
6313        }
6314
6315        #[pymethod]
6316        fn write(zelf: &Self, b: ArgBytesLike, vm: &VirtualMachine) -> PyResult<Option<usize>> {
6317            if !zelf.mode.load().is_superset(&host_io::FileMode::WRITABLE) {
6318                return Err(new_unsupported_operation(
6319                    "File or stream is not writable",
6320                    vm,
6321                ));
6322            }
6323
6324            let handle = zelf.get_fd(vm)?;
6325
6326            // A pipe, socket or terminal takes the bytes only when the other
6327            // end makes room, which may be never; see readinto above for what
6328            // holding the source's lock across that wait costs.
6329            let buf = b.borrow_buf_unlocked(vm)?;
6330
6331            // Loop on EINTR (PEP 475)
6332            let len = loop {
6333                match vm.allow_threads(|| host_io::write_once(handle, &buf)) {
6334                    Ok(n) => break n,
6335                    Err(e) if host_io::is_interrupted_error(&e) => {
6336                        vm.check_signals()?;
6337                        continue;
6338                    }
6339                    // Non-blocking mode: return None if EAGAIN
6340                    Err(e) if host_io::is_would_block_error(&e) => return Ok(None),
6341                    Err(e) => return Err(FileIO::io_error(zelf, e, vm)),
6342                }
6343            };
6344
6345            //return number of bytes written
6346            Ok(Some(len))
6347        }
6348
6349        #[pymethod]
6350        fn close(zelf: &Self, vm: &VirtualMachine) -> PyResult<()> {
6351            let res = iobase_close(zelf.as_object(), vm);
6352            if !zelf.closefd.load() {
6353                zelf.fd.store(-1);
6354                return res;
6355            }
6356            let flush_exc = res.err();
6357            if zelf.finalizing.load() {
6358                FileIO::dealloc_warn(zelf, zelf.as_object(), vm);
6359            }
6360            let fd = zelf.fd.swap(-1);
6361            let close_err = if fd >= 0 {
6362                host_io::close_owned_fd(unsafe { crt_fd::Owned::from_raw(fd) })
6363                    .map_err(|err| FileIO::io_error(zelf, err, vm))
6364                    .err()
6365            } else {
6366                None
6367            };
6368            match (flush_exc, close_err) {
6369                (Some(fe), Some(ce)) => {
6370                    ce.set_context(Some(fe));
6371                    Err(ce)
6372                }
6373                (Some(e), None) | (None, Some(e)) => Err(e),
6374                (None, None) => Ok(()),
6375            }
6376        }
6377
6378        #[pymethod]
6379        fn seekable(&self, vm: &VirtualMachine) -> PyResult<bool> {
6380            let fd = self.get_fd(vm)?;
6381            Ok(self.seekable.load().unwrap_or_else(|| {
6382                let seekable = host_io::is_seekable(fd);
6383                self.seekable.store(Some(seekable));
6384                seekable
6385            }))
6386        }
6387
6388        #[pymethod]
6389        fn seek(&self, pos: PyObjectRef, whence: HowArg, vm: &VirtualMachine) -> PyResult<Offset> {
6390            let how = whence.whence;
6391            let fd = self.get_fd(vm)?;
6392            let offset = get_offset(&pos, vm)?;
6393
6394            host_io::seek(fd, offset, how).map_err(|e| e.into_pyexception(vm))
6395        }
6396
6397        #[pymethod]
6398        fn tell(&self, vm: &VirtualMachine) -> PyResult<Offset> {
6399            let fd = self.get_fd(vm)?;
6400            host_io::tell(fd).map_err(|e| e.into_pyexception(vm))
6401        }
6402
6403        #[pymethod]
6404        fn truncate(&self, len: ObjLen, vm: &VirtualMachine) -> PyResult<Offset> {
6405            let fd = self.get_fd(vm)?;
6406            let len = match len.size {
6407                Some(l) => get_offset(&l, vm)?,
6408                None => host_io::tell(fd).map_err(|e| e.into_pyexception(vm))?,
6409            };
6410            os::ftruncate(fd, len).map_err(|e| e.into_pyexception(vm))?;
6411            Ok(len)
6412        }
6413
6414        #[pymethod]
6415        fn isatty(&self, vm: &VirtualMachine) -> PyResult<bool> {
6416            let fd = self.fileno(vm)?;
6417            Ok(host_io::isatty(fd))
6418        }
6419
6420        #[pymethod]
6421        fn __getstate__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
6422            Err(vm.new_type_error(format!("cannot pickle '{}' instances", zelf.class().name())))
6423        }
6424
6425        // fileio_dealloc_warn in Modules/_io/fileio.c
6426        #[pymethod(name = "_dealloc_warn")]
6427        fn _dealloc_warn_method(zelf: &Self, object: PyObjectRef, vm: &VirtualMachine) {
6428            FileIO::dealloc_warn(zelf, &object, vm);
6429        }
6430    }
6431
6432    impl FileIO {
6433        /// Issue ResourceWarning if fd is still open and closefd is true.
6434        fn dealloc_warn(zelf: &Py<Self>, source: &PyObject, vm: &VirtualMachine) {
6435            if zelf.fd.load() >= 0 && zelf.closefd.load() {
6436                let repr = source
6437                    .repr(vm)
6438                    .map_or_else(|_| Wtf8Buf::from("<file>"), |s| s.as_wtf8().to_owned());
6439                if let Err(e) = crate::stdlib::_warnings::warn(
6440                    vm.ctx.exceptions.resource_warning,
6441                    format!("unclosed file {repr}"),
6442                    1,
6443                    vm,
6444                ) {
6445                    vm.run_unraisable(e, None, zelf.as_object().to_owned());
6446                }
6447            }
6448        }
6449    }
6450
6451    impl Destructor for FileIO {
6452        fn slot_del(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
6453            if let Some(fileio) = zelf.downcast_ref::<Self>() {
6454                fileio.finalizing.store(true);
6455            }
6456            iobase_finalize(zelf, vm);
6457            Ok(())
6458        }
6459
6460        #[cold]
6461        fn del(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<()> {
6462            unreachable!("slot_del is implemented")
6463        }
6464    }
6465}
6466
6467// WindowsConsoleIO requires host environment and Windows
6468#[cfg(all(feature = "host_env", windows))]
6469#[pymodule]
6470mod winconsoleio {
6471    use super::{_io::*, iobase_finalize};
6472    use crate::{
6473        AsObject, Py, PyObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine,
6474        builtins::{PyBaseExceptionRef, PyUtf8StrRef},
6475        common::{lock::PyMutex, wtf8::Wtf8Buf},
6476        convert::{IntoPyException, ToPyException},
6477        function::{ArgBytesLike, ArgMemoryBuffer},
6478        types::{Constructor, DefaultConstructor, Destructor, Initializer, Representable},
6479    };
6480    use crossbeam_utils::atomic::AtomicCell;
6481    use rustpython_host_env::io as host_io;
6482    use rustpython_host_env::nt as host_nt;
6483    use rustpython_host_env::windows::ToWideString;
6484    type HANDLE = host_nt::Handle;
6485
6486    const SMALLBUF: usize = 4;
6487    const BUFMAX: usize = 32 * 1024 * 1024;
6488
6489    fn handle_from_fd(fd: i32) -> HANDLE {
6490        host_nt::handle_from_fd(fd)
6491    }
6492
6493    fn is_invalid_handle(handle: HANDLE) -> bool {
6494        host_nt::is_invalid_handle(handle)
6495    }
6496
6497    /// Check if a Python object (fd or path string) refers to a console.
6498    /// Returns 'r' (input), 'w' (output), 'x' (generic CON), or '\0' (not a console).
6499    pub(super) fn pyio_get_console_type(path_or_fd: &PyObject, vm: &VirtualMachine) -> char {
6500        // Try as integer fd first
6501        if let Ok(fd) = i32::try_from_object(vm, path_or_fd.to_owned()) {
6502            return host_nt::console_type_from_fd(fd);
6503        }
6504
6505        // Try as string path
6506        let Ok(name) = path_or_fd.str(vm) else {
6507            return '\0';
6508        };
6509        let Some(name_str) = name.to_str() else {
6510            // Surrogate strings can't be console device names
6511            return '\0';
6512        };
6513        host_nt::console_type_from_name(name_str)
6514    }
6515
6516    #[pyattr]
6517    #[pyclass(module = "_io", name = "_WindowsConsoleIO", base = _RawIOBase)]
6518    #[derive(Debug)]
6519    pub(super) struct WindowsConsoleIO {
6520        _base: _RawIOBase,
6521        fd: AtomicCell<i32>,
6522        readable: AtomicCell<bool>,
6523        writable: AtomicCell<bool>,
6524        closefd: AtomicCell<bool>,
6525        finalizing: AtomicCell<bool>,
6526        blksize: AtomicCell<i64>,
6527        buf: PyMutex<[u8; SMALLBUF]>,
6528    }
6529
6530    impl Default for WindowsConsoleIO {
6531        fn default() -> Self {
6532            Self {
6533                _base: Default::default(),
6534                fd: AtomicCell::new(-1),
6535                readable: AtomicCell::new(false),
6536                writable: AtomicCell::new(false),
6537                closefd: AtomicCell::new(false),
6538                finalizing: AtomicCell::new(false),
6539                blksize: AtomicCell::new(super::DEFAULT_BUFFER_SIZE as _),
6540                buf: PyMutex::new([0u8; SMALLBUF]),
6541            }
6542        }
6543    }
6544
6545    impl DefaultConstructor for WindowsConsoleIO {}
6546
6547    #[derive(FromArgs)]
6548    pub(super) struct WindowsConsoleIOArgs {
6549        #[pyarg(positional)]
6550        name: PyObjectRef,
6551        #[pyarg(any, default)]
6552        mode: Option<PyUtf8StrRef>,
6553        #[pyarg(any, default = true)]
6554        closefd: bool,
6555        #[allow(dead_code)]
6556        #[pyarg(any, default)]
6557        opener: Option<PyObjectRef>,
6558    }
6559
6560    impl Initializer for WindowsConsoleIO {
6561        type Args = WindowsConsoleIOArgs;
6562
6563        fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
6564            let nameobj = args.name;
6565
6566            if zelf.fd.load() >= 0 {
6567                if zelf.closefd.load() {
6568                    internal_close(zelf);
6569                } else {
6570                    zelf.fd.store(-1);
6571                }
6572            }
6573
6574            // Warn if bool is used as file descriptor
6575            if nameobj.class().is(vm.ctx.types.bool_type) {
6576                crate::stdlib::_warnings::warn(
6577                    vm.ctx.exceptions.runtime_warning,
6578                    "bool is used as a file descriptor".to_owned(),
6579                    1,
6580                    vm,
6581                )?;
6582            }
6583
6584            // Try to get fd from integer
6585            let mut fd: i32 = -1;
6586            if let Some(i) = nameobj.downcast_ref::<crate::builtins::PyInt>() {
6587                fd = i.try_to_primitive::<i32>(vm).unwrap_or(-1);
6588                if fd < 0 {
6589                    return Err(vm.new_value_error("negative file descriptor"));
6590                }
6591            }
6592
6593            // Parse mode
6594            let mode_str: &str = args.mode.as_deref().map_or("r", |s| s.as_str());
6595
6596            let mut rwa = false;
6597            let mut readable = false;
6598            let mut writable = false;
6599            let mut console_type = '\0';
6600            for c in mode_str.bytes() {
6601                match c {
6602                    b'+' | b'a' | b'b' | b'x' => {}
6603                    b'r' => {
6604                        if rwa {
6605                            return Err(
6606                                vm.new_value_error("Must have exactly one of read or write mode")
6607                            );
6608                        }
6609                        rwa = true;
6610                        readable = true;
6611                    }
6612                    b'w' => {
6613                        if rwa {
6614                            return Err(
6615                                vm.new_value_error("Must have exactly one of read or write mode")
6616                            );
6617                        }
6618                        rwa = true;
6619                        writable = true;
6620                    }
6621                    _ => {
6622                        return Err(vm.new_value_error(format!("invalid mode: {mode_str}")));
6623                    }
6624                }
6625            }
6626            if !rwa {
6627                return Err(vm.new_value_error("Must have exactly one of read or write mode"));
6628            }
6629
6630            zelf.readable.store(readable);
6631            zelf.writable.store(writable);
6632
6633            if fd < 0 {
6634                // Get console type from name
6635                console_type = pyio_get_console_type(&nameobj, vm);
6636                if console_type == 'x' {
6637                    if writable {
6638                        console_type = 'w';
6639                    } else {
6640                        console_type = 'r';
6641                    }
6642                }
6643
6644                // Opening by name
6645                zelf.closefd.store(true);
6646                if !args.closefd {
6647                    return Err(vm.new_value_error("Cannot use closefd=False with file name"));
6648                }
6649
6650                let name_str = nameobj.str(vm)?;
6651                let wide = name_str
6652                    .as_wtf8()
6653                    .to_wide_cstring()
6654                    .map_err(|e| e.to_pyexception(vm))?;
6655
6656                fd = host_nt::open_console_path_fd(&wide, writable)
6657                    .map_err(|err| err.to_pyexception(vm))?;
6658            } else {
6659                // When opened by fd, never close the fd (user owns it)
6660                zelf.closefd.store(false);
6661            }
6662
6663            zelf.fd.store(fd);
6664
6665            // Validate console type
6666            if console_type == '\0' {
6667                let handle = handle_from_fd(fd);
6668                console_type = host_nt::console_type(handle);
6669            }
6670
6671            if console_type == '\0' {
6672                // Not a console at all
6673                internal_close(zelf);
6674                return Err(vm.new_value_error("Cannot open non-console file"));
6675            }
6676
6677            if writable && console_type != 'w' {
6678                internal_close(zelf);
6679                return Err(vm.new_value_error("Cannot open console input buffer for writing"));
6680            }
6681            if readable && console_type != 'r' {
6682                internal_close(zelf);
6683                return Err(vm.new_value_error("Cannot open console output buffer for reading"));
6684            }
6685
6686            zelf.blksize.store(super::DEFAULT_BUFFER_SIZE as _);
6687            *zelf.buf.lock() = [0u8; SMALLBUF];
6688
6689            zelf.as_object().set_attr("name", nameobj, vm)?;
6690
6691            Ok(())
6692        }
6693    }
6694
6695    fn internal_close(zelf: &WindowsConsoleIO) {
6696        let fd = zelf.fd.swap(-1);
6697        if fd >= 0 && zelf.closefd.load() {
6698            let _ =
6699                host_io::close_owned_fd(unsafe { crate::host_env::crt_fd::Owned::from_raw(fd) });
6700        }
6701    }
6702
6703    impl Representable for WindowsConsoleIO {
6704        #[inline]
6705        fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
6706            let type_name = zelf.class().slot_name();
6707            let fd = zelf.fd.load();
6708            if fd < 0 {
6709                return Ok(format!("<{type_name} [closed]>"));
6710            }
6711            let mode = if zelf.readable.load() { "rb" } else { "wb" };
6712            let closefd = if zelf.closefd.load() { "True" } else { "False" };
6713            Ok(format!("<{type_name} mode='{mode}' closefd={closefd}>"))
6714        }
6715    }
6716
6717    impl WindowsConsoleIO {
6718        #[allow(dead_code)]
6719        fn io_error(
6720            zelf: &Py<Self>,
6721            error: std::io::Error,
6722            vm: &VirtualMachine,
6723        ) -> PyBaseExceptionRef {
6724            let exc = error.to_pyexception(vm);
6725            if let Ok(name) = zelf.as_object().get_attr("name", vm) {
6726                exc.as_object()
6727                    .set_attr("filename", name, vm)
6728                    .expect("OSError.filename set must succeed");
6729            }
6730            exc
6731        }
6732
6733        fn fileno(&self, vm: &VirtualMachine) -> PyResult<i32> {
6734            let fd = self.fd.load();
6735            if fd >= 0 {
6736                Ok(fd)
6737            } else {
6738                Err(io_closed_error(vm))
6739            }
6740        }
6741
6742        fn get_fd(&self, vm: &VirtualMachine) -> PyResult<i32> {
6743            self.fileno(vm)
6744        }
6745
6746        fn dealloc_warn(zelf: &Py<Self>, source: PyObjectRef, vm: &VirtualMachine) {
6747            if zelf.fd.load() >= 0 && zelf.closefd.load() {
6748                let repr = source
6749                    .repr(vm)
6750                    .map_or_else(|_| Wtf8Buf::from("<file>"), |s| s.as_wtf8().to_owned());
6751
6752                if let Err(e) = crate::stdlib::_warnings::warn(
6753                    vm.ctx.exceptions.resource_warning,
6754                    format!("unclosed file {repr}"),
6755                    1,
6756                    vm,
6757                ) {
6758                    vm.run_unraisable(e, None, zelf.as_object().to_owned());
6759                }
6760            }
6761        }
6762
6763        fn copy_from_buf(buf: &mut [u8; SMALLBUF], dest: &mut [u8]) -> usize {
6764            let mut n = 0;
6765            while buf[0] != 0 && n < dest.len() {
6766                dest[n] = buf[0];
6767                n += 1;
6768                for i in 1..SMALLBUF {
6769                    buf[i - 1] = buf[i];
6770                }
6771                buf[SMALLBUF - 1] = 0;
6772            }
6773            n
6774        }
6775    }
6776
6777    #[pyclass(
6778        with(Constructor, Initializer, Representable, Destructor),
6779        flags(BASETYPE, HAS_DICT, HAS_WEAKREF)
6780    )]
6781    impl Py<WindowsConsoleIO> {
6782        #[pygetset]
6783        fn closed(&self) -> bool {
6784            self.fd.load() < 0
6785        }
6786
6787        #[pygetset]
6788        fn closefd(&self) -> bool {
6789            self.closefd.load()
6790        }
6791
6792        #[pygetset(name = "_blksize")]
6793        fn blksize(&self) -> i64 {
6794            self.blksize.load()
6795        }
6796
6797        #[pygetset]
6798        fn mode(&self) -> &'static str {
6799            if self.readable.load() { "rb" } else { "wb" }
6800        }
6801
6802        #[pymethod]
6803        fn fileno(&self, vm: &VirtualMachine) -> PyResult<i32> {
6804            self.payload.fileno(vm)
6805        }
6806
6807        #[pymethod]
6808        fn readable(&self, vm: &VirtualMachine) -> PyResult<bool> {
6809            if self.fd.load() < 0 {
6810                return Err(io_closed_error(vm));
6811            }
6812            Ok(self.readable.load())
6813        }
6814
6815        #[pymethod]
6816        fn writable(&self, vm: &VirtualMachine) -> PyResult<bool> {
6817            if self.fd.load() < 0 {
6818                return Err(io_closed_error(vm));
6819            }
6820            Ok(self.writable.load())
6821        }
6822
6823        #[pymethod]
6824        fn isatty(&self, vm: &VirtualMachine) -> PyResult<bool> {
6825            if self.fd.load() < 0 {
6826                return Err(io_closed_error(vm));
6827            }
6828            Ok(true)
6829        }
6830
6831        #[pymethod]
6832        fn close(zelf: &Self, vm: &VirtualMachine) -> PyResult<()> {
6833            let res = iobase_close(zelf.as_object(), vm);
6834            if !zelf.closefd.load() {
6835                zelf.fd.store(-1);
6836                return res;
6837            }
6838            let flush_exc = res.err();
6839            if zelf.finalizing.load() {
6840                WindowsConsoleIO::dealloc_warn(zelf, zelf.as_object().to_owned(), vm);
6841            }
6842            let fd = zelf.fd.swap(-1);
6843            let close_err: Option<PyBaseExceptionRef> = if fd >= 0 {
6844                host_io::close_owned_fd(unsafe { crate::host_env::crt_fd::Owned::from_raw(fd) })
6845                    .err()
6846                    .map(|e| e.into_pyexception(vm))
6847            } else {
6848                None
6849            };
6850            match (flush_exc, close_err) {
6851                (Some(fe), Some(ce)) => {
6852                    ce.set_context(Some(fe));
6853                    Err(ce)
6854                }
6855                (Some(e), None) | (None, Some(e)) => Err(e),
6856                (None, None) => Ok(()),
6857            }
6858        }
6859
6860        #[pymethod]
6861        fn readinto(&self, buffer: ArgMemoryBuffer, vm: &VirtualMachine) -> PyResult<usize> {
6862            let fd = self.get_fd(vm)?;
6863            if !self.readable.load() {
6864                return Err(new_unsupported_operation(
6865                    "Console buffer does not support reading",
6866                    vm,
6867                ));
6868            }
6869            let mut buf_ref = buffer.borrow_buf_mut();
6870            let len = buf_ref.len();
6871            if len == 0 {
6872                return Ok(0);
6873            }
6874            if len > BUFMAX {
6875                return Err(vm.new_value_error(format!("cannot read more than {BUFMAX} bytes")));
6876            }
6877
6878            let handle = handle_from_fd(fd);
6879            if is_invalid_handle(handle) {
6880                return Err(std::io::Error::last_os_error().to_pyexception(vm));
6881            }
6882
6883            let dest = &mut *buf_ref;
6884            let mut smallbuf = self.buf.lock();
6885            host_nt::read_console_into(handle, dest, &mut smallbuf)
6886                .map_err(|err| err.to_pyexception(vm))
6887        }
6888
6889        #[pymethod]
6890        fn readall(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
6891            if self.fd.load() < 0 {
6892                return Err(io_closed_error(vm));
6893            }
6894
6895            let handle = handle_from_fd(self.fd.load());
6896            if is_invalid_handle(handle) {
6897                return Err(std::io::Error::last_os_error().to_pyexception(vm));
6898            }
6899
6900            let mut smallbuf = self.buf.lock();
6901            let result = host_nt::read_console_all(handle, &mut smallbuf)
6902                .map_err(|err| err.into_pyexception(vm))?;
6903            Ok(vm.ctx.new_bytes(result).into())
6904        }
6905
6906        #[pymethod]
6907        fn read(&self, size: SignedSize, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
6908            if self.fd.load() < 0 {
6909                return Err(io_closed_error(vm));
6910            }
6911            if !self.readable.load() {
6912                return Err(new_unsupported_operation(
6913                    "Console buffer does not support reading",
6914                    vm,
6915                ));
6916            }
6917            let size = size.size;
6918            if size < 0 {
6919                return self.readall(vm);
6920            }
6921            if size as usize > BUFMAX {
6922                return Err(vm.new_value_error(format!("cannot read more than {BUFMAX} bytes")));
6923            }
6924            let mut buf = vec![0u8; size as usize];
6925            let handle = handle_from_fd(self.fd.load());
6926            if is_invalid_handle(handle) {
6927                return Err(std::io::Error::last_os_error().to_pyexception(vm));
6928            }
6929
6930            let mut read_len = {
6931                let mut ibuf = self.buf.lock();
6932                WindowsConsoleIO::copy_from_buf(&mut ibuf, &mut buf)
6933            };
6934            if read_len >= size as usize {
6935                buf.truncate(read_len);
6936                return Ok(vm.ctx.new_bytes(buf).into());
6937            }
6938            {
6939                let mut ibuf = self.buf.lock();
6940                let n = host_nt::read_console_into(handle, &mut buf[read_len..], &mut ibuf)
6941                    .map_err(|err| err.to_pyexception(vm))?;
6942                read_len += n;
6943            }
6944
6945            buf.truncate(read_len);
6946            Ok(vm.ctx.new_bytes(buf).into())
6947        }
6948
6949        #[pymethod]
6950        fn write(&self, b: ArgBytesLike, vm: &VirtualMachine) -> PyResult<usize> {
6951            if self.fd.load() < 0 {
6952                return Err(io_closed_error(vm));
6953            }
6954            if !self.writable.load() {
6955                return Err(new_unsupported_operation(
6956                    "Console buffer does not support writing",
6957                    vm,
6958                ));
6959            }
6960
6961            let handle = handle_from_fd(self.fd.load());
6962            if is_invalid_handle(handle) {
6963                return Err(std::io::Error::last_os_error().to_pyexception(vm));
6964            }
6965
6966            let data = b.borrow_buf();
6967            let data = &*data;
6968            if data.is_empty() {
6969                return Ok(0);
6970            }
6971
6972            host_nt::write_console_utf8(handle, data, BUFMAX)
6973                .map_err(|err| err.into_pyexception(vm))
6974        }
6975
6976        #[pymethod(name = "__reduce__")]
6977        fn reduce(_zelf: &Self, vm: &VirtualMachine) -> PyResult {
6978            Err(vm.new_type_error("cannot pickle '_WindowsConsoleIO' instances"))
6979        }
6980    }
6981
6982    impl Destructor for WindowsConsoleIO {
6983        fn slot_del(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
6984            if let Some(cio) = zelf.downcast_ref::<Self>() {
6985                cio.finalizing.store(true);
6986            }
6987            iobase_finalize(zelf, vm);
6988            Ok(())
6989        }
6990
6991        #[cold]
6992        fn del(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<()> {
6993            unreachable!("slot_del is implemented")
6994        }
6995    }
6996}