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}