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