rightkit_tauri_shell/
deep_link.rs1use std::collections::{BTreeMap, VecDeque};
12use std::sync::Mutex;
13
14use serde::{Deserialize, Serialize};
15
16const MAX_LEN: usize = 4096;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct DeepLink {
21 pub scheme: String,
22 pub host: String,
23 pub segments: Vec<String>,
24 pub query: BTreeMap<String, String>,
25 pub raw: String,
26}
27
28fn percent_decode(raw: &str) -> Option<String> {
29 let bytes = raw.as_bytes();
30 let mut out = Vec::with_capacity(bytes.len());
31 let mut i = 0;
32 while i < bytes.len() {
33 if bytes[i] == b'%' {
34 if i + 2 >= bytes.len() {
35 return None;
36 }
37 let hi = (bytes[i + 1] as char).to_digit(16)?;
38 let lo = (bytes[i + 2] as char).to_digit(16)?;
39 out.push(((hi << 4) | lo) as u8);
40 i += 3;
41 } else {
42 out.push(bytes[i]);
43 i += 1;
44 }
45 }
46 let s = String::from_utf8(out).ok()?;
47 (!s.chars().any(char::is_control)).then_some(s)
48}
49
50pub fn parse(raw: &str, schemes: &[String]) -> Option<DeepLink> {
53 let raw = raw.trim();
54 if raw.len() > MAX_LEN || raw.chars().any(char::is_control) {
55 return None;
56 }
57 let (scheme, rest) = raw.split_once("://")?;
58 let scheme = scheme.to_ascii_lowercase();
59 if !schemes.iter().any(|s| s.eq_ignore_ascii_case(&scheme)) {
60 return None;
61 }
62 let rest = rest.split('#').next()?;
63 let (path_part, query_part) = match rest.split_once('?') {
64 Some((p, q)) => (p, Some(q)),
65 None => (rest, None),
66 };
67 let mut parts = path_part.split('/');
68 let host = percent_decode(parts.next()?)?;
69 if host.is_empty() {
70 return None;
71 }
72 let mut segments = Vec::new();
73 for seg in parts.filter(|s| !s.is_empty()) {
74 let seg = percent_decode(seg)?;
75 if seg == "." || seg == ".." || seg.contains('/') || seg.contains('\\') {
76 return None;
77 }
78 segments.push(seg);
79 }
80 let mut query = BTreeMap::new();
81 if let Some(q) = query_part {
82 for pair in q.split('&').filter(|p| !p.is_empty()) {
83 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
84 query.insert(
85 percent_decode(&k.replace('+', " "))?,
86 percent_decode(&v.replace('+', " "))?,
87 );
88 }
89 }
90 Some(DeepLink {
91 scheme,
92 host,
93 segments,
94 query,
95 raw: raw.to_owned(),
96 })
97}
98
99#[derive(Default)]
101pub struct DeepLinkQueue {
102 pending: Mutex<VecDeque<DeepLink>>,
103}
104
105impl DeepLinkQueue {
106 pub fn push_all<'a>(
107 &self,
108 raw: impl IntoIterator<Item = &'a str>,
109 schemes: &[String],
110 ) -> usize {
111 let mut q = self.pending.lock().unwrap_or_else(|e| e.into_inner());
112 for r in raw {
113 if let Some(link) = parse(r, schemes) {
114 q.push_back(link);
115 }
116 }
117 q.len()
118 }
119 pub fn drain(&self) -> Vec<DeepLink> {
120 self.pending
121 .lock()
122 .unwrap_or_else(|e| e.into_inner())
123 .drain(..)
124 .collect()
125 }
126}
127
128pub fn register_scheme(scheme: &str, executable: &std::path::Path) -> Result<bool, String> {
132 #[cfg(windows)]
133 {
134 use std::os::windows::process::CommandExt;
135 let base = format!(r"HKCU\Software\Classes\{scheme}");
136 let run = |args: &[&str]| -> Result<(), String> {
137 let out = std::process::Command::new("reg.exe")
138 .creation_flags(0x0800_0000)
139 .args(args)
140 .output()
141 .map_err(|e| e.to_string())?;
142 if out.status.success() {
143 Ok(())
144 } else {
145 Err(String::from_utf8_lossy(&out.stderr).trim().to_owned())
146 }
147 };
148 run(&["ADD", &base, "/ve", "/d", &format!("URL:{scheme}"), "/f"])?;
149 run(&["ADD", &base, "/v", "URL Protocol", "/d", "", "/f"])?;
150 run(&[
151 "ADD",
152 &format!(r"{base}\shell\open\command"),
153 "/ve",
154 "/d",
155 &format!("\"{}\" \"%1\"", executable.display()),
156 "/f",
157 ])?;
158 Ok(true)
159 }
160 #[cfg(not(windows))]
161 {
162 let _ = (scheme, executable);
163 Ok(false)
164 }
165}