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