sharepoint_cli/graph/
search.rs1use base64::Engine as _;
10use base64::engine::general_purpose::URL_SAFE_NO_PAD;
11
12use super::drives::DriveItem;
13use super::{GraphClient, PagedResponse};
14use crate::error::{CliError, Result};
15
16pub struct SearchResult {
17 pub items: Vec<DriveItem>,
18 pub next: Option<String>,
19}
20
21pub async fn search(
22 graph: &GraphClient,
23 drive_id: &str,
24 query: &str,
25 page_token: Option<&str>,
26) -> Result<SearchResult> {
27 let api = match page_token {
28 Some(t) => decode_page_token(t)?,
29 None => {
30 let escaped = query.replace('\'', "''");
32 format!("/drives/{drive_id}/root/search(q='{escaped}')")
33 }
34 };
35 let page: PagedResponse<DriveItem> = graph.get_json(&api).await?;
36 Ok(SearchResult {
37 items: page.value,
38 next: page.next_link.as_deref().map(encode_page_token),
39 })
40}
41
42pub fn glob_matches(pattern: &str, name: &str) -> bool {
45 let p = pattern.to_ascii_lowercase();
46 let n = name.to_ascii_lowercase();
47 glob_inner(p.as_bytes(), n.as_bytes())
48}
49
50fn glob_inner(pat: &[u8], s: &[u8]) -> bool {
51 let m = pat.len();
53 let n = s.len();
54 let mut dp = vec![vec![false; n + 1]; m + 1];
55 dp[0][0] = true;
56 for i in 1..=m {
57 if pat[i - 1] == b'*' {
58 dp[i][0] = dp[i - 1][0];
59 }
60 }
61 for i in 1..=m {
62 for j in 1..=n {
63 if pat[i - 1] == b'*' {
64 dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
65 } else if pat[i - 1] == b'?' || pat[i - 1] == s[j - 1] {
66 dp[i][j] = dp[i - 1][j - 1];
67 }
68 }
69 }
70 dp[m][n]
71}
72
73fn encode_page_token(next_link: &str) -> String {
74 URL_SAFE_NO_PAD.encode(next_link.as_bytes())
75}
76
77fn decode_page_token(token: &str) -> Result<String> {
78 let bytes = URL_SAFE_NO_PAD
79 .decode(token.as_bytes())
80 .map_err(|e| CliError::Input(format!("invalid --page token: {e}")))?;
81 String::from_utf8(bytes).map_err(|e| CliError::Input(format!("invalid --page token: {e}")))
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn glob_basic_matches() {
90 assert!(glob_matches("*.pptx", "Q4-plan.pptx"));
91 assert!(glob_matches("Q?-*.xlsx", "Q4-summary.xlsx"));
92 assert!(!glob_matches("*.pdf", "report.docx"));
93 }
94
95 #[test]
96 fn glob_is_case_insensitive() {
97 assert!(glob_matches("*.PPTX", "plan.pptx"));
98 assert!(glob_matches("Plan.*", "PLAN.pptx"));
99 }
100
101 #[test]
102 fn glob_handles_empty_pattern() {
103 assert!(glob_matches("", ""));
104 assert!(!glob_matches("", "x"));
105 assert!(glob_matches("*", "anything"));
106 }
107}