1use std::{ops::Deref, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4use zbus::zvariant::Type;
5
6#[derive(Debug, Serialize, Type, PartialEq, Eq, Hash, Clone)]
10pub struct AppID(String);
11
12impl AppID {
13 #[cfg(all(
14 feature = "backend",
15 any(feature = "gtk4_x11", feature = "gtk4_wayland")
16 ))]
17 #[cfg_attr(
18 docsrs,
19 doc(cfg(all(
20 feature = "backend",
21 any(feature = "gtk4_x11", feature = "gtk4_wayland")
22 )))
23 )]
24 pub fn app_info(&self) -> Option<gtk4::gio::DesktopAppInfo> {
26 let desktop_file = format!("{}.desktop", self.0);
27 gtk4::gio::DesktopAppInfo::new(&desktop_file)
28 }
29}
30
31impl FromStr for AppID {
32 type Err = crate::Error;
33 fn from_str(value: &str) -> Result<Self, Self::Err> {
34 if is_valid_app_id(value) {
35 Ok(Self(value.to_owned()))
36 } else {
37 Err(Self::Err::InvalidAppID)
38 }
39 }
40}
41
42impl TryFrom<String> for AppID {
43 type Error = crate::Error;
44
45 fn try_from(value: String) -> Result<Self, Self::Error> {
46 value.parse::<Self>()
47 }
48}
49
50impl TryFrom<&str> for AppID {
51 type Error = crate::Error;
52
53 fn try_from(value: &str) -> Result<Self, Self::Error> {
54 value.parse::<Self>()
55 }
56}
57
58impl From<AppID> for String {
59 fn from(value: AppID) -> String {
60 value.0
61 }
62}
63
64impl AsRef<str> for AppID {
65 fn as_ref(&self) -> &str {
66 self.0.as_ref()
67 }
68}
69
70impl Deref for AppID {
71 type Target = str;
72
73 fn deref(&self) -> &Self::Target {
74 &self.0
75 }
76}
77
78impl std::fmt::Display for AppID {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.write_str(self.as_ref())
81 }
82}
83
84impl<'de> Deserialize<'de> for AppID {
85 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
86 where
87 D: serde::Deserializer<'de>,
88 {
89 let app_id = String::deserialize(deserializer)?;
90 app_id
91 .parse::<Self>()
92 .map_err(|err| serde::de::Error::custom(err.to_string()))
93 }
94}
95
96#[derive(Debug, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone)]
98pub struct DocumentID(String);
99
100impl From<&str> for DocumentID {
101 fn from(value: &str) -> Self {
102 Self(value.to_owned())
103 }
104}
105
106impl From<String> for DocumentID {
107 fn from(value: String) -> Self {
108 Self(value)
109 }
110}
111
112impl From<DocumentID> for String {
113 fn from(value: DocumentID) -> String {
114 value.0
115 }
116}
117
118impl AsRef<str> for DocumentID {
119 fn as_ref(&self) -> &str {
120 self.0.as_ref()
121 }
122}
123
124impl Deref for DocumentID {
125 type Target = str;
126
127 fn deref(&self) -> &Self::Target {
128 &self.0
129 }
130}
131
132impl std::fmt::Display for DocumentID {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 f.write_str(self.as_ref())
135 }
136}
137
138fn is_valid_app_id(string: &str) -> bool {
141 let len = string.len();
142
143 if len == 0 || 255 < len {
145 return false;
146 }
147
148 let elements: Vec<&str> = string.split('.').collect();
149 let segments = elements.len();
150
151 if segments < 2 {
152 return false;
153 }
154
155 for (idx_segment, element) in elements.iter().enumerate() {
156 if element.is_empty() {
158 return false;
159 }
160
161 for (idx_char, c) in element.chars().enumerate() {
162 if idx_char == 0 && c.is_ascii_digit() {
164 return false;
165 }
166 if !is_valid_app_id_char(c) {
167 return false;
168 }
169 if idx_segment < segments - 1 && c == '-' {
171 return false;
172 }
173 }
174 }
175
176 true
177}
178
179fn is_valid_app_id_char(c: char) -> bool {
181 c.is_ascii_alphanumeric() || matches!(c, '-' | '_')
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_is_valid_app_id() {
190 assert!(is_valid_app_id("a.b"));
191 assert!(is_valid_app_id("a_c.b_c.h_c"));
192 assert!(is_valid_app_id("a.c-b"));
193 assert!(is_valid_app_id("a.c2.d"));
194
195 assert!(!is_valid_app_id("a"));
196 assert!(!is_valid_app_id(""));
197 assert!(!is_valid_app_id("a-z.b.c.d"));
198 assert!(!is_valid_app_id("a.b-z.c.d"));
199 assert!(!is_valid_app_id("a.b.c-z.d"));
200 assert!(!is_valid_app_id("a.0b.c"));
201 assert!(!is_valid_app_id("a..c"));
202 assert!(!is_valid_app_id("a.é"));
203 assert!(!is_valid_app_id("a.京"));
204
205 assert!(is_valid_app_id("_org.SomeApp"));
209 assert!(is_valid_app_id("com.org.SomeApp"));
210 assert!(is_valid_app_id("com.org_._SomeApp"));
211 assert!(is_valid_app_id("com.org._1SomeApp"));
212 assert!(is_valid_app_id("com.org._1_SomeApp"));
213 assert!(is_valid_app_id("VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.a111111111111"));
214
215 assert!(!is_valid_app_id("com.org-._SomeApp"));
216 assert!(!is_valid_app_id("package"));
217 assert!(!is_valid_app_id("NoDot"));
218 assert!(!is_valid_app_id("No-dot"));
219 assert!(!is_valid_app_id("No_dot"));
220 assert!(!is_valid_app_id("Has.Two..Consecutive.Dots"));
221 assert!(!is_valid_app_id("HasThree...Consecutive.Dots"));
222 assert!(!is_valid_app_id(".StartsWith.A.Period"));
223 assert!(!is_valid_app_id("."));
224 assert!(!is_valid_app_id("Ends.With.A.Period."));
225 assert!(!is_valid_app_id("0P.Starts.With.A.Digit"));
226 assert!(!is_valid_app_id("com.org.1SomeApp"));
227 assert!(!is_valid_app_id("Element.Starts.With.A.1Digit"));
228 assert!(!is_valid_app_id("VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.VeryLongApplicationId.a1111111111112"));
229 assert!(!is_valid_app_id(""));
230 assert!(!is_valid_app_id("contains.;nvalid.characters"));
231 assert!(!is_valid_app_id("con\nins.invalid.characters"));
232 assert!(!is_valid_app_id("con/ains.invalid.characters"));
233 assert!(!is_valid_app_id("conta|ns.invalid.characters"));
234 assert!(!is_valid_app_id("contæins.inva_å_lid.characters"));
235 }
236}