godot_core/tools/gfile.rs
1/*
2 * Copyright (c) godot-rust; Bromeon and contributors.
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6 */
7
8use std::cmp;
9use std::io::{BufRead, ErrorKind, Read, Seek, SeekFrom, Write};
10
11use crate::builtin::{GString, PackedByteArray, PackedStringArray, Variant, real};
12use crate::classes::FileAccess;
13use crate::classes::file_access::{CompressionMode, ModeFlags};
14use crate::global::Error;
15use crate::meta::error::IoError;
16use crate::meta::{AsArg, arg_into_ref};
17use crate::obj::Gd;
18
19/// Open a file for reading or writing.
20///
21/// This is a convenient wrapper around a [`FileAccess`] pointer with a unique reference, providing both safety and
22/// quality-of-life upgrades over the inner type:
23///
24/// - Exposes reading and writing capabilities of `FileAccess` in a safe way, returning [`Result<T>`](std::io::Result)
25/// instead of the `T` itself.
26/// - Makes the `FileAccess` handle exclusive to its instance, disallowing parallel reads and writes, which could introduce
27/// hard-to-track bugs due to unpredictable cursor movement. Exclusivity also ensures that when the `GFile` moves out
28/// of scope, the inner `FileAccess` does as well, automatically closing the file. Alternatively, you can [`drop()`]
29/// the `GFile` to close the file manually.
30/// - Implements useful Rust traits, namely: [`Read`], [`BufRead`], [`Write`], [`Seek`], allowing better file processing
31/// and integrating it with various tools in the Rust ecosystem (e.g. serialization).
32///
33/// Files by default are always opened with little-endian, as most files are saved as such. To switch to big-endian, use
34/// [`GFile::set_big_endian()`].
35///
36/// ## [`ModeFlags`]
37///
38/// Every constructor opening the access to a file (`open_*` associated functions) accepts the `flags` parameter,
39/// opening the file for different types of operations. Regardless of the provided `flags` value, the cursor is always
40/// positioned at the beginning of the file upon opening. To adjust its position, use [`Seek`]-provided methods.
41///
42/// - `ModeFlags::READ` opens the file for read operations.
43/// - `ModeFlags::WRITE` opens the file for write operations. If the file doesn't exist at the provided `path`, it is
44/// created. If it exists, it is truncated after the file is closed.
45/// - `ModeFlags::READ_WRITE` opens the file for read and write operations. The file is not truncated after closing.
46/// - `ModeFlags::WRITE_READ` opens the file for read and write operations. If the file doesn't exist at the provided
47/// `path`, it is created. If it exists, it is truncated.
48///
49/// ## Examples
50///
51/// ```no_run
52/// use godot::builtin::GString;
53/// use godot::classes::file_access::ModeFlags;
54/// use godot::tools::GFile;
55///
56/// fn save_game() -> Result<(), std::io::Error> {
57///
58/// // Open file in write mode
59/// let mut my_file = GFile::open("user://save_game.sav", ModeFlags::WRITE)?;
60///
61/// // Write some lines into it
62/// my_file.write_gstring_line("This is my saved game")?;
63/// my_file.write_gstring_line("I played for 5 minutes")?;
64///
65/// Ok(())
66/// // When GFile gets out of scope, the file is closed.
67/// }
68///
69/// fn load_game() -> Result<(), std::io::Error> {
70///
71/// // Open file in read mode
72/// let mut my_file = GFile::open("user://save_game.sav", ModeFlags::READ)?;
73///
74/// // Read lines
75/// let first_line = my_file.read_gstring_line()?;
76/// assert_eq!(first_line, GString::from("This is my saved game"));
77///
78/// let second_line = my_file.read_gstring_line()?;
79/// assert_eq!(second_line, GString::from("I played for 5 minutes"));
80///
81/// Ok(())
82/// }
83/// ```
84///
85/// ## See also
86///
87/// - [`FileAccess`] class in Rust.
88/// - [Godot documentation](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html) for `FileAccess`.
89pub struct GFile {
90 fa: Gd<FileAccess>,
91 buffer: Vec<u8>,
92 last_buffer_size: usize,
93 write_buffer: PackedByteArray,
94 file_length: Option<u64>,
95}
96
97impl GFile {
98 // For now - only used internally in BufRead implementation. If needed, its setting could be exposed in some way.
99 const BUFFER_SIZE: usize = 4096;
100
101 /// Open a file.
102 ///
103 /// Opens a file located at `path`, creating new [`GFile`] object. For [`ModeFlags`] description check the [`GFile`]
104 /// documentation.
105 pub fn open(path: impl AsArg<GString>, flags: ModeFlags) -> std::io::Result<Self> {
106 arg_into_ref!(path);
107
108 let fa = FileAccess::open(path, flags).ok_or_else(|| {
109 std::io::Error::other(format!(
110 "can't open file {path} in mode {flags:?}; GodotError: {:?}",
111 FileAccess::get_open_error()
112 ))
113 })?;
114
115 Ok(Self::from_inner(fa))
116 }
117
118 /// Open a compressed file.
119 ///
120 /// Opens a compressed file located at `path`, creating new [`GFile`] object. Can read only files compressed by
121 /// Godot compression formats. For [`ModeFlags`] description check the [`GFile`] documentation.
122 pub fn open_compressed(
123 path: impl AsArg<GString>,
124 flags: ModeFlags,
125 compression_mode: CompressionMode,
126 ) -> std::io::Result<Self> {
127 arg_into_ref!(path);
128
129 let fa = FileAccess::open_compressed_ex(path, flags)
130 .compression_mode(compression_mode)
131 .done()
132 .ok_or_else(|| {
133 std::io::Error::other(format!(
134 "can't open file {path} in mode {flags:?}; GodotError: {:?}",
135 FileAccess::get_open_error()
136 ))
137 })?;
138
139 Ok(Self::from_inner(fa))
140 }
141
142 /// Open a file encrypted by byte key.
143 ///
144 /// Opens a file encrypted by 32-byte long [`PackedByteArray`] located at `path`, creating new [`GFile`] object.
145 /// For [`ModeFlags`] description check the [`GFile`] documentation.
146 pub fn open_encrypted(
147 path: impl AsArg<GString>,
148 flags: ModeFlags,
149 key: &PackedByteArray,
150 ) -> std::io::Result<Self> {
151 arg_into_ref!(path);
152
153 let fa = FileAccess::open_encrypted(path, flags, key).ok_or_else(|| {
154 std::io::Error::other(format!(
155 "can't open file {path} in mode {flags:?}; GodotError: {:?}",
156 FileAccess::get_open_error()
157 ))
158 })?;
159
160 Ok(Self::from_inner(fa))
161 }
162
163 /// Open a file encrypted by password.
164 ///
165 /// Opens a file encrypted by a `password` located at `path`, creating new [`GFile`] object. For [`ModeFlags`]
166 /// description check the [`GFile`] documentation.
167 pub fn open_encrypted_with_pass(
168 path: impl AsArg<GString>,
169 flags: ModeFlags,
170 password: impl AsArg<GString>,
171 ) -> std::io::Result<Self> {
172 arg_into_ref!(path);
173 arg_into_ref!(password);
174
175 let fa = FileAccess::open_encrypted_with_pass(path, flags, password).ok_or_else(|| {
176 std::io::Error::other(format!(
177 "can't open file {path} in mode {flags:?}; GodotError: {:?}",
178 FileAccess::get_open_error()
179 ))
180 })?;
181 Ok(Self::from_inner(fa))
182 }
183
184 /// Creates new [`GFile`] from a [`FileAccess`] pointer with a reference count of 1.
185 ///
186 /// For this method to work, the provided `file_access` must be unique -- no other reference to it can exist.
187 /// Its state is retained: both [`ModeFlags`] with which it was open and current internal cursor position.
188 ///
189 /// See also [`into_inner`](Self::into_inner) for the opposite operation.
190 pub fn try_from_unique(file_access: Gd<FileAccess>) -> Result<Self, IoError> {
191 let file_access = IoError::check_unique_open_file_access(file_access)?;
192 Ok(Self::from_inner(file_access))
193 }
194
195 /// Retrieve inner pointer to the [`FileAccess`].
196 ///
197 /// This instance of `GFile` will be destroyed, but the file is kept open as long as there is at least one reference
198 /// pointing to it. Its state is retained: both [`ModeFlags`] with which it was opened and current internal cursor position.
199 ///
200 /// See also [`try_from_unique`](Self::try_from_unique) for the opposite operation.
201 pub fn into_inner(self) -> Gd<FileAccess> {
202 self.fa
203 }
204
205 // ----------------------------------------------------------------------------------------------------------------------------------------------
206 // Remaps of the internal FileAccess methods.
207
208 /// Get last modified time as a Unix timestamp.
209 #[doc(alias = "get_modified_time")]
210 pub fn modified_time(path: impl AsArg<GString>) -> std::io::Result<u64> {
211 arg_into_ref!(path);
212 let modified_time = FileAccess::get_modified_time(path);
213
214 if modified_time == 0 {
215 Err(std::io::Error::other(format!(
216 "can't retrieve last modified time: {path}"
217 )))
218 } else {
219 Ok(modified_time)
220 }
221 }
222
223 /// Calculates the MD5 checksum of the file at the given path.
224 #[doc(alias = "get_md5")]
225 pub fn md5(path: impl AsArg<GString>) -> std::io::Result<GString> {
226 arg_into_ref!(path);
227 let md5 = FileAccess::get_md5(path);
228
229 if md5.is_empty() {
230 Err(std::io::Error::other(format!(
231 "failed to compute file's MD5 checksum: {path}"
232 )))
233 } else {
234 Ok(md5)
235 }
236 }
237
238 /// Calculates the SHA-256 checksum of the file at the given path.
239 #[doc(alias = "get_sha256")]
240 pub fn sha256(path: impl AsArg<GString>) -> std::io::Result<GString> {
241 arg_into_ref!(path);
242 let sha256 = FileAccess::get_sha256(path);
243
244 if sha256.is_empty() {
245 Err(std::io::Error::other(format!(
246 "failed to compute file's SHA-256 checksum: {path}"
247 )))
248 } else {
249 Ok(sha256)
250 }
251 }
252
253 /// Reads the next byte from the file as [`u8`].
254 ///
255 /// Underlying Godot method:
256 /// [`FileAccess::get_8`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-8).
257 #[doc(alias = "get_8")]
258 pub fn read_u8(&mut self) -> std::io::Result<u8> {
259 let val = self.fa.get_8();
260 self.check_error()?;
261 Ok(val)
262 }
263
264 /// Reads the next 2 bytes from the file as [`u16`].
265 ///
266 /// Underlying Godot method:
267 /// [`FileAccess::get_16`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-16).
268 #[doc(alias = "get_16")]
269 pub fn read_u16(&mut self) -> std::io::Result<u16> {
270 let val = self.fa.get_16();
271 self.check_error()?;
272 Ok(val)
273 }
274
275 /// Reads the next 4 bytes from the file as [`u32`].
276 ///
277 /// Underlying Godot method:
278 /// [`FileAccess::get_32`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-32).
279 #[doc(alias = "get_32")]
280 pub fn read_u32(&mut self) -> std::io::Result<u32> {
281 let val = self.fa.get_32();
282 self.check_error()?;
283 Ok(val)
284 }
285
286 /// Reads the next 8 bytes from the file as [`u64`].
287 ///
288 /// Underlying Godot method:
289 /// [`FileAccess::get_64`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-64).
290 #[doc(alias = "get_64")]
291 pub fn read_u64(&mut self) -> std::io::Result<u64> {
292 let val = self.fa.get_64();
293 self.check_error()?;
294 Ok(val)
295 }
296
297 /// Reads a Pascal string (length-prefixed) from the current position.
298 ///
299 /// A _Pascal string_ is useful for writing and retrieving variable-length string data from binary files. It is saved with a
300 /// length prefix (as opposed to C strings, which end with a null terminator). Text is interpreted as UTF-8 encoded.
301 ///
302 /// See also:
303 /// - [Wikipedia article](https://en.wikipedia.org/wiki/String_(computer_science)#Length-prefixed)
304 /// - [Godot `FileAccess::get_pascal_string`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-pascal-string)
305 #[doc(alias = "get_pascal_string")]
306 pub fn read_pascal_string(&mut self) -> std::io::Result<GString> {
307 let val = self.fa.get_pascal_string();
308 self.check_error()?;
309 Ok(val)
310 }
311
312 /// Reads the next line of the file as [`GString`].
313 ///
314 /// To retrieve the file as [`String`] instead, use the [`Read`] trait method
315 /// [`read_to_string()`](https://doc.rust-lang.org/std/io/trait.Read.html#method.read_to_string).
316 ///
317 /// Underlying Godot method:
318 /// [`FileAccess::get_line`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-line).
319 #[doc(alias = "get_line")]
320 pub fn read_gstring_line(&mut self) -> std::io::Result<GString> {
321 let val = self.fa.get_line();
322 self.check_error()?;
323 Ok(val)
324 }
325
326 /// Reads the whole file as UTF-8 [`GString`].
327 ///
328 /// To retrieve the file as [`String`] instead, use the [`Read`] trait method
329 /// [`read_to_string()`](https://doc.rust-lang.org/std/io/trait.Read.html#method.read_to_string).
330 ///
331 /// Underlying Godot method:
332 /// [`FileAccess::get_as_text`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-as-text).
333 /// Note that Godot 4.6 removed `skip_cr` parameter in [PR #110867](https://github.com/godotengine/godot/pull/110867). This high-level
334 /// API does not provide it for any version, to avoid case differentiation.
335 #[doc(alias = "get_as_text")]
336 pub fn read_as_gstring_entire(&mut self) -> std::io::Result<GString> {
337 let val = self.fa.get_as_text();
338 self.check_error()?;
339 Ok(val)
340 }
341
342 /// Reads the next line of the file in delimiter-separated file.
343 ///
344 /// For reading traditional `CSV` format, provide comma (`','`) as `delim`.
345 ///
346 /// Underlying Godot method:
347 /// [`FileAccess::get_csv_line`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-csv-line).
348 #[doc(alias = "get_csv_line")]
349 pub fn read_csv_line(
350 &mut self,
351 delim: impl AsArg<GString>,
352 ) -> std::io::Result<PackedStringArray> {
353 arg_into_ref!(delim);
354
355 // FIXME: pass by-ref
356 let val = self.fa.get_csv_line_ex().delim(delim).done();
357 self.check_error()?;
358 Ok(val)
359 }
360
361 /// Reads the next 4 bytes from file as [`f32`].
362 ///
363 /// Underlying Godot method:
364 /// [`FileAccess::get_float`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-float).
365 #[doc(alias = "get_float")]
366 pub fn read_f32(&mut self) -> std::io::Result<f32> {
367 let val = self.fa.get_float();
368 self.check_error()?;
369 Ok(val)
370 }
371
372 /// Reads the next 8 bytes from file as [`f64`].
373 ///
374 /// Underlying Godot method:
375 /// [`FileAccess::get_double`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-double).
376 #[doc(alias = "get_double")]
377 pub fn read_f64(&mut self) -> std::io::Result<f64> {
378 let val = self.fa.get_double();
379 self.check_error()?;
380 Ok(val)
381 }
382
383 /// Reads the next 4 or 8 bytes from file as `real`, depending on configuration.
384 ///
385 /// See [`real`][type@real] type for more information.
386 ///
387 /// Underlying Godot method:
388 /// [`FileAccess::get_float`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-float) or
389 /// [`FileAccess::get_double`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-double)
390 /// (note that [`FileAccess::get_real`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-real)
391 /// does not return an actual `real`).
392 ///
393 /// <div class="warning">
394 /// <strong>Warning:</strong>
395 /// Since this involves a configuration-dependent type, you may not be able to read the value back if Godot uses different precision setting
396 /// (single or double) than the one used to write the value.
397 /// </div>
398 #[doc(alias = "get_real")]
399 pub fn read_real(&mut self) -> std::io::Result<real> {
400 #[cfg(feature = "double-precision")] #[cfg_attr(published_docs, doc(cfg(feature = "double-precision")))]
401 let val = self.fa.get_double();
402
403 #[cfg(not(feature = "double-precision"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "double-precision"))))]
404 let val = self.fa.get_float();
405
406 self.check_error()?;
407 Ok(val)
408 }
409
410 /// Reads the next [`Variant`] value from file.
411 ///
412 /// If `allow_objects` is set to `true`, objects will be decoded.
413 ///
414 /// <div class="warning">
415 /// <strong>Warning:</strong> Deserialized objects can contain code which gets executed. Do not use this option if the serialized object
416 /// comes from untrusted sources, to avoid potential security threats such as remote code execution.
417 /// </div>
418 ///
419 /// Underlying Godot method:
420 /// [`FileAccess::get_var`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-var).
421 #[doc(alias = "get_var")]
422 pub fn read_variant(&mut self, allow_objects: bool) -> std::io::Result<Variant> {
423 let val = self.fa.get_var_ex().allow_objects(allow_objects).done();
424 self.check_error()?;
425 Ok(val)
426 }
427
428 /// Writes [`u8`] as the next byte in the file.
429 ///
430 /// Underlying Godot method:
431 /// [`FileAccess::store_8`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-8).
432 #[doc(alias = "store_8")]
433 pub fn write_u8(&mut self, value: u8) -> std::io::Result<()> {
434 self.fa.store_8(value);
435 self.clear_file_length();
436 self.check_error()?;
437 Ok(())
438 }
439
440 /// Writes [`u16`] as the next 2 bytes in the file.
441 ///
442 /// Underlying Godot method:
443 /// [`FileAccess::store_16`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-16).
444 #[doc(alias = "store_16")]
445 pub fn write_u16(&mut self, value: u16) -> std::io::Result<()> {
446 self.fa.store_16(value);
447 self.clear_file_length();
448 self.check_error()?;
449 Ok(())
450 }
451
452 /// Writes [`u32`] as the next 4 bytes in the file.
453 ///
454 /// Underlying Godot method:
455 /// [`FileAccess::store_32`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-32).
456 #[doc(alias = "store_32")]
457 pub fn write_u32(&mut self, value: u32) -> std::io::Result<()> {
458 self.fa.store_32(value);
459 self.clear_file_length();
460 self.check_error()?;
461 Ok(())
462 }
463
464 /// Writes [`u64`] as the next 8 bytes in the file.
465 ///
466 /// Underlying Godot method:
467 /// [`FileAccess::store_64`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-64).
468 #[doc(alias = "store_64")]
469 pub fn write_u64(&mut self, value: u64) -> std::io::Result<()> {
470 self.fa.store_64(value);
471 self.clear_file_length();
472 self.check_error()?;
473 Ok(())
474 }
475
476 /// Writes [`f32`] as the 32 bits in the file.
477 ///
478 /// Underlying Godot method:
479 /// [`FileAccess::store_float`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-float).
480 #[doc(alias = "store_float")]
481 pub fn write_f32(&mut self, value: f32) -> std::io::Result<()> {
482 self.fa.store_float(value);
483 self.clear_file_length();
484 self.check_error()?;
485 Ok(())
486 }
487
488 /// Writes [`f64`] as the 64 bits in the file.
489 ///
490 /// Underlying Godot method:
491 /// [`FileAccess::store_double`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-double).
492 #[doc(alias = "store_double")]
493 pub fn write_f64(&mut self, value: f64) -> std::io::Result<()> {
494 self.fa.store_double(value);
495 self.clear_file_length();
496 self.check_error()?;
497 Ok(())
498 }
499
500 /// Writes a `real` (`f32` or `f64`) as the next 4 or 8 bytes in the file, depending on configuration.
501 ///
502 /// See [`real`][type@real] type for more information.
503 ///
504 /// Underlying Godot method:
505 /// [`FileAccess::store_float`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-float) or
506 /// [`FileAccess::store_double`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-double)
507 /// (note that [`FileAccess::store_real`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-real)
508 /// does not accept an actual `real`).
509 ///
510 /// <div class="warning">
511 /// <strong>Warning:</strong>
512 /// Since this involves a configuration-dependent type, you may not be able to read the value back if Godot uses different precision setting
513 /// (single or double) than the one used to write the value.
514 /// </div>
515 #[doc(alias = "store_real")]
516 pub fn write_real(&mut self, value: real) -> std::io::Result<()> {
517 // FileAccess::store_real() does not accept an actual real_t; work around this.
518
519 #[cfg(feature = "double-precision")] #[cfg_attr(published_docs, doc(cfg(feature = "double-precision")))]
520 self.fa.store_double(value);
521
522 #[cfg(not(feature = "double-precision"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "double-precision"))))]
523 self.fa.store_float(value);
524
525 self.clear_file_length();
526 self.check_error()?;
527 Ok(())
528 }
529
530 /// Writes string to the file.
531 ///
532 /// This function is meant to be used in text files. To store a string in a binary file, use `store_pascal_string()`
533 ///
534 /// Underlying Godot method:
535 /// [`FileAccess::store_string`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-string).
536 #[doc(alias = "store_string")]
537 pub fn write_gstring(&mut self, value: impl AsArg<GString>) -> std::io::Result<()> {
538 arg_into_ref!(value);
539
540 self.fa.store_string(value);
541 self.clear_file_length();
542 self.check_error()?;
543 Ok(())
544 }
545
546 /// Writes string to the file as Pascal String.
547 ///
548 /// This function is meant to be used in binary files. To store a string in a text file, use `store_string()`
549 ///
550 /// Pascal String is useful for writing and retrieving verying-length string data from binary files. It is saved
551 /// with length-prefix, instead of null terminator as in C strings.
552 ///
553 /// See also:
554 /// - [Wikipedia article](https://en.wikipedia.org/wiki/String_(computer_science)#Length-prefixed)
555 /// - [Godot `FileAccess::store_pascal_string`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-pascal-string)
556 #[doc(alias = "store_pascal_string")]
557 pub fn write_pascal_string(&mut self, value: impl AsArg<GString>) -> std::io::Result<()> {
558 arg_into_ref!(value);
559
560 self.fa.store_pascal_string(value);
561 self.clear_file_length();
562 self.check_error()?;
563 Ok(())
564 }
565
566 /// Write string to the file as a line.
567 ///
568 /// Underlying Godot method:
569 /// [`FileAccess::store_line`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-line).
570 #[doc(alias = "store_line")]
571 pub fn write_gstring_line(&mut self, value: impl AsArg<GString>) -> std::io::Result<()> {
572 arg_into_ref!(value);
573
574 self.fa.store_line(value);
575 self.clear_file_length();
576 self.check_error()?;
577 Ok(())
578 }
579
580 /// Write [`PackedStringArray`] to the file as delimited line.
581 ///
582 /// For writing traditional `CSV` format, provide comma (`','`) as `delim`.
583 ///
584 /// Underlying Godot method:
585 /// [`FileAccess::store_csv_line`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-csv-line).
586 #[doc(alias = "store_csv_line")]
587 pub fn write_csv_line(
588 &mut self,
589 values: &PackedStringArray,
590 delim: impl AsArg<GString>,
591 ) -> std::io::Result<()> {
592 arg_into_ref!(delim);
593
594 self.fa.store_csv_line_ex(values).delim(delim).done();
595 self.clear_file_length();
596 self.check_error()?;
597 Ok(())
598 }
599
600 /// Write [`Variant`] to the file.
601 ///
602 /// If `full_objects` is set to `true`, encoding objects is allowed (and can potentially include GDScript code). Not
603 /// all properties of the Variant are included. Only properties that are exported (have `#[export]` derive attribute)
604 /// will be serialized.
605 ///
606 /// Underlying Godot method:
607 /// [`FileAccess::store_var`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-var).
608 #[doc(alias = "store_var")]
609 pub fn write_variant(&mut self, value: Variant, full_objects: bool) -> std::io::Result<()> {
610 self.fa
611 .store_var_ex(&value)
612 .full_objects(full_objects)
613 .done();
614 self.clear_file_length();
615 self.check_error()?;
616 Ok(())
617 }
618
619 /// Set true to use big-endian, false to use little-endian.
620 ///
621 /// Endianness can be set mid-file, not only at the start position. It makes it possible to write different sections
622 /// of binary file with different endianness, though it is not recommended - can lead to confusion and mistakes during
623 /// consequent read operations.
624 pub fn set_big_endian(&mut self, value: bool) {
625 self.fa.set_big_endian(value);
626 }
627
628 /// Check endianness of current file access.
629 pub fn is_big_endian(&self) -> bool {
630 self.fa.is_big_endian()
631 }
632
633 /// Get path of the opened file.
634 #[doc(alias = "get_path")]
635 pub fn path(&self) -> GString {
636 self.fa.get_path()
637 }
638
639 /// Get absolute path of the opened file.
640 #[doc(alias = "get_path_absolute")]
641 pub fn path_absolute(&self) -> GString {
642 self.fa.get_path_absolute()
643 }
644
645 /// Returns the current cursor position.
646 #[doc(alias = "get_position")]
647 pub fn position(&self) -> u64 {
648 self.fa.get_position()
649 }
650
651 /// Get file length in bytes.
652 #[doc(alias = "get_length")]
653 pub fn length(&self) -> u64 {
654 self.fa.get_length()
655 }
656
657 /// Checks if the file cursor has read past the end of the file.
658 pub fn eof_reached(&self) -> bool {
659 self.fa.eof_reached()
660 }
661
662 // ----------------------------------------------------------------------------------------------------------------------------------------------
663 // Private methods.
664
665 // Error handling utility function.
666 fn check_error(&self) -> Result<(), std::io::Error> {
667 let error = self.fa.get_error();
668 if error == Error::OK {
669 return Ok(());
670 }
671
672 Err(std::io::Error::other(format!("GodotError: {error:?}")))
673 }
674
675 // File length cache is stored and kept when possible because `FileAccess::get_length()` turned out to be slowing down
676 // reading operations on big files with methods stemming from `std::io::Read` and `std::io::BufRead`.
677 fn check_file_length(&mut self) -> u64 {
678 if let Some(length) = self.file_length {
679 return length;
680 }
681 let file_length = self.fa.get_length();
682 self.file_length = Some(file_length);
683 file_length
684 }
685
686 // The file length cache is cleared during writing operations, as this is the only place when the file length could be
687 // changed - unless file is modified by some other `GFile`, but we cannot do anything about it then.
688 fn clear_file_length(&mut self) {
689 self.file_length = None;
690 }
691
692 // Private constructor function.
693 fn from_inner(fa: Gd<FileAccess>) -> Self {
694 let file_length = Some(fa.get_length());
695 Self {
696 fa,
697 buffer: vec![0; Self::BUFFER_SIZE],
698 last_buffer_size: 0,
699 write_buffer: PackedByteArray::new(),
700 file_length,
701 }
702 }
703
704 // Writer utilities.
705 fn extend_write_buffer(&mut self, len: usize) {
706 if self.write_buffer.len() >= len {
707 return;
708 }
709 self.write_buffer.resize(len)
710 }
711
712 fn pack_into_write_buffer(&mut self, buf: &[u8]) {
713 self.extend_write_buffer(buf.len());
714 let write_slice = self.write_buffer.as_mut_slice();
715 write_slice[0..buf.len()].copy_from_slice(buf);
716 }
717}
718
719// ----------------------------------------------------------------------------------------------------------------------------------------------
720// Trait implementations.
721
722impl Read for GFile {
723 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
724 let length = self.check_file_length();
725 let position = self.fa.get_position();
726 if position >= length {
727 return Ok(0);
728 }
729
730 let remaining_bytes = (length - position) as usize;
731 let bytes_to_read = cmp::min(buf.len(), remaining_bytes);
732 if bytes_to_read == 0 {
733 return Ok(0);
734 }
735
736 let gd_buffer = self.fa.get_buffer(bytes_to_read as i64);
737 let bytes_read = gd_buffer.len();
738 buf[0..bytes_read].copy_from_slice(gd_buffer.as_slice());
739
740 self.check_error()?;
741
742 Ok(bytes_read)
743 }
744}
745
746impl Write for GFile {
747 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
748 self.pack_into_write_buffer(buf);
749 self.fa
750 .store_buffer(&self.write_buffer.subarray(0..buf.len()));
751 self.clear_file_length();
752 self.check_error()?;
753
754 Ok(buf.len())
755 }
756
757 fn flush(&mut self) -> std::io::Result<()> {
758 self.fa.flush();
759 self.check_error()?;
760 Ok(())
761 }
762}
763
764impl Seek for GFile {
765 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
766 match pos {
767 SeekFrom::Start(position) => {
768 self.fa.seek(position);
769 self.check_error()?;
770 Ok(position)
771 }
772 SeekFrom::End(offset) => {
773 if (self.check_file_length() as i64) < offset {
774 return Err(std::io::Error::new(
775 ErrorKind::InvalidInput,
776 "Position can't be set before the file beginning",
777 ));
778 }
779 self.fa.seek_end_ex().position(offset).done();
780 self.check_error()?;
781 Ok(self.fa.get_position())
782 }
783 SeekFrom::Current(offset) => {
784 let new_pos = self.fa.get_position() as i64 + offset;
785 if new_pos < 0 {
786 return Err(std::io::Error::new(
787 ErrorKind::InvalidInput,
788 "Position can't be set before the file beginning",
789 ));
790 }
791 let new_pos = new_pos as u64;
792 self.fa.seek(new_pos);
793 self.check_error()?;
794 Ok(new_pos)
795 }
796 }
797 }
798}
799
800impl BufRead for GFile {
801 fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
802 // We need to determine number of remaining bytes - otherwise the `FileAccess::get_buffer return in an error`.
803 let remaining_bytes = self.check_file_length() - self.fa.get_position();
804 let buffer_read_size = cmp::min(remaining_bytes as usize, Self::BUFFER_SIZE);
805
806 // We need to keep the amount of last read side to be able to adjust cursor position in `consume`.
807 self.last_buffer_size = buffer_read_size;
808 self.buffer = vec![0; Self::BUFFER_SIZE];
809
810 let gd_buffer = self.fa.get_buffer(buffer_read_size as i64);
811 self.check_error()?;
812
813 let read_buffer = &mut self.buffer[0..gd_buffer.len()];
814
815 read_buffer.copy_from_slice(gd_buffer.as_slice());
816
817 Ok(read_buffer)
818 }
819
820 fn consume(&mut self, amt: usize) {
821 // Cursor is being moved by `FileAccess::get_buffer()` call, so we need to adjust it.
822 let offset = (self.last_buffer_size - amt) as i64;
823 let pos = SeekFrom::Current(-offset);
824
825 self.seek(pos).expect("failed to consume bytes during read");
826 }
827}