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::{ffi, prelude::*, Cancellable, FileEnumerator, FileInfo, IOErrorEnum};
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    let instance = &*(enumerator as *mut T::Instance);
158    let imp = instance.imp();
159    let cancellable = Option::<Cancellable>::from_glib_none(cancellable);
160
161    let res = imp.next_file(cancellable.as_ref());
162
163    match res {
164        Ok(fileinfo) => fileinfo.to_glib_full(),
165        Err(err) => {
166            if !error.is_null() {
167                *error = err.to_glib_full()
168            }
169            std::ptr::null_mut()
170        }
171    }
172}
173
174unsafe extern "C" fn close_fn<T: FileEnumeratorImpl>(
175    enumerator: *mut ffi::GFileEnumerator,
176    cancellable: *mut ffi::GCancellable,
177    error: *mut *mut glib::ffi::GError,
178) -> glib::ffi::gboolean {
179    let instance = &*(enumerator as *mut T::Instance);
180    let imp = instance.imp();
181    let cancellable = Option::<Cancellable>::from_glib_none(cancellable);
182
183    let res = imp.close(cancellable.as_ref());
184
185    if !error.is_null() {
186        *error = res.1.to_glib_full()
187    }
188
189    res.0.into_glib()
190}
191
192#[cfg(test)]
193mod tests {
194    // The following tests rely on a custom type `MyCustomFileEnumerator` that extends another custom type `MyFileEnumerator`.
195    // For each virtual method defined in class `gio::ffi::GFileEnumeratorClass`, a test checks that `MyCustomFileEnumerator` and `MyFileEnumerator` return the same results.
196
197    use super::*;
198
199    // Define `MyCustomFileEnumerator` as a subclass of `MyFileEnumerator`.
200    mod imp {
201        use std::cell::Cell;
202
203        use super::*;
204
205        #[derive(Default)]
206        pub struct MyFileEnumerator(Cell<i32>);
207
208        #[glib::object_subclass]
209        impl ObjectSubclass for MyFileEnumerator {
210            const NAME: &'static str = "MyFileEnumerator";
211            type Type = super::MyFileEnumerator;
212            type ParentType = FileEnumerator;
213        }
214
215        impl ObjectImpl for MyFileEnumerator {
216            fn dispose(&self) {
217                let _ = self.obj().close(Cancellable::NONE);
218            }
219        }
220
221        // Implements `FileEnumeratorImpl` with custom implementation.
222        impl FileEnumeratorImpl for MyFileEnumerator {
223            fn next_file(
224                &self,
225                cancellable: Option<&Cancellable>,
226            ) -> Result<Option<FileInfo>, glib::Error> {
227                if cancellable.is_some_and(|c| c.is_cancelled()) {
228                    Err(glib::Error::new::<IOErrorEnum>(
229                        IOErrorEnum::Cancelled,
230                        "Operation was cancelled",
231                    ))
232                } else {
233                    match self.0.get() {
234                        -1 => Err(glib::Error::new::<IOErrorEnum>(
235                            IOErrorEnum::Closed,
236                            "Enumerator is closed",
237                        )),
238                        i if i < 3 => {
239                            let file_info = FileInfo::new();
240                            file_info.set_display_name(&format!("file{i}"));
241                            self.0.set(i + 1);
242                            Ok(Some(file_info))
243                        }
244                        _ => Ok(None),
245                    }
246                }
247            }
248
249            fn close(&self, cancellable: Option<&Cancellable>) -> (bool, Option<glib::Error>) {
250                if cancellable.is_some_and(|c| c.is_cancelled()) {
251                    (
252                        false,
253                        Some(glib::Error::new::<IOErrorEnum>(
254                            IOErrorEnum::Cancelled,
255                            "Operation was cancelled",
256                        )),
257                    )
258                } else {
259                    self.0.set(-1);
260                    (true, None)
261                }
262            }
263        }
264
265        #[derive(Default)]
266        pub struct MyCustomFileEnumerator;
267
268        #[glib::object_subclass]
269        impl ObjectSubclass for MyCustomFileEnumerator {
270            const NAME: &'static str = "MyCustomFileEnumerator";
271            type Type = super::MyCustomFileEnumerator;
272            type ParentType = super::MyFileEnumerator;
273        }
274
275        impl ObjectImpl for MyCustomFileEnumerator {}
276
277        // Implements `FileEnumeratorImpl` with default implementation, which calls the parent's implementation.
278        impl FileEnumeratorImpl for MyCustomFileEnumerator {}
279
280        impl MyFileEnumeratorImpl for MyCustomFileEnumerator {}
281    }
282
283    glib::wrapper! {
284        pub struct MyFileEnumerator(ObjectSubclass<imp::MyFileEnumerator>) @extends FileEnumerator;
285    }
286
287    pub trait MyFileEnumeratorImpl:
288        ObjectImpl + ObjectSubclass<Type: IsA<MyFileEnumerator> + IsA<FileEnumerator>>
289    {
290    }
291
292    // To make this class subclassable we need to implement IsSubclassable
293    unsafe impl<T: MyFileEnumeratorImpl + FileEnumeratorImpl> IsSubclassable<T> for MyFileEnumerator {}
294
295    glib::wrapper! {
296        pub struct MyCustomFileEnumerator(ObjectSubclass<imp::MyCustomFileEnumerator>) @extends MyFileEnumerator, FileEnumerator;
297    }
298
299    #[test]
300    fn file_enumerator_next_file() {
301        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
302        let my_custom_file_enumerator = glib::Object::new::<MyCustomFileEnumerator>();
303        let res = my_custom_file_enumerator.next_file(Cancellable::NONE);
304        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
305        let filename = res.unwrap().unwrap().display_name();
306
307        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
308        let my_file_enumerator = glib::Object::new::<MyFileEnumerator>();
309        let res = my_file_enumerator.next_file(Cancellable::NONE);
310        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
311        let expected = res.unwrap().unwrap().display_name();
312
313        // both filenames should equal
314        assert_eq!(filename, expected);
315
316        // and also next results until there is no more file info
317        for res in my_custom_file_enumerator.upcast::<FileEnumerator>() {
318            assert!(res.as_ref().is_ok());
319            let filename = res.unwrap().display_name();
320
321            let res = my_file_enumerator.next_file(Cancellable::NONE);
322            assert!(res.as_ref().is_ok_and(|res| res.is_some()));
323            let expected = res.unwrap().unwrap().display_name();
324
325            // both filenames should equal
326            assert_eq!(filename, expected);
327        }
328    }
329
330    #[test]
331    fn file_enumerator_close() {
332        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
333        let my_custom_file_enumerator = glib::Object::new::<MyCustomFileEnumerator>();
334        let res = my_custom_file_enumerator.next_file(Cancellable::NONE);
335        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
336        let filename = res.unwrap().unwrap().display_name();
337
338        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
339        let my_file_enumerator = glib::Object::new::<MyFileEnumerator>();
340        let res = my_file_enumerator.next_file(Cancellable::NONE);
341        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
342        let expected = res.unwrap().unwrap().display_name();
343
344        // both filenames should equal
345        assert_eq!(filename, expected);
346
347        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::close`
348        let res = my_custom_file_enumerator.close(Cancellable::NONE);
349        assert_eq!(res.1, None);
350        let closed = res.0;
351
352        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::close`
353        let res = my_file_enumerator.close(Cancellable::NONE);
354        assert_eq!(res.1, None);
355        let expected = res.0;
356
357        // both results should equal
358        assert_eq!(closed, expected);
359
360        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
361        let res = my_custom_file_enumerator.next_file(Cancellable::NONE);
362        assert!(res.is_err());
363        let err = res.unwrap_err();
364
365        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
366        let res = my_file_enumerator.next_file(Cancellable::NONE);
367        assert!(res.is_err());
368        let expected = res.unwrap_err();
369
370        // both errors should equal
371        assert_eq!(err.domain(), expected.domain());
372        assert!(err.matches::<IOErrorEnum>(IOErrorEnum::Closed));
373        assert!(expected.matches::<IOErrorEnum>(IOErrorEnum::Closed));
374        assert_eq!(err.message(), expected.message());
375    }
376
377    #[test]
378    fn file_enumerator_cancel() {
379        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
380        let my_custom_file_enumerator = glib::Object::new::<MyCustomFileEnumerator>();
381        let res = my_custom_file_enumerator.next_file(Cancellable::NONE);
382        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
383        let filename = res.unwrap().unwrap().display_name();
384
385        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file`
386        let my_file_enumerator = glib::Object::new::<MyFileEnumerator>();
387        let res = my_file_enumerator.next_file(Cancellable::NONE);
388        assert!(res.as_ref().is_ok_and(|res| res.is_some()));
389        let expected = res.unwrap().unwrap().display_name();
390
391        // both filenames should equal
392        assert_eq!(filename, expected);
393
394        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file` with cancel
395        let cancellable = Cancellable::new();
396        cancellable.cancel();
397        let res = my_custom_file_enumerator.next_file(Some(&cancellable));
398        assert!(res.as_ref().is_err());
399        let err = res.unwrap_err();
400
401        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::next_file` with cancel
402        let cancellable = Cancellable::new();
403        cancellable.cancel();
404        let res = my_file_enumerator.next_file(Some(&cancellable));
405        assert!(res.as_ref().is_err());
406        let expected = res.unwrap_err();
407
408        // both errors should equal
409        assert_eq!(err.domain(), expected.domain());
410        assert!(err.matches::<IOErrorEnum>(IOErrorEnum::Cancelled));
411        assert!(expected.matches::<IOErrorEnum>(IOErrorEnum::Cancelled));
412        assert_eq!(err.message(), expected.message());
413
414        // invoke `MyCustomFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::close` with cancel
415        let cancellable = Cancellable::new();
416        cancellable.cancel();
417        let res = my_custom_file_enumerator.close(Some(&cancellable));
418        assert!(res.1.is_some());
419        let err = res.1.unwrap();
420
421        // invoke `MyFileEnumerator` implementation of `gio::ffi::GFileEnumeratorClass::close` with cancel
422        let cancellable = Cancellable::new();
423        cancellable.cancel();
424        let res = my_file_enumerator.close(Some(&cancellable));
425        assert!(res.1.is_some());
426        let expected = res.1.unwrap();
427
428        // both errors should equal
429        assert_eq!(err.domain(), expected.domain());
430        assert!(err.matches::<IOErrorEnum>(IOErrorEnum::Cancelled));
431        assert!(expected.matches::<IOErrorEnum>(IOErrorEnum::Cancelled));
432        assert_eq!(err.message(), expected.message());
433    }
434}