freedesktop_file_parser/
parser.rs

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