freedesktop_desktop_entry/
decoder.rs

1// Copyright 2021 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use std::{
5    fs::{self},
6    mem,
7    path::{Path, PathBuf},
8};
9
10use crate::{DesktopEntry, Group};
11use crate::{Groups, LocaleMap};
12use thiserror::Error;
13
14#[derive(Debug, Error)]
15pub enum DecodeError {
16    #[error("path does not contain a valid app ID")]
17    AppID,
18    #[error(transparent)]
19    Io(#[from] std::io::Error),
20    #[error("MultipleGroupWithSameName")]
21    MultipleGroupWithSameName,
22    #[error("KeyValueWithoutAGroup")]
23    KeyValueWithoutAGroup,
24    #[error("InvalidKey. Accepted: A-Za-z0-9")]
25    InvalidKey,
26    #[error("KeyDoesNotExist, this can happens when a localized key has no default value")]
27    KeyDoesNotExist,
28    #[error("InvalidValue")]
29    InvalidValue,
30}
31
32impl DesktopEntry {
33    pub fn from_str<L>(
34        path: impl Into<PathBuf>,
35        input: &str,
36        locales_filter: Option<&[L]>,
37    ) -> Result<DesktopEntry, DecodeError>
38    where
39        L: AsRef<str>,
40    {
41        #[inline(never)]
42        fn inner<'a>(
43            path: PathBuf,
44            input: &'a str,
45            locales_filter: Option<Vec<&str>>,
46        ) -> Result<DesktopEntry, DecodeError> {
47            let path: PathBuf = path.into();
48
49            let appid = get_app_id(&path)?;
50
51            let mut groups = Groups::default();
52            let mut active_group: Option<ActiveGroup> = None;
53            let mut active_keys: Option<ActiveKeys> = None;
54            let mut ubuntu_gettext_domain = None;
55
56            let mut missing_keys = Vec::new();
57            for line in input.lines() {
58                process_line(
59                    line,
60                    &mut groups,
61                    &mut active_group,
62                    &mut active_keys,
63                    &mut ubuntu_gettext_domain,
64                    locales_filter.as_deref(),
65                    &mut missing_keys,
66                )?;
67            }
68            if missing_keys.len() > 0 {
69                let mut check_again = mem::take(&mut missing_keys);
70                for missing in check_again.drain(..) {
71                    process_line(
72                        missing,
73                        &mut groups,
74                        &mut active_group,
75                        &mut active_keys,
76                        &mut ubuntu_gettext_domain,
77                        locales_filter.as_deref(),
78                        &mut missing_keys,
79                    )?;
80                }
81                if missing_keys.len() > 0 {
82                    return Err(DecodeError::KeyDoesNotExist);
83                }
84            }
85
86            if let Some(active_keys) = active_keys.take() {
87                match &mut active_group {
88                    Some(active_group) => {
89                        active_group.group.0.insert(
90                            active_keys.key_name,
91                            (active_keys.default_value, active_keys.locales),
92                        );
93                    }
94                    None => return Err(DecodeError::KeyValueWithoutAGroup),
95                }
96            }
97
98            if let Some(mut group) = active_group.take() {
99                groups
100                    .0
101                    .entry(group.group_name)
102                    .or_insert_with(|| Group::default())
103                    .0
104                    .append(&mut group.group.0);
105            }
106
107            Ok(DesktopEntry {
108                appid: appid.to_string(),
109                groups,
110                path,
111                ubuntu_gettext_domain,
112            })
113        }
114
115        inner(path.into(), input, locales_filter.map(add_generic_locales))
116    }
117
118    /// Return an owned [`DesktopEntry`]
119    #[inline]
120    pub fn from_path<L>(
121        path: impl Into<PathBuf>,
122        locales_filter: Option<&[L]>,
123    ) -> Result<DesktopEntry, DecodeError>
124    where
125        L: AsRef<str>,
126    {
127        let path: PathBuf = path.into();
128        let input = fs::read_to_string(&path)?;
129        Self::from_str(path, &input, locales_filter)
130    }
131}
132
133#[inline]
134fn get_app_id<P: AsRef<Path> + ?Sized>(path: &P) -> Result<&str, DecodeError> {
135    let appid = path
136        .as_ref()
137        .file_stem()
138        .ok_or(DecodeError::AppID)?
139        .to_str()
140        .ok_or(DecodeError::AppID)?;
141    Ok(appid)
142}
143
144#[derive(Debug)]
145struct ActiveGroup {
146    group_name: String,
147    group: Group,
148}
149
150#[derive(Debug)]
151struct ActiveKeys {
152    key_name: String,
153    default_value: String,
154    locales: LocaleMap,
155}
156
157#[inline(never)]
158fn process_line<'a>(
159    line: &'a str,
160    groups: &mut Groups,
161    active_group: &mut Option<ActiveGroup>,
162    active_keys: &mut Option<ActiveKeys>,
163    ubuntu_gettext_domain: &mut Option<String>,
164    locales_filter: Option<&[&str]>,
165    missing_keys: &mut Vec<&'a str>,
166) -> Result<(), DecodeError> {
167    if line.trim().is_empty() || line.starts_with('#') {
168        return Ok(());
169    }
170
171    let line_bytes = line.as_bytes();
172
173    if line_bytes[0] == b'[' {
174        if missing_keys.len() > 0 {
175            let mut check_again = mem::take(missing_keys);
176            for missing in check_again.drain(..) {
177                process_line(
178                    missing,
179                    groups,
180                    active_group,
181                    active_keys,
182                    ubuntu_gettext_domain,
183                    locales_filter,
184                    missing_keys,
185                )?;
186            }
187            if missing_keys.len() > 0 {
188                return Err(DecodeError::KeyDoesNotExist);
189            }
190        }
191        if let Some(end) = memchr::memrchr(b']', &line_bytes[1..]) {
192            let group_name = &line[1..end + 1];
193
194            if let Some(active_keys) = active_keys.take() {
195                match active_group {
196                    Some(active_group) => {
197                        active_group.group.0.insert(
198                            active_keys.key_name,
199                            (active_keys.default_value, active_keys.locales),
200                        );
201                    }
202                    None => return Err(DecodeError::KeyValueWithoutAGroup),
203                }
204            }
205
206            if let Some(mut group) = active_group.take() {
207                groups
208                    .0
209                    .entry(group.group_name)
210                    .or_insert_with(|| Group::default())
211                    .0
212                    .append(&mut group.group.0);
213            }
214
215            active_group.replace(ActiveGroup {
216                group_name: group_name.to_string(),
217                group: Group::default(),
218            });
219        }
220    } else if let Some(delimiter) = memchr::memchr(b'=', line_bytes) {
221        let key = &line[..delimiter];
222        let value = format_value(&line[delimiter + 1..])?;
223
224        if key.is_empty() {
225            return Err(DecodeError::InvalidKey);
226        }
227
228        // if locale
229        if key.as_bytes()[key.len() - 1] == b']' {
230            if let Some(start) = memchr::memchr(b'[', key.as_bytes()) {
231                // verify that the name is the same of active key ?
232                // or we can assume this is the case
233                // let local_key = &key[..start];
234
235                let locale = &key[start + 1..key.len() - 1];
236
237                match locales_filter {
238                    Some(locales_filter) if !locales_filter.iter().any(|l| *l == locale) => {
239                        return Ok(())
240                    }
241                    _ => (),
242                }
243
244                match active_keys {
245                    Some(active_keys) => {
246                        active_keys.locales.insert(locale.to_string(), value);
247                    }
248                    None => {
249                        missing_keys.push(line);
250                    }
251                }
252
253                return Ok(());
254            }
255        }
256
257        if key == "X-Ubuntu-Gettext-Domain" {
258            *ubuntu_gettext_domain = Some(value.to_string());
259            return Ok(());
260        }
261
262        if let Some(active_keys) = active_keys.take() {
263            match active_group {
264                Some(active_group) => {
265                    active_group.group.0.insert(
266                        active_keys.key_name,
267                        (active_keys.default_value, active_keys.locales),
268                    );
269                }
270                None => return Err(DecodeError::KeyValueWithoutAGroup),
271            }
272        }
273        active_keys.replace(ActiveKeys {
274            // todo: verify that the key only contains A-Za-z0-9 ?
275            key_name: key.trim().to_string(),
276            default_value: value,
277            locales: LocaleMap::default(),
278        });
279    }
280    Ok(())
281}
282
283// https://specifications.freedesktop.org/desktop-entry-spec/latest/value-types.html
284#[inline]
285fn format_value(input: &str) -> Result<String, DecodeError> {
286    let input = if let Some(input) = input.strip_prefix(" ") {
287        input
288    } else {
289        input
290    };
291
292    let mut res = String::with_capacity(input.len());
293
294    let mut last: usize = 0;
295
296    for i in memchr::memchr_iter(b'\\', input.as_bytes()) {
297        // edge case for //
298        if last > i {
299            continue;
300        }
301
302        // when there is an \ at the end
303        if input.len() <= i + 1 {
304            return Err(DecodeError::InvalidValue);
305        }
306
307        if last < i {
308            res.push_str(&input[last..i]);
309        }
310
311        last = i + 2;
312
313        match input.as_bytes()[i + 1] {
314            b's' => res.push(' '),
315            b'n' => res.push('\n'),
316            b't' => res.push('\t'),
317            b'r' => res.push('\r'),
318            b'\\' => res.push('\\'),
319            _ => {
320                return Err(DecodeError::InvalidValue);
321            }
322        }
323    }
324
325    if last < input.len() {
326        res.push_str(&input[last..input.len()]);
327    }
328
329    Ok(res)
330}
331
332/// Ex: if a locale equal fr_FR, add fr
333#[inline]
334fn add_generic_locales<L: AsRef<str>>(locales: &[L]) -> Vec<&str> {
335    let mut v = Vec::with_capacity(locales.len() + 1);
336
337    for l in locales {
338        let l = l.as_ref();
339
340        v.push(l);
341
342        if let Some(start) = memchr::memchr(b'_', l.as_bytes()) {
343            v.push(l.split_at(start).0)
344        }
345    }
346
347    v
348}