freedesktop_file_parser/
parser.rs

1use crate::internal_structs::vec_to_map;
2use std::{cell::RefCell, collections::HashMap, fmt, rc::Rc};
3
4use crate::{
5    internal_structs::{
6        DesktopActionInternal, DesktopEntryInternal, Header, LocaleStringInternal,
7        LocaleStringListInternal,
8    },
9    structs::ParseError,
10    DesktopFile, IconString,
11};
12
13#[derive(Debug)]
14enum LineType {
15    Header,
16    ValPair,
17}
18
19#[derive(Debug)]
20enum EntryType {
21    Entry(Rc<RefCell<DesktopEntryInternal>>),
22    Action(usize),
23}
24
25#[derive(Debug)]
26struct Line<'a> {
27    content: Vec<Character<'a>>,
28    line_number: usize,
29}
30
31impl<'a> Line<'a> {
32    pub fn from_data(line: &'a str, line_number: usize) -> Self {
33        let content: Vec<Character<'a>> = line
34            .trim_end()
35            .char_indices()
36            .map(|(col_number, ch)| Character {
37                content: &line[col_number..col_number + ch.len_utf8()],
38                line_number,
39                col_number,
40            })
41            .filter(|ch| !(ch.col_number == 0 && ch.content == " "))
42            .collect();
43
44        Self {
45            content,
46            line_number,
47        }
48    }
49
50    pub fn line_type(&self) -> LineType {
51        if self.content[0].content == "[" {
52            LineType::Header
53        } else {
54            LineType::ValPair
55        }
56    }
57}
58impl<'a> fmt::Display for Line<'a> {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        for ch in &self.content {
61            write!(f, "{}", ch.content)?;
62        }
63        Ok(())
64    }
65}
66
67#[derive(Debug, Clone)]
68struct Character<'a> {
69    content: &'a str,
70    line_number: usize,
71    col_number: usize,
72}
73
74fn filter_lines(input: &str) -> Vec<Line> {
75    input
76        .split("\n")
77        .enumerate()
78        .filter(|element| !element.1.is_empty() && !element.1.trim().starts_with("#"))
79        .map(|(num, l)| Line::from_data(l, num))
80        .collect()
81}
82
83fn parse_header(input: &Line) -> Result<Header, ParseError> {
84    enum HeaderParseState {
85        Idle,
86        Content,
87    }
88
89    let mut state = HeaderParseState::Idle;
90    let mut result = String::new();
91
92    for (ind, ch) in input.content.iter().enumerate() {
93        match state {
94            HeaderParseState::Idle => match ch.content {
95                "[" => {
96                    state = HeaderParseState::Content;
97                }
98                _ => {
99                    return Err(ParseError::InternalError {
100                        msg: "line is mis-classified as a header".into(),
101                        row: ch.line_number,
102                        col: ch.col_number,
103                    });
104                }
105            },
106            HeaderParseState::Content => match ch.content {
107                "]" => {
108                    if ind != input.content.len() - 1 {
109                        return Err(ParseError::Syntax {
110                            msg: "nothing is expected after \"]\"".to_string(),
111                            row: ch.line_number,
112                            col: ch.col_number,
113                        });
114                    }
115                }
116                "[" => {
117                    return Err(ParseError::UnacceptableCharacter {
118                        ch: ch.content.to_string(),
119                        row: ch.line_number,
120                        col: ch.col_number,
121                        msg: format!("\"{}\" is not accepted in header", ch.content),
122                    });
123                }
124                _ => {
125                    if ch.content.chars().next().unwrap().is_control() {
126                        return Err(ParseError::UnacceptableCharacter {
127                            ch: ch.content.to_string(),
128                            row: ch.line_number,
129                            col: ch.col_number,
130                            msg: "none".to_string(),
131                        });
132                    }
133                    result.push_str(ch.content);
134                }
135            },
136        }
137    }
138
139    if result == "Desktop Entry" {
140        Ok(Header::DesktopEntry)
141    } else if let Some(remain) = result.strip_prefix("Desktop Action ") {
142        Ok(Header::DesktopAction {
143            name: remain.to_string(),
144        })
145    } else {
146        Ok(Header::Other { name: result })
147    }
148}
149
150/// Contains the parsed info of a key value line
151#[derive(Clone)]
152struct LinePart {
153    key: String,
154    locale: Option<String>,
155    value: String,
156    line_number: usize,
157}
158
159fn split_into_parts(line: &Line) -> Result<LinePart, ParseError> {
160    #[cfg(test)]
161    println!("This line is: {:?}", line.to_string());
162
163    enum State {
164        /// the initial key parser
165        Key,
166        /// the locale parser
167        KeyLocale,
168        /// the character that ends the locale spec
169        LocaleToValue,
170        /// the value
171        Value,
172    }
173
174    let mut result = LinePart {
175        key: "".into(),
176        locale: None,
177        value: "".into(),
178        line_number: line.line_number,
179    };
180
181    let mut state = State::Key;
182    let mut key_has_space = false;
183
184    for ch in line.content.iter() {
185        match state {
186            State::Key => match ch.content {
187                "[" => {
188                    state = State::KeyLocale;
189                    result.locale = Some("".into())
190                }
191
192                "=" => state = State::Value,
193
194                " " => key_has_space = true,
195
196                "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M"
197                | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
198                | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m"
199                | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
200                | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "-" => {
201                    if !key_has_space {
202                        result.key.push_str(ch.content)
203                    } else {
204                        return Err(ParseError::Syntax {
205                            msg: "Keys shouldn't have characters other than A-Za-z0-9-".into(),
206                            row: ch.line_number,
207                            col: ch.col_number,
208                        });
209                    }
210                }
211
212                _ => {
213                    return Err(ParseError::Syntax {
214                        msg: "Keys shouldn't have characters other than A-Za-z0-9-".into(),
215                        row: ch.line_number,
216                        col: ch.col_number,
217                    })
218                }
219            },
220
221            State::KeyLocale => match ch.content {
222                "]" => state = State::LocaleToValue,
223
224                _ => {
225                    if let Some(ref mut str) = result.locale {
226                        str.push_str(ch.content);
227                    }
228                }
229            },
230
231            State::LocaleToValue => match ch.content {
232                "=" => state = State::Value,
233
234                _ => {
235                    return Err(ParseError::Syntax {
236                        msg: "Expect \"=\" after \"=\"".into(),
237                        row: ch.line_number,
238                        col: ch.col_number,
239                    });
240                }
241            },
242
243            State::Value => result.value.push_str(ch.content),
244        }
245    }
246
247    result.value = result.value.trim_start().to_string();
248    result.key = result.key.trim_end().to_string();
249
250    Ok(result)
251}
252
253fn set_locale_str(parts: LinePart, str: &mut LocaleStringInternal) -> Result<(), ParseError> {
254    // make sure that one property is only declared once
255
256    match parts.locale {
257        Some(locale) => {
258            if str.variants.contains_key(&locale) {
259                return Err(ParseError::RepetitiveKey {
260                    key: parts.key,
261                    row: parts.line_number,
262                    col: 0,
263                });
264            }
265            str.variants.insert(locale, parts.value);
266        }
267        None => {
268            if str.default.is_none() {
269                str.default = Some(parts.value);
270            } else {
271                return Err(ParseError::RepetitiveKey {
272                    key: parts.key,
273                    row: parts.line_number,
274                    col: 0,
275                });
276            }
277        }
278    }
279
280    Ok(())
281}
282
283fn set_optional_locale_str(
284    parts: LinePart,
285    opt: &mut Option<LocaleStringInternal>,
286) -> Result<(), ParseError> {
287    match opt {
288        Some(str) => set_locale_str(parts, str),
289
290        None => {
291            {
292                let mut inner = LocaleStringInternal::default();
293
294                set_locale_str(parts, &mut inner)?;
295
296                *opt = Some(inner);
297            };
298            Ok(())
299        }
300    }
301}
302
303fn set_bool(parts: LinePart, val: &mut bool) -> Result<(), ParseError> {
304    *val = parts
305        .value
306        .parse::<bool>()
307        .map_err(|_| ParseError::Syntax {
308            msg: "Property's value needs to be bool".into(),
309            row: parts.line_number,
310            col: 0,
311        })?;
312    Ok(())
313}
314
315fn set_optional_bool(parts: LinePart, opt: &mut Option<bool>) -> Result<(), ParseError> {
316    // check for redeclaration
317    match opt {
318        Some(_) => {
319            return Err(ParseError::RepetitiveKey {
320                key: parts.key,
321                row: parts.line_number,
322                col: 0,
323            });
324        }
325        None => {
326            let mut res = false;
327            set_bool(parts, &mut res)?;
328            *opt = Some(res);
329        }
330    }
331
332    Ok(())
333}
334
335fn set_optional_list(parts: LinePart, opt: &mut Option<Vec<String>>) -> Result<(), ParseError> {
336    if !opt.is_none() {
337        return Err(ParseError::RepetitiveKey {
338            key: parts.key,
339            row: parts.line_number,
340            col: 0,
341        });
342    }
343
344    *opt = Some({
345        let mut res = parts
346            .value
347            .split(";")
348            .map(|s| s.to_string())
349            .collect::<Vec<String>>();
350
351        if let Some(val) = res.last() {
352            if val.is_empty() {
353                res.pop();
354            }
355        }
356
357        res
358    });
359    Ok(())
360}
361
362fn set_optional_str(parts: LinePart, opt: &mut Option<String>) -> Result<(), ParseError> {
363    if !opt.is_none() {
364        return Err(ParseError::RepetitiveKey {
365            key: parts.key,
366            row: parts.line_number,
367            col: 0,
368        });
369    }
370
371    *opt = Some(parts.value);
372    Ok(())
373}
374
375fn set_optional_icon_str(parts: LinePart, opt: &mut Option<IconString>) -> Result<(), ParseError> {
376    if !opt.is_none() {
377        return Err(ParseError::RepetitiveKey {
378            key: parts.key,
379            row: parts.line_number,
380            col: 0,
381        });
382    }
383
384    *opt = Some(IconString {
385        content: parts.value,
386    });
387    Ok(())
388}
389
390fn fill_entry_val(entry: &mut DesktopEntryInternal, parts: LinePart) -> Result<(), ParseError> {
391    match parts.key.as_str() {
392        "Type" => {
393            if entry.entry_type.is_some() {
394                return Err(ParseError::RepetitiveKey {
395                    key: "Type".into(),
396                    row: parts.line_number,
397                    col: 0,
398                });
399            }
400
401            entry.entry_type = Some(crate::internal_structs::EntryTypeInternal::from(
402                parts.value.as_str(),
403            ));
404        }
405        "Version" => set_optional_str(parts, &mut entry.version)?,
406        "Name" => set_optional_locale_str(parts, &mut entry.name)?,
407        "GenericName" => set_optional_locale_str(parts, &mut entry.generic_name)?,
408        "NoDisplay" => set_optional_bool(parts, &mut entry.no_display)?,
409        "Comment" => set_optional_locale_str(parts, &mut entry.comment)?,
410        "Icon" => set_optional_icon_str(parts, &mut entry.icon)?,
411        "Hidden" => set_optional_bool(parts, &mut entry.hidden)?,
412        "OnlyShowIn" => set_optional_list(parts, &mut entry.only_show_in)?,
413        "NotShowIn" => set_optional_list(parts, &mut entry.not_show_in)?,
414        "DBusActivatable" => set_optional_bool(parts, &mut entry.dbus_activatable)?,
415        "TryExec" => set_optional_str(parts, &mut entry.try_exec)?,
416        "Exec" => set_optional_str(parts, &mut entry.exec)?,
417        "Path" => set_optional_str(parts, &mut entry.path)?,
418        "Terminal" => set_optional_bool(parts, &mut entry.terminal)?,
419        "Actions" => set_optional_list(parts, &mut entry.actions)?,
420        "MimeType" => set_optional_list(parts, &mut entry.mime_type)?,
421        "Categories" => set_optional_list(parts, &mut entry.categories)?,
422        "Implements" => set_optional_list(parts, &mut entry.implements)?,
423        "Keywords" => {
424            let mut split = parts
425                .value
426                .split(";")
427                .map(|str| str.to_string())
428                .collect::<Vec<String>>();
429
430            if let Some(val) = split.last() {
431                if val.is_empty() {
432                    split.pop();
433                }
434            }
435
436            match entry.keywords {
437                Some(ref mut kwds) => match parts.locale {
438                    Some(locale) => {
439                        if kwds.variants.contains_key(&locale) {
440                            return Err(ParseError::RepetitiveKey {
441                                key: "Keywords".into(),
442                                row: parts.line_number,
443                                col: 0,
444                            });
445                        }
446
447                        kwds.variants.insert(locale, split);
448                    }
449                    None => {
450                        if kwds.default.is_some() {
451                            return Err(ParseError::RepetitiveKey {
452                                key: "Keywords".into(),
453                                row: parts.line_number,
454                                col: 0,
455                            });
456                        }
457
458                        kwds.default = Some(split);
459                    }
460                },
461                None => {
462                    let mut res = LocaleStringListInternal::default();
463                    match parts.locale {
464                        Some(locale) => {
465                            res.variants.insert(locale, split);
466                        }
467                        None => {
468                            res.default = Some(split);
469                        }
470                    }
471
472                    entry.keywords = Some(res);
473                }
474            }
475        }
476        "StartupNotify" => set_optional_bool(parts, &mut entry.startup_notify)?,
477        "StartupWMClass" => set_optional_str(parts, &mut entry.startup_wm_class)?,
478        "URL" => set_optional_str(parts, &mut entry.url)?,
479        "PrefersNonDefaultGPU" => set_optional_bool(parts, &mut entry.prefers_non_default_gpu)?,
480        "SingleMainWindow" => set_optional_bool(parts, &mut entry.single_main_window)?,
481
482        _ => {}
483    }
484
485    Ok(())
486}
487
488fn process_entry_val_pair(line: &Line, entry: &mut DesktopEntryInternal) -> Result<(), ParseError> {
489    let parts = split_into_parts(line)?;
490
491    fill_entry_val(entry, parts)
492}
493
494fn fill_action_val(action: &mut DesktopActionInternal, parts: LinePart) -> Result<(), ParseError> {
495    match parts.key.as_str() {
496        "Name" => set_optional_locale_str(parts, &mut action.name)?,
497        "Exec" => set_optional_str(parts, &mut action.exec)?,
498        "Icon" => set_optional_icon_str(parts, &mut action.icon)?,
499        _ => {}
500    }
501
502    Ok(())
503}
504
505fn process_action_val_pair(
506    line: &Line,
507    action: &mut DesktopActionInternal,
508) -> Result<(), ParseError> {
509    let parts = split_into_parts(line)?;
510
511    fill_action_val(action, parts)
512}
513
514/// Parses a desktop file's content into a structured DesktopFile.
515///
516/// # Arguments
517/// * `input` - The string content of a .desktop file
518///
519/// # Returns
520/// * `Ok(DesktopFile)` - Successfully parsed desktop file with all entries and actions
521/// * `Err(ParseError)` - If the file cannot be parsed or is missing required fields
522///
523/// # Examples
524/// ```
525/// let content = r#"[Desktop Entry]
526/// Type=Application
527/// Name=Firefox
528/// Exec=firefox %u"#;
529///
530/// let desktop_file = freedesktop_file_parser::parse(content).unwrap();
531/// assert_eq!(desktop_file.entry.name.default, "Firefox");
532/// ```
533pub fn parse(input: &str) -> Result<DesktopFile, ParseError> {
534    let mut lines = filter_lines(input);
535    let result_entry = Rc::new(RefCell::new(DesktopEntryInternal::default()));
536
537    let mut is_entry_found = false;
538    let mut is_first_entry = true;
539
540    let mut result_actions: Vec<DesktopActionInternal> = vec![];
541    let mut current_target = EntryType::Entry(result_entry.clone());
542
543    for line in lines.iter_mut() {
544        match current_target {
545            EntryType::Entry(ref entry) => match line.line_type() {
546                LineType::Header => {
547                    match parse_header(line)? {
548                        Header::DesktopEntry => {
549                            if is_entry_found {
550                                return Err(ParseError::RepetitiveEntry {
551                                    msg: "none".into(),
552                                    row: line.line_number,
553                                    col: 0,
554                                });
555                            } else {
556                                is_entry_found = true;
557                            }
558
559                            if !is_first_entry {
560                                return Err(ParseError::InternalError { msg: "it should be able to return error when entry is not in the first header".into(), row: line.line_number, col: 0 });
561                            } else {
562                                is_first_entry = false;
563                            }
564                        }
565                        Header::DesktopAction { name } => {
566                            if !is_entry_found {
567                                return Err(ParseError::InternalError { msg: "it should be able to return error when an action appears before an entry".into(), row: line.line_number, col: 0 });
568                            }
569
570                            if is_first_entry {
571                                return Err(ParseError::FormatError {
572                                    msg: "none".into(),
573                                    row: line.line_number,
574                                    col: 0,
575                                });
576                            }
577
578                            result_actions.push(DesktopActionInternal {
579                                ref_name: name,
580                                ..Default::default()
581                            });
582
583                            current_target = EntryType::Action(result_actions.len() - 1);
584                        }
585                        _ => {}
586                    };
587                }
588                LineType::ValPair => {
589                    process_entry_val_pair(line, &mut entry.borrow_mut())?;
590                }
591            },
592
593            EntryType::Action(index) => match line.line_type() {
594                LineType::Header => match parse_header(line)? {
595                    Header::DesktopEntry => {
596                        return Err(ParseError::RepetitiveEntry {
597                            msg: "There should only be one entry on top".into(),
598                            row: line.line_number,
599                            col: 0,
600                        });
601                    }
602                    Header::DesktopAction { name } => {
603                        result_actions.push(DesktopActionInternal {
604                            ref_name: name,
605                            ..Default::default()
606                        });
607                        current_target = EntryType::Action(result_actions.len() - 1)
608                    }
609                    _ => {}
610                },
611                LineType::ValPair => {
612                    let target = &mut result_actions[index];
613                    process_action_val_pair(line, target)?;
614                }
615            },
616        }
617    }
618
619    let mut entry = result_entry.take();
620    let actions = match entry.actions {
621        Some(ref mut d) => vec_to_map(result_actions, d)?,
622        None => HashMap::new(),
623    };
624
625    Ok(DesktopFile {
626        entry: entry.try_into()?,
627        actions,
628    })
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    #[test]
636    fn filter_lines_test() {
637        let res = filter_lines("aaa你好 \n\n\n aaaa\n           #sadas")
638            .iter()
639            .map(|l| l.to_string())
640            .collect::<Vec<_>>();
641
642        println!("{:?}", res);
643        assert_eq!(vec!["aaa你好", "aaaa"], res);
644    }
645
646    #[test]
647    fn test_clense() {
648        let content = r#"
649Name = a
650Type = Application
651        "#;
652
653        let l = filter_lines(content);
654        let parts = split_into_parts(&l[0]).unwrap();
655        assert_eq!(parts.key, "Name".to_string());
656        assert_eq!(parts.value, "a".to_string());
657    }
658}