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::{real, GString, PackedByteArray, PackedStringArray, Variant};
12use crate::classes::file_access::{CompressionMode, ModeFlags};
13use crate::classes::FileAccess;
14use crate::global::Error;
15use crate::meta::error::IoError;
16use crate::meta::{arg_into_ref, AsArg};
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    /// If `skip_cr` is set to `true`, carriage return (`'\r'`) will be ignored, and only line feed (`'\n'`) indicates a new line.
329    ///
330    /// To retrieve the file as [`String`] instead, use the [`Read`] trait method
331    /// [`read_to_string()`](https://doc.rust-lang.org/std/io/trait.Read.html#method.read_to_string).
332    ///
333    /// Underlying Godot method:
334    /// [`FileAccess::get_as_text`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-as-text).
335    // For Godot versions before `skip_cr` has been removed, see: https://github.com/godotengine/godot/pull/110867.
336    #[doc(alias = "get_as_text")]
337    #[cfg(before_api = "4.6")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.6")))]
338    pub fn read_as_gstring_entire(&mut self, skip_cr: bool) -> std::io::Result<GString> {
339        let val = self.fa.get_as_text_ex().skip_cr(skip_cr).done();
340        self.check_error()?;
341        Ok(val)
342    }
343
344    /// Reads the whole file as UTF-8 [`GString`].
345    ///
346    /// To retrieve the file as [`String`] instead, use the [`Read`] trait method
347    /// [`read_to_string()`](https://doc.rust-lang.org/std/io/trait.Read.html#method.read_to_string).
348    ///
349    /// Underlying Godot method:
350    /// [`FileAccess::get_as_text`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-as-text).
351    #[doc(alias = "get_as_text")]
352    #[cfg(since_api = "4.6")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.6")))]
353    pub fn read_as_gstring_entire(&mut self) -> std::io::Result<GString> {
354        let val = self.fa.get_as_text();
355        self.check_error()?;
356        Ok(val)
357    }
358
359    /// Reads the next line of the file in delimiter-separated file.
360    ///
361    /// For reading traditional `CSV` format, provide comma (`','`) as `delim`.
362    ///
363    /// Underlying Godot method:
364    /// [`FileAccess::get_csv_line`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-csv-line).
365    #[doc(alias = "get_csv_line")]
366    pub fn read_csv_line(
367        &mut self,
368        delim: impl AsArg<GString>,
369    ) -> std::io::Result<PackedStringArray> {
370        arg_into_ref!(delim);
371
372        // FIXME: pass by-ref
373        let val = self.fa.get_csv_line_ex().delim(delim).done();
374        self.check_error()?;
375        Ok(val)
376    }
377
378    /// Reads the next 4 bytes from file as [`f32`].
379    ///
380    /// Underlying Godot method:
381    /// [`FileAccess::get_float`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-float).
382    #[doc(alias = "get_float")]
383    pub fn read_f32(&mut self) -> std::io::Result<f32> {
384        let val = self.fa.get_float();
385        self.check_error()?;
386        Ok(val)
387    }
388
389    /// Reads the next 8 bytes from file as [`f64`].
390    ///
391    /// Underlying Godot method:
392    /// [`FileAccess::get_double`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-double).
393    #[doc(alias = "get_double")]
394    pub fn read_f64(&mut self) -> std::io::Result<f64> {
395        let val = self.fa.get_double();
396        self.check_error()?;
397        Ok(val)
398    }
399
400    /// Reads the next 4 or 8 bytes from file as `real`, depending on configuration.
401    ///
402    /// See [`real`][type@real] type for more information.
403    ///
404    /// Underlying Godot method:
405    /// [`FileAccess::get_float`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-float) or
406    /// [`FileAccess::get_double`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-double)
407    /// (note that [`FileAccess::get_real`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-real)
408    /// does not return an actual `real`).
409    ///
410    /// <div class="warning">
411    /// <strong>Warning:</strong>
412    /// Since this involves a configuration-dependent type, you may not be able to read the value back if Godot uses different precision setting
413    /// (single or double) than the one used to write the value.
414    /// </div>
415    #[doc(alias = "get_real")]
416    pub fn read_real(&mut self) -> std::io::Result<real> {
417        #[cfg(feature = "double-precision")] #[cfg_attr(published_docs, doc(cfg(feature = "double-precision")))]
418        let val = self.fa.get_double();
419
420        #[cfg(not(feature = "double-precision"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "double-precision"))))]
421        let val = self.fa.get_float();
422
423        self.check_error()?;
424        Ok(val)
425    }
426
427    /// Reads the next [`Variant`] value from file.
428    ///
429    /// If `allow_objects` is set to `true`, objects will be decoded.
430    ///
431    /// <div class="warning">
432    /// <strong>Warning:</strong> Deserialized objects can contain code which gets executed. Do not use this option if the serialized object
433    /// comes from untrusted sources, to avoid potential security threats such as remote code execution.
434    /// </div>
435    ///
436    /// Underlying Godot method:
437    /// [`FileAccess::get_var`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-var).
438    #[doc(alias = "get_var")]
439    pub fn read_variant(&mut self, allow_objects: bool) -> std::io::Result<Variant> {
440        let val = self.fa.get_var_ex().allow_objects(allow_objects).done();
441        self.check_error()?;
442        Ok(val)
443    }
444
445    /// Writes [`u8`] as the next byte in the file.
446    ///
447    /// Underlying Godot method:
448    /// [`FileAccess::store_8`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-8).
449    #[doc(alias = "store_8")]
450    pub fn write_u8(&mut self, value: u8) -> std::io::Result<()> {
451        self.fa.store_8(value);
452        self.clear_file_length();
453        self.check_error()?;
454        Ok(())
455    }
456
457    /// Writes [`u16`] as the next 2 bytes in the file.
458    ///
459    /// Underlying Godot method:
460    /// [`FileAccess::store_16`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-16).
461    #[doc(alias = "store_16")]
462    pub fn write_u16(&mut self, value: u16) -> std::io::Result<()> {
463        self.fa.store_16(value);
464        self.clear_file_length();
465        self.check_error()?;
466        Ok(())
467    }
468
469    /// Writes [`u32`] as the next 4 bytes in the file.
470    ///
471    /// Underlying Godot method:
472    /// [`FileAccess::store_32`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-32).
473    #[doc(alias = "store_32")]
474    pub fn write_u32(&mut self, value: u32) -> std::io::Result<()> {
475        self.fa.store_32(value);
476        self.clear_file_length();
477        self.check_error()?;
478        Ok(())
479    }
480
481    /// Writes [`u64`] as the next 8 bytes in the file.
482    ///
483    /// Underlying Godot method:
484    /// [`FileAccess::store_64`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-64).
485    #[doc(alias = "store_64")]
486    pub fn write_u64(&mut self, value: u64) -> std::io::Result<()> {
487        self.fa.store_64(value);
488        self.clear_file_length();
489        self.check_error()?;
490        Ok(())
491    }
492
493    /// Writes [`f32`] as the 32 bits in the file.
494    ///
495    /// Underlying Godot method:
496    /// [`FileAccess::store_float`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-float).
497    #[doc(alias = "store_float")]
498    pub fn write_f32(&mut self, value: f32) -> std::io::Result<()> {
499        self.fa.store_float(value);
500        self.clear_file_length();
501        self.check_error()?;
502        Ok(())
503    }
504
505    /// Writes [`f64`] as the 64 bits in the file.
506    ///
507    /// Underlying Godot method:
508    /// [`FileAccess::store_double`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-double).
509    #[doc(alias = "store_double")]
510    pub fn write_f64(&mut self, value: f64) -> std::io::Result<()> {
511        self.fa.store_double(value);
512        self.clear_file_length();
513        self.check_error()?;
514        Ok(())
515    }
516
517    /// Writes a `real` (`f32` or `f64`) as the next 4 or 8 bytes in the file, depending on configuration.
518    ///
519    /// See [`real`][type@real] type for more information.
520    ///
521    /// Underlying Godot method:
522    /// [`FileAccess::store_float`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-float) or
523    /// [`FileAccess::store_double`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-double)
524    /// (note that [`FileAccess::store_real`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-real)
525    /// does not accept an actual `real`).
526    ///
527    /// <div class="warning">
528    /// <strong>Warning:</strong>
529    /// Since this involves a configuration-dependent type, you may not be able to read the value back if Godot uses different precision setting
530    /// (single or double) than the one used to write the value.
531    /// </div>
532    #[doc(alias = "store_real")]
533    pub fn write_real(&mut self, value: real) -> std::io::Result<()> {
534        // FileAccess::store_real() does not accept an actual real_t; work around this.
535
536        #[cfg(feature = "double-precision")] #[cfg_attr(published_docs, doc(cfg(feature = "double-precision")))]
537        self.fa.store_double(value);
538
539        #[cfg(not(feature = "double-precision"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "double-precision"))))]
540        self.fa.store_float(value);
541
542        self.clear_file_length();
543        self.check_error()?;
544        Ok(())
545    }
546
547    /// Writes string to the file.
548    ///
549    /// This function is meant to be used in text files. To store a string in a binary file, use `store_pascal_string()`
550    ///
551    /// Underlying Godot method:
552    /// [`FileAccess::store_string`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-string).
553    #[doc(alias = "store_string")]
554    pub fn write_gstring(&mut self, value: impl AsArg<GString>) -> std::io::Result<()> {
555        arg_into_ref!(value);
556
557        self.fa.store_string(value);
558        self.clear_file_length();
559        self.check_error()?;
560        Ok(())
561    }
562
563    /// Writes string to the file as Pascal String.
564    ///
565    /// This function is meant to be used in binary files. To store a string in a text file, use `store_string()`
566    ///
567    /// Pascal String is useful for writing and retrieving verying-length string data from binary files. It is saved
568    /// with length-prefix, instead of null terminator as in C strings.
569    ///
570    /// See also:
571    /// - [Wikipedia article](https://en.wikipedia.org/wiki/String_(computer_science)#Length-prefixed)
572    /// - [Godot `FileAccess::store_pascal_string`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-pascal-string)
573    #[doc(alias = "store_pascal_string")]
574    pub fn write_pascal_string(&mut self, value: impl AsArg<GString>) -> std::io::Result<()> {
575        arg_into_ref!(value);
576
577        self.fa.store_pascal_string(value);
578        self.clear_file_length();
579        self.check_error()?;
580        Ok(())
581    }
582
583    /// Write string to the file as a line.
584    ///
585    /// Underlying Godot method:
586    /// [`FileAccess::store_line`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-line).
587    #[doc(alias = "store_line")]
588    pub fn write_gstring_line(&mut self, value: impl AsArg<GString>) -> std::io::Result<()> {
589        arg_into_ref!(value);
590
591        self.fa.store_line(value);
592        self.clear_file_length();
593        self.check_error()?;
594        Ok(())
595    }
596
597    /// Write [`PackedStringArray`] to the file as delimited line.
598    ///
599    /// For writing traditional `CSV` format, provide comma (`','`) as `delim`.
600    ///
601    /// Underlying Godot method:
602    /// [`FileAccess::store_csv_line`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-csv-line).
603    #[doc(alias = "store_csv_line")]
604    pub fn write_csv_line(
605        &mut self,
606        values: &PackedStringArray,
607        delim: impl AsArg<GString>,
608    ) -> std::io::Result<()> {
609        arg_into_ref!(delim);
610
611        self.fa.store_csv_line_ex(values).delim(delim).done();
612        self.clear_file_length();
613        self.check_error()?;
614        Ok(())
615    }
616
617    /// Write [`Variant`] to the file.
618    ///
619    /// If `full_objects` is set to `true`, encoding objects is allowed (and can potentially include GDScript code). Not
620    /// all properties of the Variant are included. Only properties that are exported (have `#[export]` derive attribute)
621    /// will be serialized.
622    ///
623    /// Underlying Godot method:
624    /// [`FileAccess::store_var`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-var).
625    #[doc(alias = "store_var")]
626    pub fn write_variant(&mut self, value: Variant, full_objects: bool) -> std::io::Result<()> {
627        self.fa
628            .store_var_ex(&value)
629            .full_objects(full_objects)
630            .done();
631        self.clear_file_length();
632        self.check_error()?;
633        Ok(())
634    }
635
636    /// Set true to use big-endian, false to use little-endian.
637    ///
638    /// Endianness can be set mid-file, not only at the start position. It makes it possible to write different sections
639    /// of binary file with different endianness, though it is not recommended - can lead to confusion and mistakes during
640    /// consequent read operations.
641    pub fn set_big_endian(&mut self, value: bool) {
642        self.fa.set_big_endian(value);
643    }
644
645    /// Check endianness of current file access.
646    pub fn is_big_endian(&self) -> bool {
647        self.fa.is_big_endian()
648    }
649
650    /// Get path of the opened file.
651    #[doc(alias = "get_path")]
652    pub fn path(&self) -> GString {
653        self.fa.get_path()
654    }
655
656    /// Get absolute path of the opened file.
657    #[doc(alias = "get_path_absolute")]
658    pub fn path_absolute(&self) -> GString {
659        self.fa.get_path_absolute()
660    }
661
662    /// Returns the current cursor position.
663    #[doc(alias = "get_position")]
664    pub fn position(&self) -> u64 {
665        self.fa.get_position()
666    }
667
668    /// Get file length in bytes.
669    #[doc(alias = "get_length")]
670    pub fn length(&self) -> u64 {
671        self.fa.get_length()
672    }
673
674    /// Checks if the file cursor has read past the end of the file.
675    pub fn eof_reached(&self) -> bool {
676        self.fa.eof_reached()
677    }
678
679    // ----------------------------------------------------------------------------------------------------------------------------------------------
680    // Private methods.
681
682    // Error handling utility function.
683    fn check_error(&self) -> Result<(), std::io::Error> {
684        let error = self.fa.get_error();
685        if error == Error::OK {
686            return Ok(());
687        }
688
689        Err(std::io::Error::other(format!("GodotError: {error:?}")))
690    }
691
692    // File length cache is stored and kept when possible because `FileAccess::get_length()` turned out to be slowing down
693    // reading operations on big files with methods stemming from `std::io::Read` and `std::io::BufRead`.
694    fn check_file_length(&mut self) -> u64 {
695        if let Some(length) = self.file_length {
696            return length;
697        }
698        let file_length = self.fa.get_length();
699        self.file_length = Some(file_length);
700        file_length
701    }
702
703    // The file length cache is cleared during writing operations, as this is the only place when the file length could be
704    // changed - unless file is modified by some other `GFile`, but we cannot do anything about it then.
705    fn clear_file_length(&mut self) {
706        self.file_length = None;
707    }
708
709    // Private constructor function.
710    fn from_inner(fa: Gd<FileAccess>) -> Self {
711        let file_length = Some(fa.get_length());
712        Self {
713            fa,
714            buffer: vec![0; Self::BUFFER_SIZE],
715            last_buffer_size: 0,
716            write_buffer: PackedByteArray::new(),
717            file_length,
718        }
719    }
720
721    // Writer utilities.
722    fn extend_write_buffer(&mut self, len: usize) {
723        if self.write_buffer.len() >= len {
724            return;
725        }
726        self.write_buffer.resize(len)
727    }
728
729    fn pack_into_write_buffer(&mut self, buf: &[u8]) {
730        self.extend_write_buffer(buf.len());
731        let write_slice = self.write_buffer.as_mut_slice();
732        write_slice[0..buf.len()].copy_from_slice(buf);
733    }
734}
735
736// ----------------------------------------------------------------------------------------------------------------------------------------------
737// Trait implementations.
738
739impl Read for GFile {
740    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
741        let length = self.check_file_length();
742        let position = self.fa.get_position();
743        if position >= length {
744            return Ok(0);
745        }
746
747        let remaining_bytes = (length - position) as usize;
748        let bytes_to_read = cmp::min(buf.len(), remaining_bytes);
749        if bytes_to_read == 0 {
750            return Ok(0);
751        }
752
753        let gd_buffer = self.fa.get_buffer(bytes_to_read as i64);
754        let bytes_read = gd_buffer.len();
755        buf[0..bytes_read].copy_from_slice(gd_buffer.as_slice());
756
757        self.check_error()?;
758
759        Ok(bytes_read)
760    }
761}
762
763impl Write for GFile {
764    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
765        self.pack_into_write_buffer(buf);
766        self.fa
767            .store_buffer(&self.write_buffer.subarray(0..buf.len()));
768        self.clear_file_length();
769        self.check_error()?;
770
771        Ok(buf.len())
772    }
773
774    fn flush(&mut self) -> std::io::Result<()> {
775        self.fa.flush();
776        self.check_error()?;
777        Ok(())
778    }
779}
780
781impl Seek for GFile {
782    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
783        match pos {
784            SeekFrom::Start(position) => {
785                self.fa.seek(position);
786                self.check_error()?;
787                Ok(position)
788            }
789            SeekFrom::End(offset) => {
790                if (self.check_file_length() as i64) < offset {
791                    return Err(std::io::Error::new(
792                        ErrorKind::InvalidInput,
793                        "Position can't be set before the file beginning",
794                    ));
795                }
796                self.fa.seek_end_ex().position(offset).done();
797                self.check_error()?;
798                Ok(self.fa.get_position())
799            }
800            SeekFrom::Current(offset) => {
801                let new_pos = self.fa.get_position() as i64 + offset;
802                if new_pos < 0 {
803                    return Err(std::io::Error::new(
804                        ErrorKind::InvalidInput,
805                        "Position can't be set before the file beginning",
806                    ));
807                }
808                let new_pos = new_pos as u64;
809                self.fa.seek(new_pos);
810                self.check_error()?;
811                Ok(new_pos)
812            }
813        }
814    }
815}
816
817impl BufRead for GFile {
818    fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
819        // We need to determine number of remaining bytes - otherwise the `FileAccess::get_buffer return in an error`.
820        let remaining_bytes = self.check_file_length() - self.fa.get_position();
821        let buffer_read_size = cmp::min(remaining_bytes as usize, Self::BUFFER_SIZE);
822
823        // We need to keep the amount of last read side to be able to adjust cursor position in `consume`.
824        self.last_buffer_size = buffer_read_size;
825        self.buffer = vec![0; Self::BUFFER_SIZE];
826
827        let gd_buffer = self.fa.get_buffer(buffer_read_size as i64);
828        self.check_error()?;
829
830        let read_buffer = &mut self.buffer[0..gd_buffer.len()];
831
832        read_buffer.copy_from_slice(gd_buffer.as_slice());
833
834        Ok(read_buffer)
835    }
836
837    fn consume(&mut self, amt: usize) {
838        // Cursor is being moved by `FileAccess::get_buffer()` call, so we need to adjust it.
839        let offset = (self.last_buffer_size - amt) as i64;
840        let pos = SeekFrom::Current(-offset);
841
842        self.seek(pos).expect("failed to consume bytes during read");
843    }
844}