1use regex::Regex;
2use std::{
3 collections::HashMap,
4 path::{Path, PathBuf},
5 fs::File,
6 io::{BufRead, BufReader},
7};
8
9#[derive(Debug, Clone)]
10pub enum ParseError {
11 IoError(String),
12 InvalidFormat(String),
13 MissingRequiredKey(String),
14}
15
16impl std::fmt::Display for ParseError {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 ParseError::IoError(msg) => write!(f, "IO error: {}", msg),
20 ParseError::InvalidFormat(msg) => write!(f, "Invalid format: {}", msg),
21 ParseError::MissingRequiredKey(msg) => write!(f, "Missing required key: {}", msg),
22 }
23 }
24}
25
26impl std::error::Error for ParseError {}
27
28#[derive(Debug, Clone, PartialEq)]
29pub enum ValueType {
30 String(String),
31 #[allow(dead_code)] LocaleString(String),
33 #[allow(dead_code)] IconString(String),
35 Boolean(bool),
36 Numeric(f64),
37 StringList(Vec<String>),
38 #[allow(dead_code)] LocaleStringList(Vec<String>),
40}
41
42#[derive(Debug, Clone)]
43pub struct LocalizedKey {
44 pub key: String,
45 pub locale: Option<String>,
46}
47
48impl LocalizedKey {
49 pub fn parse(input: &str) -> Self {
50 if let Some(bracket_start) = input.find('[') {
51 if let Some(bracket_end) = input.find(']') {
52 if bracket_start < bracket_end {
53 let key = input[..bracket_start].to_string();
54 let locale = input[bracket_start + 1..bracket_end].to_string();
55 return Self {
56 key,
57 locale: Some(locale),
58 };
59 }
60 }
61 }
62 Self {
63 key: input.to_string(),
64 locale: None,
65 }
66 }
67}
68
69#[derive(Debug, Default)]
70pub struct DesktopEntryGroup {
71 #[allow(dead_code)] pub name: String,
73 pub fields: HashMap<String, ValueType>,
74 pub localized_fields: HashMap<String, HashMap<String, ValueType>>,
75}
76
77impl DesktopEntryGroup {
78 pub fn new<S: Into<String>>(name: S) -> Self {
79 Self {
80 name: name.into(),
81 fields: HashMap::new(),
82 localized_fields: HashMap::new(),
83 }
84 }
85
86 pub fn insert_field(&mut self, key: &str, value: ValueType) {
87 let localized_key = LocalizedKey::parse(key);
88
89 if let Some(locale) = localized_key.locale {
90 self.localized_fields
91 .entry(localized_key.key)
92 .or_default()
93 .insert(locale, value);
94 } else {
95 self.fields.insert(localized_key.key, value);
96 }
97 }
98
99 pub fn get_field(&self, key: &str) -> Option<&ValueType> {
100 self.fields.get(key)
101 }
102
103 pub fn get_localized_field(&self, key: &str, locale: Option<&str>) -> Option<&ValueType> {
104 if let Some(locale) = locale {
105 if let Some(localized_map) = self.localized_fields.get(key) {
106 if let Some(value) = localized_map.get(locale) {
108 return Some(value);
109 }
110
111 if let Some(value) = self.try_locale_fallback(localized_map, locale) {
113 return Some(value);
114 }
115 }
116 }
117
118 self.fields.get(key)
120 }
121
122 fn try_locale_fallback<'a>(&self, localized_map: &'a HashMap<String, ValueType>, locale: &str) -> Option<&'a ValueType> {
123 let locale_without_encoding = if let Some(dot_pos) = locale.find('.') {
125 &locale[..dot_pos]
126 } else {
127 locale
128 };
129
130 let (lang, country, modifier) = Self::parse_locale_components(locale_without_encoding);
132
133 if let (Some(country), Some(modifier)) = (country, modifier) {
140 let full_locale = format!("{}_{}{}", lang, country, modifier);
142 if let Some(value) = localized_map.get(&full_locale) {
143 return Some(value);
144 }
145
146 let lang_country = format!("{}_{}", lang, country);
148 if let Some(value) = localized_map.get(&lang_country) {
149 return Some(value);
150 }
151
152 let lang_modifier = format!("{}{}", lang, modifier);
154 if let Some(value) = localized_map.get(&lang_modifier) {
155 return Some(value);
156 }
157 } else if let Some(country) = country {
158 let lang_country = format!("{}_{}", lang, country);
160 if let Some(value) = localized_map.get(&lang_country) {
161 return Some(value);
162 }
163 } else if let Some(modifier) = modifier {
164 let lang_modifier = format!("{}{}", lang, modifier);
166 if let Some(value) = localized_map.get(&lang_modifier) {
167 return Some(value);
168 }
169 }
170
171 localized_map.get(lang)
173 }
174
175 fn parse_locale_components(locale: &str) -> (&str, Option<&str>, Option<&str>) {
176 let (base, modifier) = if let Some(at_pos) = locale.find('@') {
177 (&locale[..at_pos], Some(&locale[at_pos..]))
178 } else {
179 (locale, None)
180 };
181
182 let (lang, country) = if let Some(under_pos) = base.find('_') {
183 (&base[..under_pos], Some(&base[under_pos + 1..]))
184 } else {
185 (base, None)
186 };
187
188 (lang, country, modifier)
189 }
190}
191
192#[derive(Debug, Default)]
193pub struct DesktopEntry {
194 pub path: PathBuf,
195 pub groups: HashMap<String, DesktopEntryGroup>,
196}
197
198impl DesktopEntry {
199 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, ParseError> {
200 let file = File::open(path.as_ref())
201 .map_err(|e| ParseError::IoError(format!("Failed to open file: {}", e)))?;
202 let reader = BufReader::new(file);
203
204 let group_header_regex = Regex::new(r"^\[([^\[\]]+)\]$")
205 .map_err(|e| ParseError::InvalidFormat(format!("Regex error: {}", e)))?;
206
207 let mut current_group: Option<String> = None;
208 let mut entry = DesktopEntry {
209 path: path.as_ref().to_path_buf(),
210 ..Default::default()
211 };
212
213 for (line_num, line) in reader.lines().enumerate() {
214 let line = line.map_err(|e| ParseError::IoError(format!("Failed to read line {}: {}", line_num + 1, e)))?;
215 let line = line.trim();
216
217 if line.is_empty() || line.starts_with('#') {
219 continue;
220 }
221
222 if let Some(captures) = group_header_regex.captures(line) {
224 let group_name = captures[1].to_string();
225 current_group = Some(group_name.clone());
226 entry.groups.entry(group_name.clone())
227 .or_insert_with(|| DesktopEntryGroup::new(group_name));
228 continue;
229 }
230
231 if let Some(eq_pos) = line.find('=') {
233 let key = line[..eq_pos].trim();
234 let value = line[eq_pos + 1..].trim();
235
236 if key.is_empty() {
237 continue; }
239
240 if !is_valid_key_name(key) {
241 return Err(ParseError::InvalidFormat(format!("Invalid key name: {}", key)));
242 }
243
244 if let Some(ref group_name) = current_group {
245 let parsed_value = parse_value(value)?;
246 if let Some(group) = entry.groups.get_mut(group_name) {
247 group.insert_field(key, parsed_value);
248 }
249 } else {
250 return Err(ParseError::InvalidFormat("Key-value pair found before any group header".to_string()));
251 }
252 }
253 }
254
255 entry.validate()?;
257
258 Ok(entry)
259 }
260
261 fn validate(&self) -> Result<(), ParseError> {
262 let desktop_entry = self.groups.get("Desktop Entry")
263 .ok_or_else(|| ParseError::MissingRequiredKey("Desktop Entry group is required".to_string()))?;
264
265 let entry_type = desktop_entry.get_field("Type")
267 .ok_or_else(|| ParseError::MissingRequiredKey("Type key is required".to_string()))?;
268
269 desktop_entry.get_field("Name")
271 .ok_or_else(|| ParseError::MissingRequiredKey("Name key is required".to_string()))?;
272
273 if let ValueType::String(type_val) = entry_type {
275 if type_val == "Application" {
276 let dbus_activatable = desktop_entry.get_field("DBusActivatable")
277 .and_then(|v| match v {
278 ValueType::Boolean(b) => Some(*b),
279 _ => None,
280 })
281 .unwrap_or(false);
282
283 if !dbus_activatable {
284 desktop_entry.get_field("Exec")
285 .ok_or_else(|| ParseError::MissingRequiredKey("Exec key is required for Application type".to_string()))?;
286 }
287 } else if type_val == "Link" {
288 desktop_entry.get_field("URL")
290 .ok_or_else(|| ParseError::MissingRequiredKey("URL key is required for Link type".to_string()))?;
291 }
292 }
293
294 Ok(())
295 }
296
297 pub fn get_desktop_entry_group(&self) -> Option<&DesktopEntryGroup> {
298 self.groups.get("Desktop Entry")
299 }
300}
301
302fn is_valid_key_name(key: &str) -> bool {
303 let base_key = if let Some(bracket_pos) = key.find('[') {
305 &key[..bracket_pos]
306 } else {
307 key
308 };
309
310 base_key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
312}
313
314fn parse_value(value: &str) -> Result<ValueType, ParseError> {
315 let unescaped = unescape_value(value);
317
318 match unescaped.to_lowercase().as_str() {
320 "true" => return Ok(ValueType::Boolean(true)),
321 "false" => return Ok(ValueType::Boolean(false)),
322 _ => {}
323 }
324
325 if let Ok(num) = unescaped.parse::<f64>() {
327 return Ok(ValueType::Numeric(num));
328 }
329
330 if value.contains(';') {
332 let items = split_semicolon_list(value);
333 return Ok(ValueType::StringList(items));
334 }
335
336 Ok(ValueType::String(unescaped))
338}
339
340fn unescape_value(value: &str) -> String {
341 let mut result = String::new();
342 let mut chars = value.chars();
343
344 while let Some(ch) = chars.next() {
345 if ch == '\\' {
346 if let Some(next_ch) = chars.next() {
347 match next_ch {
348 's' => result.push(' '),
349 'n' => result.push('\n'),
350 't' => result.push('\t'),
351 'r' => result.push('\r'),
352 '\\' => result.push('\\'),
353 ';' => result.push(';'), _ => {
355 result.push('\\');
357 result.push(next_ch);
358 }
359 }
360 } else {
361 result.push('\\');
362 }
363 } else {
364 result.push(ch);
365 }
366 }
367
368 result
369}
370
371fn split_semicolon_list(value: &str) -> Vec<String> {
372 let mut result = Vec::new();
373 let mut current_item = String::new();
374 let mut chars = value.chars().peekable();
375
376 while let Some(ch) = chars.next() {
377 if ch == '\\' {
378 if let Some(&next_ch) = chars.peek() {
379 if next_ch == ';' {
380 current_item.push(';');
382 chars.next(); } else {
384 current_item.push(ch);
386 if let Some(escaped_ch) = chars.next() {
387 current_item.push(escaped_ch);
388 }
389 }
390 } else {
391 current_item.push(ch);
392 }
393 } else if ch == ';' {
394 let trimmed = current_item.trim();
396 if !trimmed.is_empty() {
397 result.push(unescape_value(trimmed));
398 }
399 current_item.clear();
400 } else {
401 current_item.push(ch);
402 }
403 }
404
405 let trimmed = current_item.trim();
407 if !trimmed.is_empty() {
408 result.push(unescape_value(trimmed));
409 }
410
411 result
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn test_localized_key_parsing() {
420 let key = LocalizedKey::parse("Name");
421 assert_eq!(key.key, "Name");
422 assert_eq!(key.locale, None);
423
424 let key = LocalizedKey::parse("Name[en_US]");
425 assert_eq!(key.key, "Name");
426 assert_eq!(key.locale, Some("en_US".to_string()));
427 }
428
429 #[test]
430 fn test_value_parsing() {
431 assert_eq!(parse_value("true").unwrap(), ValueType::Boolean(true));
432 assert_eq!(parse_value("false").unwrap(), ValueType::Boolean(false));
433 assert_eq!(parse_value("123.45").unwrap(), ValueType::Numeric(123.45));
434 assert_eq!(parse_value("hello").unwrap(), ValueType::String("hello".to_string()));
435 assert_eq!(
436 parse_value("one;two;three").unwrap(),
437 ValueType::StringList(vec!["one".to_string(), "two".to_string(), "three".to_string()])
438 );
439 }
440
441 #[test]
442 fn test_escape_sequences() {
443 assert_eq!(unescape_value("hello\\sworld"), "hello world");
444 assert_eq!(unescape_value("line1\\nline2"), "line1\nline2");
445 assert_eq!(unescape_value("tab\\there"), "tab\there");
446 assert_eq!(unescape_value("backslash\\\\"), "backslash\\");
447 }
448
449 #[test]
450 fn test_key_validation() {
451 assert!(is_valid_key_name("Name"));
452 assert!(is_valid_key_name("Name[en_US]"));
453 assert!(is_valid_key_name("X-Custom-Key"));
454 assert!(!is_valid_key_name("Invalid Key"));
455 assert!(!is_valid_key_name("Key=Value"));
456 }
457}