freedesktop_desktop_entry/
decoder.rs1use 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 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 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
98pub 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
110pub 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 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 #[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 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 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 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 key_name: key.trim().to_string(),
369 default_value: value,
370 locales: LocaleMap::default(),
371 });
372 }
373 _ => (),
374 }
375 Ok(())
376}
377
378#[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 if last > i {
394 continue;
395 }
396
397 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#[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}