mdquery_rs/apple/
query.rs

1use std::ptr::{self, NonNull};
2use super::{api::*, MDItemKey};
3use super::{MDItem, MDQueryBuilder, MDQueryScope};
4use anyhow::{anyhow, Result};
5use objc2_core_foundation::{CFArrayCreate, CFIndex, CFRetained, CFString};
6
7/// A wrapper around macOS Spotlight search query API.
8/// Provides functionality to create and execute metadata queries.
9pub struct MDQuery(CFRetained<CoreMDQuery>);
10
11impl MDQuery {
12    /// Creates a new query builder with default settings.
13    ///
14    /// # Returns
15    /// A new `MDQueryBuilder` instance for configuring the query.
16    pub fn builder() -> MDQueryBuilder {
17        MDQueryBuilder::default()
18    }
19
20    /// Creates a new MDQuery with the given query string and optional parameters.
21    ///
22    /// # Parameters
23    /// * `query` - A Spotlight query string
24    /// * `scopes` - Optional vector of search scopes to limit the query
25    /// * `max_count` - Optional maximum number of results to return
26    ///
27    /// # Returns
28    /// A Result containing the MDQuery on success, or an error if query creation fails.
29    pub fn new(
30        query: &str,
31        scopes: Option<Vec<MDQueryScope>>,
32        max_count: Option<usize>,
33    ) -> Result<Self> {
34        let query = CFString::from_str(query);
35
36        let md_query = unsafe {
37            MDQueryCreate(
38                None, // kCFAllocatorDefault
39                &query, None, None,
40            )
41        }
42        .ok_or(anyhow!("MDQuery create failed, check query syntax."))?;
43
44        if let Some(scopes) = scopes {
45            let scopes = scopes
46                .into_iter()
47                .map(|scope| scope.into_scope_string())
48                .map(|scope| CFString::from_str(&scope))
49                .collect::<Vec<_>>();
50
51            let scopes = unsafe {
52                CFArrayCreate(
53                    None,
54                    scopes.as_ptr() as *mut _,
55                    scopes.len() as CFIndex,
56                    ptr::null(),
57                )
58            }
59            .ok_or(anyhow!("MDQuery create failed when create scope array."))?;
60
61            unsafe {
62                MDQuerySetSearchScope(&md_query, &scopes, 0);
63            }
64        }
65
66        if let Some(max_count) = max_count {
67            unsafe {
68                MDQuerySetMaxCount(&md_query, max_count as CFIndex);
69            }
70        }
71
72        Ok(Self(md_query))
73    }
74
75    /// Executes the query and collects the results.
76    ///
77    /// # Returns
78    /// A Result containing a vector of MDItem objects on success, or an error if execution fails.
79    pub fn execute(self) -> Result<Vec<MDItem>> {
80        unsafe {
81            let success = MDQueryExecute(&self.0, MDQueryOptionsFlags::SYNCHRONOUS as _);
82
83            if !success {
84                return Err(anyhow!("MDQuery execute failed."));
85            }
86
87            let count = MDQueryGetResultCount(&self.0);
88            let mut items = Vec::with_capacity(count as usize);
89            for i in 0..count {
90                let item_ptr = MDQueryGetResultAtIndex(&self.0, i as _) as *mut CoreMDItem;
91                if let Some(item) = NonNull::new(item_ptr) {
92                    if let Some(value) = MDItemCopyAttribute(
93                        item.as_ref(),
94                        &CFString::from_str(MDItemKey::Path.as_str()),
95                    ) {
96                        if let Ok(path_str) = value.downcast::<CFString>() {
97                            let path = (*path_str).to_string();
98                            if let Ok(item) = MDItem::from_path(&path) {
99                                items.push(item);
100                            }
101                        }
102                    }
103                }
104            }
105            Ok(items)
106        }
107    }
108}
109
110// https://developer.apple.com/documentation/coreservices/mdqueryoptionflags?language=objc
111#[repr(C)]
112struct MDQueryOptionsFlags(u32);
113
114#[allow(unused)]
115impl MDQueryOptionsFlags {
116    const NONE: u32 = 0;
117    const SYNCHRONOUS: u32 = 1;
118    const WANTS_UPDATES: u32 = 4;
119    const ALLOW_FS_TRANSLATIONS: u32 = 8;
120}
121
122#[cfg(test)]
123mod tests {
124    use std::path::PathBuf;
125
126    use super::*;
127
128    #[test]
129    fn test_md_query_execute() {
130        let query = MDQuery::new(
131            "kMDItemFSName = \"Safari.app\"",
132            Some(vec![MDQueryScope::Custom("/Applications".into())]),
133            Some(5),
134        )
135        .unwrap();
136
137        let items = query.execute().unwrap();
138        assert_eq!(items.len(), 1);
139        assert_eq!(
140            items[0].path().unwrap(),
141            PathBuf::from("/Applications/Safari.app")
142        );
143    }
144
145    #[test]
146    fn test_empty_result() {
147        let query = MDQuery::new(
148            "kMDItemFSName = \"ThisFileDoesNotExist123456789.xyz\"",
149            Some(vec![MDQueryScope::Computer]),
150            None,
151        )
152        .unwrap();
153        let items = query.execute().unwrap();
154        assert_eq!(items.len(), 0);
155    }
156
157    #[test]
158    fn test_invalid_query() {
159        let result = MDQuery::new(
160            "invalid query syntax !!!",
161            Some(vec![MDQueryScope::Computer]),
162            None,
163        );
164        assert!(result.is_err());
165    }
166}