monty_types/file_mode.rs
1//! [`FileMode`] — the parsed, validated form of a Python `open()` mode
2//! string, carried by [`MontyFileHandle`](crate::object::MontyFileHandle).
3
4use std::{borrow::Cow, str::FromStr};
5/// A parsed Python `open()` mode.
6///
7/// This single enum captures everything that matters about how a file was
8/// opened: the access pattern (`r`/`w`/`a` and the `+` update flag) and
9/// whether the file is binary. The variant name encodes the access pattern;
10/// the `bool` payload is `true` for binary and `false` for text — i.e.
11/// `Read(true)` is `'rb'` and `Read(false)` is `'r'`.
12///
13/// Construct one with the [`FromStr`] impl (`mode_str.parse::<FileMode>()`).
14/// The original input string is
15/// intentionally not preserved; [`FileMode::as_str`] rebuilds the canonical
16/// CPython form (`'r'`, `'rb+'`, `'wb'`, …), matching how CPython itself
17/// normalizes input like `'rt'` → `'r'` and `'r+b'` → `'rb+'`.
18///
19/// `+` update modes (`ReadUpdate`/`WriteUpdate`/`AppendUpdate`) are reserved
20/// in the enum so the mode space is fully represented, but [`FromStr`]
21/// currently rejects them — properly modelling them needs read-position
22/// tracking that the file wrapper does not yet implement. Treat the `Update`
23/// variants as unreachable at runtime; do not pattern-match against them as
24/// if they were a valid result of parsing user input.
25///
26/// Carried publicly by [`MontyObject::FileHandle`](crate::object::MontyObject) so a host servicing file
27/// operations can inspect the mode without re-parsing the raw string.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
29pub enum FileMode {
30 /// `r` / `rb`: read-only; the file must already exist.
31 Read(bool),
32 /// `r+` / `rb+`: read and write an existing file. Reserved; not yet
33 /// produced by [`FromStr`].
34 ReadUpdate(bool),
35 /// `w` / `wb`: write-only; truncate the file (creating it if missing) on open.
36 Write(bool),
37 /// `w+` / `wb+`: read and write; truncate the file (creating it if missing).
38 /// Reserved; not yet produced by [`FromStr`].
39 WriteUpdate(bool),
40 /// `a` / `ab`: write-only appending; create the file if missing, preserving content.
41 Append(bool),
42 /// `a+` / `ab+`: read and append; create the file if missing, preserving content.
43 /// Reserved; not yet produced by [`FromStr`].
44 AppendUpdate(bool),
45}
46impl FileMode {
47 /// Returns the canonical Python `open()` mode string for this mode,
48 /// matching what CPython exposes via `file.mode`.
49 ///
50 /// The result is always one of the 12 well-formed mode strings (`r`, `rb`,
51 /// `r+`, `rb+`, `w`, `wb`, `w+`, `wb+`, `a`, `ab`, `a+`, `ab+`). This is
52 /// the canonical form CPython itself normalizes user input into — e.g.
53 /// `'rt'` → `'r'`, `'r+b'` → `'rb+'`, `'br'` → `'rb'`.
54 #[must_use]
55 pub fn as_str(&self) -> &'static str {
56 match self {
57 Self::Read(false) => "r",
58 Self::Read(true) => "rb",
59 Self::ReadUpdate(false) => "r+",
60 Self::ReadUpdate(true) => "rb+",
61 Self::Write(false) => "w",
62 Self::Write(true) => "wb",
63 Self::WriteUpdate(false) => "w+",
64 Self::WriteUpdate(true) => "wb+",
65 Self::Append(false) => "a",
66 Self::Append(true) => "ab",
67 Self::AppendUpdate(false) => "a+",
68 Self::AppendUpdate(true) => "ab+",
69 }
70 }
71
72 /// Whether the file is binary (`'rb'`, `'wb'`, …) rather than text.
73 #[must_use]
74 pub fn is_binary(&self) -> bool {
75 let (Self::Read(b)
76 | Self::ReadUpdate(b)
77 | Self::Write(b)
78 | Self::WriteUpdate(b)
79 | Self::Append(b)
80 | Self::AppendUpdate(b)) = self;
81 *b
82 }
83
84 /// Whether `read()` is allowed by this mode.
85 #[must_use]
86 pub fn readable(&self) -> bool {
87 matches!(
88 self,
89 Self::Read(_) | Self::ReadUpdate(_) | Self::WriteUpdate(_) | Self::AppendUpdate(_)
90 )
91 }
92
93 /// Whether `write()` is allowed by this mode.
94 #[must_use]
95 pub fn writable(&self) -> bool {
96 matches!(
97 self,
98 Self::Write(_) | Self::WriteUpdate(_) | Self::Append(_) | Self::AppendUpdate(_) | Self::ReadUpdate(_)
99 )
100 }
101
102 /// Whether writes should always append (`a`/`a+`).
103 #[must_use]
104 pub fn is_append(&self) -> bool {
105 matches!(self, Self::Append(_) | Self::AppendUpdate(_))
106 }
107
108 /// Whether `open()` must truncate the file to empty immediately (`w`/`w+`).
109 #[must_use]
110 pub fn truncate(&self) -> bool {
111 matches!(self, Self::Write(_) | Self::WriteUpdate(_))
112 }
113
114 /// Whether `open()` must create the file immediately if missing.
115 ///
116 /// True for the `w`/`w+` and `a`/`a+` families. For append modes this must
117 /// not disturb existing content.
118 #[must_use]
119 pub fn create(&self) -> bool {
120 matches!(
121 self,
122 Self::Write(_) | Self::WriteUpdate(_) | Self::Append(_) | Self::AppendUpdate(_)
123 )
124 }
125 /// Returns the bare Python type name (`type(f).__name__`) for this mode.
126 #[must_use]
127 pub fn type_name(&self) -> &'static str {
128 match self {
129 _ if !self.is_binary() => "TextIOWrapper",
130 Self::ReadUpdate(_) | Self::WriteUpdate(_) | Self::AppendUpdate(_) => "BufferedRandom",
131 Self::Read(_) => "BufferedReader",
132 Self::Write(_) | Self::Append(_) => "BufferedWriter",
133 }
134 }
135
136 /// Returns the fully-qualified `_io` wrapper type name a file opened with
137 /// this mode presents as, matching CPython's `repr(f)` (e.g.
138 /// `"_io.TextIOWrapper"`). The module-less form is [`type_name`](Self::type_name).
139 #[must_use]
140 pub fn file_type_name(&self) -> &'static str {
141 match self {
142 _ if !self.is_binary() => "_io.TextIOWrapper",
143 Self::ReadUpdate(_) | Self::WriteUpdate(_) | Self::AppendUpdate(_) => "_io.BufferedRandom",
144 Self::Read(_) => "_io.BufferedReader",
145 Self::Write(_) | Self::Append(_) => "_io.BufferedWriter",
146 }
147 }
148}
149/// Parses a Python `open()` mode string into a [`FileMode`].
150///
151/// Monty supports the common read, write, append, and update combinations in
152/// text or binary form. Exclusive creation (`x`) is rejected for now because
153/// it needs a dedicated mount-table operation to be race-free.
154///
155/// The `Err` payload is a CPython-matched message — an unknown mode
156/// character, duplicated `b`/`t`/`+`, conflicting binary+text flags, more
157/// than one of the `r`/`w`/`a` actions, or none at all (`''`, `'b'`, `'t'`).
158impl FromStr for FileMode {
159 type Err = Cow<'static, str>;
160
161 fn from_str(mode: &str) -> Result<Self, Self::Err> {
162 let mut action = None;
163 let mut binary = false;
164 let mut text = false;
165
166 for ch in mode.chars() {
167 match ch {
168 'r' | 'w' | 'a' => {
169 if action.replace(ch).is_some() {
170 // CPython's duplicate-action message differs from the missing-action
171 // one below (lowercase, no `... and at most one plus` suffix).
172 return Err("must have exactly one of create/read/write/append mode".into());
173 }
174 }
175 'x' => return Err("exclusive creation mode is not supported".into()),
176 'b' => {
177 if binary {
178 return Err("invalid mode: binary mode specified twice".into());
179 }
180 binary = true;
181 }
182 't' => {
183 if text {
184 return Err("invalid mode: text mode specified twice".into());
185 }
186 text = true;
187 }
188 // `+` modes (`r+`, `w+`, `a+`, and their `b` variants) need
189 // read-position tracking that Monty does not yet implement.
190 // Reject them outright rather than silently truncating on the
191 // first write (which would happen because the OS-level read
192 // and write ops are full-file one-shots).
193 '+' => return Err("update modes ('+') are not yet supported".into()),
194 _ => return Err(format!("invalid mode: {ch:?}").into()),
195 }
196 }
197
198 if binary && text {
199 return Err("can't have text and binary mode at once".into());
200 }
201 let Some(action) = action else {
202 return Err("Must have exactly one of create/read/write/append mode and at most one plus".into());
203 };
204
205 Ok(match action {
206 'w' => Self::Write(binary),
207 'a' => Self::Append(binary),
208 _ => Self::Read(binary),
209 })
210 }
211}