ashpd/
app_id.rs

1use std::{ops::Deref, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4use zbus::zvariant::Type;
5
6/// The application ID.
7///
8/// See <https://developer.gnome.org/documentation/tutorials/application-id.html>.
9#[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    /// Retrieves the associated `gio::DesktopAppInfo` if found
25    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/// The ID of a file in the document store.
97#[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
138// Helpers
139
140fn is_valid_app_id(string: &str) -> bool {
141    let len = string.len();
142
143    // The app id has to be between 0 < len <= 255
144    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        // No empty segments.
157        if element.is_empty() {
158            return false;
159        }
160
161        for (idx_char, c) in element.chars().enumerate() {
162            // First char cannot be a digit.
163            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            // Only the last segment can contain `-`.
170            if idx_segment < segments - 1 && c == '-' {
171                return false;
172            }
173        }
174    }
175
176    true
177}
178
179/// Only valid chars are a-z A-Z 0-9 - _
180fn 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        // Tests from
206        // https://github.com/bilelmoussaoui/flatpak-vscode/blob/master/src/test/suite/extension.test.ts
207
208        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}