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#[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 Key,
167 KeyLocale,
169 LocaleToValue,
171 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 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 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
510pub 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}