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    path::{Path, PathBuf},
7};
8
9use crate::{DesktopEntry, Group};
10use crate::{Groups, LocaleMap};
11use bstr::ByteSlice;
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 happen when a localized key has no default value")]
27    KeyDoesNotExist,
28    #[error("InvalidValue")]
29    InvalidValue,
30    #[error("InvalidGroup")]
31    InvalidGroup,
32    #[error("InvalidEntry")]
33    InvalidEntry,
34}
35
36#[derive(Debug, Eq, PartialEq)]
37pub enum Line<'a> {
38    Group(&'a str),
39    Entry(&'a str, &'a str),
40    Comment(&'a str),
41}
42
43#[inline]
44pub fn parse_line<'a>(line: &'a str) -> Result<Line<'a>, DecodeError> {
45    if line.trim().is_empty() || line.starts_with('#') {
46        return Ok(Line::Comment(&line));
47    }
48
49    let line_bytes = line.as_bytes();
50
51    // if group
52    if line_bytes[0] == b'[' {
53        if let Some(end) = memchr::memrchr(b']', &line_bytes[1..]) {
54            let group_name = &line[1..end + 1];
55            return Ok(Line::Group(group_name));
56        } else {
57            return Err(DecodeError::InvalidGroup);
58        }
59    }
60    // else, if entry
61    else if let Some(delimiter) = memchr::memchr(b'=', line_bytes) {
62        let key = &line[..delimiter];
63        let value = &line[delimiter + 1..];
64
65        if key.is_empty() {
66            return Err(DecodeError::InvalidKey);
67        }
68        return Ok(Line::Entry(key, value));
69    } else {
70        return Err(DecodeError::InvalidEntry);
71    }
72}
73
74fn group_entry_from_str(
75    input: &str,
76    group: &str,
77    entry: &str,
78) -> Result<Option<String>, DecodeError> {
79    let mut in_group = false;
80
81    for line in input.lines() {
82        match parse_line(line)? {
83            Line::Group(parsed_group) => {
84                in_group = parsed_group == group;
85            }
86            Line::Entry(key, value) => {
87                if in_group && key == entry {
88                    return Ok(Some(format_value(value)?));
89                }
90            }
91            _ => (),
92        }
93    }
94
95    Ok(None)
96}
97
98/// Return a single entry from a specified group
99pub fn group_entry_from_path(
100    path: impl Into<PathBuf>,
101    group: &str,
102    entry: &str,
103) -> Result<Option<String>, DecodeError> {
104    let path: PathBuf = path.into();
105    let input = fs::read_to_string(&path)?;
106
107    group_entry_from_str(&input, group, entry)
108}
109
110/// Return a single desktop entry
111pub fn desktop_entry_from_path(
112    path: impl Into<PathBuf>,
113    entry: &str,
114) -> Result<Option<String>, DecodeError> {
115    let path: PathBuf = path.into();
116    let input = fs::read_to_string(&path)?;
117
118    group_entry_from_str(&input, "Desktop Entry", entry)
119}
120
121struct UnknownKey<'a> {
122    key: &'a str,
123    locale: String,
124    value: String,
125}
126
127impl DesktopEntry {
128    pub fn from_str<L>(
129        path: impl Into<PathBuf>,
130        input: &str,
131        locales_filter: Option<&[L]>,
132    ) -> Result<DesktopEntry, DecodeError>
133    where
134        L: AsRef<str>,
135    {
136        #[inline(never)]
137        fn inner<'a>(
138            path: PathBuf,
139            input: &'a str,
140            locales_filter: Option<Vec<&str>>,
141        ) -> Result<DesktopEntry, DecodeError> {
142            let path: PathBuf = path.into();
143
144            let appid = get_app_id(&path)?;
145
146            let mut groups = Groups::default();
147            let mut active_group: Option<ActiveGroup> = None;
148            let mut active_keys: Option<ActiveKeys> = None;
149            let mut ubuntu_gettext_domain = None;
150
151            let mut unknown_keys: Vec<UnknownKey> = Vec::new();
152
153            for line in input.lines() {
154                process_line(
155                    line,
156                    &mut groups,
157                    &mut active_group,
158                    &mut active_keys,
159                    &mut ubuntu_gettext_domain,
160                    locales_filter.as_deref(),
161                    &mut unknown_keys,
162                )?;
163            }
164            
165            if let Some(active_keys) = active_keys.take() {
166                match &mut active_group {
167                    Some(active_group) => {
168                        active_group.group.0.insert(
169                            active_keys.key_name,
170                            (active_keys.default_value, active_keys.locales),
171                        );
172                    }
173                    None => return Err(DecodeError::KeyValueWithoutAGroup),
174                }
175            }
176
177            // insert keys which have no group
178            for unknown_key in unknown_keys.drain(..) {
179                match &mut active_group {
180                    Some(active_group) => match active_group.group.0.get_mut(unknown_key.key) {
181                        Some((_, locale_map)) => {
182                            locale_map.insert(unknown_key.locale, unknown_key.value);
183                        }
184                        None => return Err(DecodeError::KeyDoesNotExist),
185                    },
186                    None => return Err(DecodeError::KeyDoesNotExist),
187                }
188            }
189
190            if let Some(mut group) = active_group.take() {
191                groups
192                    .0
193                    .entry(group.group_name)
194                    .or_insert_with(|| Group::default())
195                    .0
196                    .append(&mut group.group.0);
197            }
198
199            Ok(DesktopEntry {
200                appid,
201                groups,
202                path,
203                ubuntu_gettext_domain,
204            })
205        }
206
207        inner(path.into(), input, locales_filter.map(add_generic_locales))
208    }
209
210    /// Return an owned [`DesktopEntry`]
211    #[inline]
212    pub fn from_path<L>(
213        path: impl Into<PathBuf>,
214        locales_filter: Option<&[L]>,
215    ) -> Result<DesktopEntry, DecodeError>
216    where
217        L: AsRef<str>,
218    {
219        let path: PathBuf = path.into();
220        let input = fs::read_to_string(&path)?;
221        Self::from_str(path, &input, locales_filter)
222    }
223}
224
225#[inline]
226fn get_app_id<P: AsRef<Path> + ?Sized>(path: &P) -> Result<String, DecodeError> {
227    let path_as_bytes = path
228        .as_ref()
229        .as_os_str()
230        .as_encoded_bytes()
231        .strip_suffix(b".desktop")
232        .ok_or(DecodeError::AppID)?;
233
234    Ok(
235        if let Some((_prefix, entry)) = path_as_bytes.rsplit_once_str("/applications/") {
236            String::from_utf8(entry.replace(b"/", b"-"))
237                .ok()
238                .ok_or(DecodeError::AppID)?
239        } else {
240            path.as_ref()
241                .file_stem()
242                .ok_or(DecodeError::AppID)?
243                .to_str()
244                .ok_or(DecodeError::AppID)?
245                .to_owned()
246        },
247    )
248}
249
250#[derive(Debug)]
251struct ActiveGroup {
252    group_name: String,
253    group: Group,
254}
255
256#[derive(Debug)]
257struct ActiveKeys {
258    key_name: String,
259    default_value: String,
260    locales: LocaleMap,
261}
262
263#[inline(never)]
264fn process_line<'a>(
265    line: &'a str,
266    groups: &mut Groups,
267    active_group: &mut Option<ActiveGroup>,
268    active_keys: &mut Option<ActiveKeys>,
269    ubuntu_gettext_domain: &mut Option<String>,
270    locales_filter: Option<&[&str]>,
271    unknown_keys: &mut Vec<UnknownKey<'a>>,
272) -> Result<(), DecodeError> {
273    match parse_line(line)? {
274        Line::Group(group_name) => {
275            // insert keys which have no group
276            for unknown_key in unknown_keys.drain(..) {
277                match active_group {
278                    Some(active_group) => match active_group.group.0.get_mut(unknown_key.key) {
279                        Some((_, locale_map)) => {
280                            locale_map.insert(unknown_key.locale, unknown_key.value);
281                        }
282                        None => return Err(DecodeError::KeyDoesNotExist),
283                    },
284                    None => return Err(DecodeError::KeyDoesNotExist),
285                }
286            }
287
288            if let Some(active_keys) = active_keys.take() {
289                match active_group {
290                    Some(active_group) => {
291                        active_group.group.0.insert(
292                            active_keys.key_name,
293                            (active_keys.default_value, active_keys.locales),
294                        );
295                    }
296                    None => return Err(DecodeError::KeyValueWithoutAGroup),
297                }
298            }
299
300            if let Some(mut group) = active_group.take() {
301                groups
302                    .0
303                    .entry(group.group_name)
304                    .or_insert_with(|| Group::default())
305                    .0
306                    .append(&mut group.group.0);
307            }
308
309            active_group.replace(ActiveGroup {
310                group_name: group_name.to_string(),
311                group: Group::default(),
312            });
313        }
314        Line::Entry(key, value) => {
315            let value = format_value(value)?;
316
317            // if locale
318            if key.as_bytes()[key.len() - 1] == b']' {
319                if let Some(start) = memchr::memchr(b'[', key.as_bytes()) {
320                    let locale = &key[start + 1..key.len() - 1];
321
322                    let key = &key[..start];
323
324                    match locales_filter {
325                        Some(locales_filter) if !locales_filter.iter().any(|l| *l == locale) => {
326                            return Ok(());
327                        }
328                        _ => (),
329                    }
330
331                    // we verify that the name is the same of active key
332                    // even tho this is forbidden by the spec, nautilus does this for example
333                    if let Some(active_keys) = active_keys
334                        .as_mut()
335                        .filter(|active_keys| active_keys.key_name == key)
336                    {
337                        active_keys.locales.insert(locale.to_string(), value);
338                    } else {
339                        unknown_keys.push(UnknownKey {
340                            key,
341                            locale: locale.to_string(),
342                            value,
343                        });
344                    }
345
346                    return Ok(());
347                }
348            }
349
350            if key == "X-Ubuntu-Gettext-Domain" {
351                *ubuntu_gettext_domain = Some(value.to_string());
352                return Ok(());
353            }
354
355            if let Some(active_keys) = active_keys.take() {
356                match active_group {
357                    Some(active_group) => {
358                        active_group.group.0.insert(
359                            active_keys.key_name,
360                            (active_keys.default_value, active_keys.locales),
361                        );
362                    }
363                    None => return Err(DecodeError::KeyValueWithoutAGroup),
364                }
365            }
366            active_keys.replace(ActiveKeys {
367                // todo: verify that the key only contains A-Za-z0-9 ?
368                key_name: key.trim().to_string(),
369                default_value: value,
370                locales: LocaleMap::default(),
371            });
372        }
373        _ => (),
374    }
375    Ok(())
376}
377
378// https://specifications.freedesktop.org/desktop-entry-spec/latest/value-types.html
379#[inline]
380pub(crate) fn format_value(input: &str) -> Result<String, DecodeError> {
381    let input = if let Some(input) = input.strip_prefix(" ") {
382        input
383    } else {
384        input
385    };
386
387    let mut res = String::with_capacity(input.len());
388
389    let mut last: usize = 0;
390
391    for i in memchr::memchr_iter(b'\\', input.as_bytes()) {
392        // edge case for //
393        if last > i {
394            continue;
395        }
396
397        // when there is an \ at the end
398        if input.len() <= i + 1 {
399            return Err(DecodeError::InvalidValue);
400        }
401
402        if last < i {
403            res.push_str(&input[last..i]);
404        }
405
406        last = i + 2;
407
408        match input.as_bytes()[i + 1] {
409            b's' => res.push(' '),
410            b'n' => res.push('\n'),
411            b't' => res.push('\t'),
412            b'r' => res.push('\r'),
413            b'\\' => res.push('\\'),
414            _ => {
415                return Err(DecodeError::InvalidValue);
416            }
417        }
418    }
419
420    if last < input.len() {
421        res.push_str(&input[last..input.len()]);
422    }
423
424    Ok(res)
425}
426
427/// Ex: if a locale equal fr_FR, add fr
428#[inline]
429fn add_generic_locales<L: AsRef<str>>(locales: &[L]) -> Vec<&str> {
430    let mut v = Vec::with_capacity(locales.len() + 1);
431
432    for l in locales {
433        let l = l.as_ref();
434
435        v.push(l);
436
437        if let Some(start) = memchr::memchr(b'_', l.as_bytes()) {
438            v.push(l.split_at(start).0)
439        }
440    }
441
442    v
443}
444
445#[cfg(test)]
446mod test {
447    use crate::{decoder::Line, parse_line};
448
449    #[test]
450    fn test_parse_empty_comment() {
451        let line = parse_line("").unwrap();
452
453        assert_eq!(line, Line::Comment(""));
454    }
455
456    #[test]
457    fn test_parse_hash_comment() {
458        let line = parse_line("# comment").unwrap();
459
460        assert_eq!(line, Line::Comment("# comment"));
461    }
462
463    #[test]
464    fn test_parse_group() {
465        let line = parse_line("[Some Group]").unwrap();
466
467        assert_eq!(line, Line::Group("Some Group"));
468    }
469
470    #[test]
471    fn test_parse_entry() {
472        let line = parse_line("key=value").unwrap();
473
474        assert_eq!(line, Line::Entry("key", "value"));
475    }
476
477    #[test]
478    fn test_group_error() {
479        let result = parse_line("[Some Group");
480
481        assert!(result.is_err());
482    }
483
484    #[test]
485    fn test_entry_error() {
486        let result = parse_line("invalid entry");
487
488        assert!(result.is_err());
489    }
490}