gio/subclass/
file_enumerator.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use glib::{prelude::*, subclass::prelude::*, translate::*};
4
5use crate::{Cancellable, FileEnumerator, FileInfo, IOErrorEnum, ffi, prelude::*};
6
7// Support custom implementation of virtual functions defined in `gio::ffi::GFileEnumeratorClass` except pairs `xxx_async/xxx_finish` for which GIO provides a default implementation (which should be ok).
8// TODO: overriding these default implementations might still be useful for subclasses (if they can do something better than blocking IO).
9pub trait FileEnumeratorImpl: ObjectImpl + ObjectSubclass<Type: IsA<FileEnumerator>> {
10    /// Returns information for the next file in the enumerated object.
11    /// Will block until the information is available. The #GFileInfo
12    /// returned from this function will contain attributes that match the
13    /// attribute string that was passed when the #GFileEnumerator was created.
14    ///
15    /// See the documentation of #GFileEnumerator for information about the
16    /// order of returned files.
17    ///
18    /// On error, returns [`None`] and sets @error to the error. If the
19    /// enumerator is at the end, [`None`] will be returned and @error will
20    /// be unset.
21    /// ## `cancellable`
22    /// optional #GCancellable object, [`None`] to ignore.
23    ///
24    /// # Returns
25    ///
26    /// A #GFileInfo or [`None`] on error
27    ///    or end of enumerator.  Free the returned object with
28    ///    g_object_unref() when no longer needed.
29    fn next_file(
30        &self,
31        cancellable: Option<&Cancellable>,
32    ) -> Result<Option<FileInfo>, glib::Error> {
33        self.parent_next_file(cancellable)
34    }
35
36    // rustdoc-stripper-ignore-next
37    /// Closes the enumerator (see [`FileEnumeratorExt::close`]).
38    ///
39    /// NOTE: If the enumerator has not been explicitly closed, GIO closes it when the object is dropped.
40    /// But GIO does it by calling `close` vfunc in `finalize`, which is not safe and could lead to undefined behavior,
41    /// such as accessing freed memory or resources, which can cause crashes or other unexpected behavior.
42    ///
43    /// An issue has been opened in GLib to address this: <https://gitlab.gnome.org/GNOME/glib/-/issues/3713> and a MR has been opened to fix it: <https://gitlab.gnome.org/GNOME/glib/-/merge_requests/4672>.
44    ///
45    /// Until this is fixed, it is unsafe to rely on the enumerator being closed when the object is dropped.
46    /// It is recommended to close the enumerator explicitly before dropping it, by calling [`FileEnumeratorExt::close`],
47    /// or to implement the [`ObjectImpl::dispose`] method and call [`FileEnumeratorExt::close`] there (it is safe to access the object there):
48    /// ```ignore
49    /// pub struct MyFileEnumerator();
50    ///
51    /// #[glib::object_subclass]
52    /// impl ObjectSubclass for MyFileEnumerator { ... }
53    ///
54    /// impl ObjectImpl for MyFileEnumerator {
55    ///     fn dispose(&self) {
56    ///         // close the enumerator here is safe and avoids `finalize` to call close.
57    ///         let _ = self.obj().close(Cancellable::NONE);
58    ///     }
59    /// }
60    ///
61    /// impl FileEnumeratorImpl for MyFileEnumerator { ... }
62    /// ```
63    ///
64    /// [`FileEnumeratorExt::close`]: ../auto/file_enumerator/trait.FileEnumeratorExt.html#method.close
65    /// [`ObjectImpl::dispose`]: ../../glib/subclass/object/trait.ObjectImpl.html#method.dispose
66    fn close(&self, cancellable: Option<&Cancellable>) -> (bool, Option<glib::Error>) {
67        self.parent_close(cancellable)
68    }
69}
70
71// Support parent implementation of virtual functions defined in `gio::ffi::GFileEnumeratorClass` except pairs `xxx_async/xxx_finish` for which GIO provides a default implementation (which should be ok).
72// TODO: add parent implementation of `xxx_async/xxx_finish` virtual functions if overriding these default implementations is supported.
73pub trait FileEnumeratorImplExt: FileEnumeratorImpl {
74    fn parent_next_file(
75        &self,
76        cancellable: Option<&Cancellable>,
77    ) -> Result<Option<FileInfo>, glib::Error> {
78        if self.obj().is_closed() {
79            Err(glib::Error::new::<IOErrorEnum>(
80                IOErrorEnum::Closed,
81                "Enumerator is closed",
82            ))
83        } else {
84            unsafe {
85                let data = Self::type_data();
86                let parent_class = data.as_ref().parent_class() as *const ffi::GFileEnumeratorClass;
87
88                let f = (*parent_class)
89                    .next_file
90                    .expect("No parent class implementation for \"next_file\"");
91
92                let mut error = std::ptr::null_mut();
93                let res = f(
94                    self.obj()
95                        .unsafe_cast_ref::<FileEnumerator>()
96                        .to_glib_none()
97                        .0,
98                    cancellable.as_ref().to_glib_none().0,
99                    &mut error,
100                );
101                if error.is_null() {
102                    Ok(from_glib_full(res))
103                } else {
104                    Err(from_glib_full(error))
105                }
106            }
107        }
108    }
109
110    fn parent_close(&self, cancellable: Option<&Cancellable>) -> (bool, Option<glib::Error>) {
111        unsafe {
112            let data = Self::type_data();
113            let parent_class = data.as_ref().parent_class() as *const ffi::GFileEnumeratorClass;
114
115            let f = (*parent_class)
116                .close_fn
117                .expect("No parent class implementation for \"close_fn\"");
118
119            let mut error = std::ptr::null_mut();
120            let is_ok = f(
121                self.obj()
122                    .unsafe_cast_ref::<FileEnumerator>()
123                    .to_glib_none()
124                    .0,
125                cancellable.as_ref().to_glib_none().0,
126                &mut error,
127            );
128            (from_glib(is_ok), from_glib_full(error))
129        }
130    }
131}
132
133impl<T: FileEnumeratorImpl> FileEnumeratorImplExt for T {}
134
135// Implement virtual functions defined in `gio::ffi::GFileEnumeratorClass` except pairs `xxx_async/xxx_finish` for which GIO provides a default implementation.
136unsafe impl<T: FileEnumeratorImpl> IsSubclassable<T> for FileEnumerator {
137    fn class_init(class: &mut ::glib::Class<Self>) {
138        Self::parent_class_init::<T>(class);
139
140        let klass = class.as_mut();
141        klass.next_file = Some(next_file::<T>);
142        klass.close_fn = Some(close_fn::<T>);
143        // `GFileEnumerator` already implements `xxx_async/xxx_finish` vfuncs and this should be ok.
144        // TODO: when needed, override the `GFileEnumerator` implementation of the following vfuncs:
145        // klass.next_files_async = Some(next_files_async::<T>);
146        // klass.next_files_finish = Some(next_files_finish::<T>);
147        // klass.close_async = Some(close_async::<T>);
148        // klass.close_finish = Some(close_finish::<T>);
149    }
150}
151
152unsafe extern "C" fn next_file<T: FileEnumeratorImpl>(
153    enumerator: *mut ffi::GFileEnumerator,
154    cancellable: *mut ffi::GCancellable,
155    error: *mut *mut glib::ffi::GError,
156) -> *mut ffi::GFileInfo {
157    unsafe {
158        let instance = &*(enumerator as *mut T::Instance);
159        let imp = instance.imp();
160        let cancellable = Option::<Cancellable>::from_glib_none(cancellable);
161
162        let res = imp.next_file(cancellable.as_ref());
163
164        match res {
165            Ok(fileinfo) => fileinfo.to_glib_full(),
166            Err(err) => {
167                if !error.is_null() {
168                    *error = err.to_glib_full()
169                }
170                std::ptr::null_mut()
171            }
172        }
173    }
174}
175
176unsafe extern "C" fn close_fn<T: FileEnumeratorImpl>(
177    enumerator: *mut ffi::GFileEnumerator,
178    cancellable: *mut ffi::GCancellable,
179    error: *mut *mut glib::ffi::GError,
180) -> glib::ffi::gboolean {
181    unsafe {
182        let instance = &*(enumerator as *mut T::Instance);
183        let imp = instance.imp();
184        let cancellable = Option::<Cancellable>::from_glib_none(cancellable);
185
186        let res = imp.close(cancellable.as_ref());
187
188        if !error.is_null() {
189            *error = res.1.to_glib_full()
190        }
191
192        res.0.into_glib()
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    // The following tests rely on a custom type `MyCustomFileEnumerator` that extends another custom type `MyFileEnumerator`.
199    // For each virtual method defined in class `gio::ffi::GFileEnumeratorClass`, a test checks that `MyCustomFileEnumerator` and `MyFileEnumerator` return the same results.
200
201    use super::*;
202
203    // Define `MyCustomFileEnumerator` as a subclass of `MyFileEnumerator`.
204    mod imp {
205        use std::cell::Cell;
206
207        use super::*;
208
209        #[derive(Default)]
210        pub struct MyFileEnumerator(Cell<i32>);
211
212        #[glib::object_subclass]
213        impl ObjectSubclass for MyFileEnumerator {
214            const NAME: &'static str = "MyFileEnumerator";
215            type Type = super::MyFileEnumerator;
216            type ParentType = FileEnumerator;
217        }
218
219        impl ObjectImpl for MyFileEnumerator {
220            fn dispose(&self) {
221                let _ = self.obj().close(Cancellable::NONE);
222            }
223        }
224
225        // Implements `FileEnumeratorImpl` with custom implementation.
226        impl FileEnumeratorImpl for MyFileEnumerator {
227            fn next_file(
228                &self,
229                cancellable: Option<&Cancellable>,
230            ) -> Result<Option<FileInfo>, glib::Error> {
231                if cancellable.is_some_and(|c| c.is_cancelled()) {
232                    Err(glib::Error::new::<IOErrorEnum>(
233                        IOErrorEnum::Cancelled,
234                        "Operation was cancelled",
235                    ))
236                } else {
237                    match self.0.get() {
238                        -1 => Err(glib::Error::new::<IOErrorEnum>(
239                            IOErrorEnum::Closed,
240                            "Enumerator is closed",
241                        )),
242                        i if i < 3 => {
243                            let file_info = FileInfo::new();
244                            file_info.set_display_name(&format!("file{i}"));
245                            self.0.set(i + 1);
246                            Ok(Some(file_info))
247                        }
248                        _ => Ok(None),
249                    }
250                }
251            }
252
253            fn close(&self, cancellable: Option<&Cancellable>) -> (bool, Option<glib::Error>) {
254                if cancellable.is_some_and(|c| c.is_cancelled()) {
255                    (
256                        false,
257                        Some(glib::Error::new::<IOErrorEnum>(
258                            IOErrorEnum::Cancelled,
259                            "Operation was cancelled",
260                        )),
261                    )
262                } else {
263                    self.0.set(-1);
264                    (true, None)
265                }
266            }
267        }
268
269        #[derive(Default)]
270        pub struct MyCustomFileEnumerator;
271
272        #[glib::object_subclass]
273        impl ObjectSubclass for MyCustomFileEnumerator {
274            const NAME: &'static str = "MyCustomFileEnumerator";
275            type Type = super::MyCustomFileEnumerator;
276            type ParentType = super::MyFileEnumerator;
277        }
278
279        impl ObjectImpl for MyCustomFileEnumerator {}
280
281        // Implements `FileEnumeratorImpl` with default implementation, which calls the parent's implementation.
282        impl FileEnumeratorImpl for MyCustomFileEnumerator {}
283
284        impl MyFileEnumeratorImpl for MyCustomFileEnumerator {}
285    }
286
287    glib::wrapper! {
288        pub struct MyFileEnumerator(ObjectSubclass<imp::MyFileEnumerator>) @extends FileEnumerator;
289    }
290
291    pub trait MyFileEnumeratorImpl:
292        ObjectImpl + ObjectSubclass<Type: IsA<MyFileEnumerator> + IsA<FileEnumerator>>
293    {
294    }
295
296    // To make this class subclassable we need to implement IsSubclassable
297    unsafe impl<T: MyFileEnumeratorImpl + FileEnumeratorImpl> IsSubclassable<T> for MyFileEnumerator {}
298
299    glib::wrapper! {
300        pub struct MyCustomFileEnumerator(ObjectSubclass<imp::MyCustomFileEnumerator>) @extends MyFileEnumerator, FileEnumerator;
301    }
302
303    #[test]
304    fn file_enumerator_next_file() {
305        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
306        let my_custom_file_enumerator = glib::Object::new::<MyCustomFileEnumerator>();
307        let res = my_custom_file_enumerator.next_file(Cancellable::NONE);
308        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
309        let filename = res.unwrap().unwrap().display_name();
310
311        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
312        let my_file_enumerator = glib::Object::new::<MyFileEnumerator>();
313        let res = my_file_enumerator.next_file(Cancellable::NONE);
314        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
315        let expected = res.unwrap().unwrap().display_name();
316
317        // both filenames should equal
318        assert_eq!(filename, expected);
319
320        // and also next results until there is no more file info
321        for res in my_custom_file_enumerator.upcast::<FileEnumerator>() {
322            assert!(res.as_ref().is_ok());
323            let filename = res.unwrap().display_name();
324
325            let res = my_file_enumerator.next_file(Cancellable::NONE);
326            assert!(res.as_ref().is_ok_and(|res| res.is_some()));
327            let expected = res.unwrap().unwrap().display_name();
328
329            // both filenames should equal
330            assert_eq!(filename, expected);
331        }
332    }
333
334    #[test]
335    fn file_enumerator_close() {
336        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
337        let my_custom_file_enumerator = glib::Object::new::<MyCustomFileEnumerator>();
338        let res = my_custom_file_enumerator.next_file(Cancellable::NONE);
339        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
340        let filename = res.unwrap().unwrap().display_name();
341
342        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
343        let my_file_enumerator = glib::Object::new::<MyFileEnumerator>();
344        let res = my_file_enumerator.next_file(Cancellable::NONE);
345        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
346        let expected = res.unwrap().unwrap().display_name();
347
348        // both filenames should equal
349        assert_eq!(filename, expected);
350
351        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::close`
352        let res = my_custom_file_enumerator.close(Cancellable::NONE);
353        assert_eq!(res.1, None);
354        let closed = res.0;
355
356        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::close`
357        let res = my_file_enumerator.close(Cancellable::NONE);
358        assert_eq!(res.1, None);
359        let expected = res.0;
360
361        // both results should equal
362        assert_eq!(closed, expected);
363
364        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
365        let res = my_custom_file_enumerator.next_file(Cancellable::NONE);
366        assert!(res.is_err());
367        let err = res.unwrap_err();
368
369        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
370        let res = my_file_enumerator.next_file(Cancellable::NONE);
371        assert!(res.is_err());
372        let expected = res.unwrap_err();
373
374        // both errors should equal
375        assert_eq!(err.domain(), expected.domain());
376        assert!(err.matches::<IOErrorEnum>(IOErrorEnum::Closed));
377        assert!(expected.matches::<IOErrorEnum>(IOErrorEnum::Closed));
378        assert_eq!(err.message(), expected.message());
379    }
380
381    #[test]
382    fn file_enumerator_cancel() {
383        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
384        let my_custom_file_enumerator = glib::Object::new::<MyCustomFileEnumerator>();
385        let res = my_custom_file_enumerator.next_file(Cancellable::NONE);
386        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
387        let filename = res.unwrap().unwrap().display_name();
388
389        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
390        let my_file_enumerator = glib::Object::new::<MyFileEnumerator>();
391        let res = my_file_enumerator.next_file(Cancellable::NONE);
392        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
393        let expected = res.unwrap().unwrap().display_name();
394
395        // both filenames should equal
396        assert_eq!(filename, expected);
397
398        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file` with cancel
399        let cancellable = Cancellable::new();
400        cancellable.cancel();
401        let res = my_custom_file_enumerator.next_file(Some(&cancellable));
402        assert!(res.as_ref().is_err());
403        let err = res.unwrap_err();
404
405        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file` with cancel
406        let cancellable = Cancellable::new();
407        cancellable.cancel();
408        let res = my_file_enumerator.next_file(Some(&cancellable));
409        assert!(res.as_ref().is_err());
410        let expected = res.unwrap_err();
411
412        // both errors should equal
413        assert_eq!(err.domain(), expected.domain());
414        assert!(err.matches::<IOErrorEnum>(IOErrorEnum::Cancelled));
415        assert!(expected.matches::<IOErrorEnum>(IOErrorEnum::Cancelled));
416        assert_eq!(err.message(), expected.message());
417
418        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::close` with cancel
419        let cancellable = Cancellable::new();
420        cancellable.cancel();
421        let res = my_custom_file_enumerator.close(Some(&cancellable));
422        assert!(res.1.is_some());
423        let err = res.1.unwrap();
424
425        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::close` with cancel
426        let cancellable = Cancellable::new();
427        cancellable.cancel();
428        let res = my_file_enumerator.close(Some(&cancellable));
429        assert!(res.1.is_some());
430        let expected = res.1.unwrap();
431
432        // both errors should equal
433        assert_eq!(err.domain(), expected.domain());
434        assert!(err.matches::<IOErrorEnum>(IOErrorEnum::Cancelled));
435        assert!(expected.matches::<IOErrorEnum>(IOErrorEnum::Cancelled));
436        assert_eq!(err.message(), expected.message());
437    }
438}