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 happens when a localized key has no default value")]
27 KeyDoesNotExist,
28 #[error("InvalidValue")]
29 InvalidValue,
30}
31
32struct UnknownKey<'a> {
33 key: &'a str,
34 locale: String,
35 value: String,
36}
37
38impl DesktopEntry {
39 pub fn from_str<L>(
40 path: impl Into<PathBuf>,
41 input: &str,
42 locales_filter: Option<&[L]>,
43 ) -> Result<DesktopEntry, DecodeError>
44 where
45 L: AsRef<str>,
46 {
47 #[inline(never)]
48 fn inner<'a>(
49 path: PathBuf,
50 input: &'a str,
51 locales_filter: Option<Vec<&str>>,
52 ) -> Result<DesktopEntry, DecodeError> {
53 let path: PathBuf = path.into();
54
55 let appid = get_app_id(&path)?;
56
57 let mut groups = Groups::default();
58 let mut active_group: Option<ActiveGroup> = None;
59 let mut active_keys: Option<ActiveKeys> = None;
60 let mut ubuntu_gettext_domain = None;
61
62 let mut unknown_keys: Vec<UnknownKey> = Vec::new();
63
64 for line in input.lines() {
65 process_line(
66 line,
67 &mut groups,
68 &mut active_group,
69 &mut active_keys,
70 &mut ubuntu_gettext_domain,
71 locales_filter.as_deref(),
72 &mut unknown_keys,
73 )?;
74 }
75
76 for unknown_key in unknown_keys.drain(..) {
78 match &mut active_group {
79 Some(active_group) => match active_group.group.0.get_mut(unknown_key.key) {
80 Some((_, locale_map)) => {
81 locale_map.insert(unknown_key.locale, unknown_key.value);
82 }
83 None => return Err(DecodeError::KeyDoesNotExist),
84 },
85 None => return Err(DecodeError::KeyDoesNotExist),
86 }
87 }
88
89 if let Some(active_keys) = active_keys.take() {
90 match &mut active_group {
91 Some(active_group) => {
92 active_group.group.0.insert(
93 active_keys.key_name,
94 (active_keys.default_value, active_keys.locales),
95 );
96 }
97 None => return Err(DecodeError::KeyValueWithoutAGroup),
98 }
99 }
100
101 if let Some(mut group) = active_group.take() {
102 groups
103 .0
104 .entry(group.group_name)
105 .or_insert_with(|| Group::default())
106 .0
107 .append(&mut group.group.0);
108 }
109
110 Ok(DesktopEntry {
111 appid,
112 groups,
113 path,
114 ubuntu_gettext_domain,
115 })
116 }
117
118 inner(path.into(), input, locales_filter.map(add_generic_locales))
119 }
120
121 #[inline]
123 pub fn from_path<L>(
124 path: impl Into<PathBuf>,
125 locales_filter: Option<&[L]>,
126 ) -> Result<DesktopEntry, DecodeError>
127 where
128 L: AsRef<str>,
129 {
130 let path: PathBuf = path.into();
131 let input = fs::read_to_string(&path)?;
132 Self::from_str(path, &input, locales_filter)
133 }
134}
135
136#[inline]
137fn get_app_id<P: AsRef<Path> + ?Sized>(path: &P) -> Result<String, DecodeError> {
138 let path_as_bytes = path
139 .as_ref()
140 .as_os_str()
141 .as_encoded_bytes()
142 .strip_suffix(b".desktop")
143 .ok_or(DecodeError::AppID)?;
144
145 Ok(
146 if let Some((_prefix, entry)) = path_as_bytes.rsplit_once_str("/applications/") {
147 String::from_utf8(entry.replace(b"/", b"-"))
148 .ok()
149 .ok_or(DecodeError::AppID)?
150 } else {
151 path.as_ref()
152 .file_stem()
153 .ok_or(DecodeError::AppID)?
154 .to_str()
155 .ok_or(DecodeError::AppID)?
156 .to_owned()
157 },
158 )
159}
160
161#[derive(Debug)]
162struct ActiveGroup {
163 group_name: String,
164 group: Group,
165}
166
167#[derive(Debug)]
168struct ActiveKeys {
169 key_name: String,
170 default_value: String,
171 locales: LocaleMap,
172}
173
174#[inline(never)]
175fn process_line<'a>(
176 line: &'a str,
177 groups: &mut Groups,
178 active_group: &mut Option<ActiveGroup>,
179 active_keys: &mut Option<ActiveKeys>,
180 ubuntu_gettext_domain: &mut Option<String>,
181 locales_filter: Option<&[&str]>,
182 unknown_keys: &mut Vec<UnknownKey<'a>>,
183) -> Result<(), DecodeError> {
184 if line.trim().is_empty() || line.starts_with('#') {
185 return Ok(());
186 }
187
188 let line_bytes = line.as_bytes();
189
190 if line_bytes[0] == b'[' {
192 for unknown_key in unknown_keys.drain(..) {
194 match active_group {
195 Some(active_group) => match active_group.group.0.get_mut(unknown_key.key) {
196 Some((_, locale_map)) => {
197 locale_map.insert(unknown_key.locale, unknown_key.value);
198 }
199 None => return Err(DecodeError::KeyDoesNotExist),
200 },
201 None => return Err(DecodeError::KeyDoesNotExist),
202 }
203 }
204
205 if let Some(end) = memchr::memrchr(b']', &line_bytes[1..]) {
206 let group_name = &line[1..end + 1];
207
208 if let Some(active_keys) = active_keys.take() {
209 match active_group {
210 Some(active_group) => {
211 active_group.group.0.insert(
212 active_keys.key_name,
213 (active_keys.default_value, active_keys.locales),
214 );
215 }
216 None => return Err(DecodeError::KeyValueWithoutAGroup),
217 }
218 }
219
220 if let Some(mut group) = active_group.take() {
221 groups
222 .0
223 .entry(group.group_name)
224 .or_insert_with(|| Group::default())
225 .0
226 .append(&mut group.group.0);
227 }
228
229 active_group.replace(ActiveGroup {
230 group_name: group_name.to_string(),
231 group: Group::default(),
232 });
233 }
234 }
235 else if let Some(delimiter) = memchr::memchr(b'=', line_bytes) {
237 let key = &line[..delimiter];
238 let value = format_value(&line[delimiter + 1..])?;
239
240 if key.is_empty() {
241 return Err(DecodeError::InvalidKey);
242 }
243
244 if key.as_bytes()[key.len() - 1] == b']' {
246 if let Some(start) = memchr::memchr(b'[', key.as_bytes()) {
247 let locale = &key[start + 1..key.len() - 1];
248
249 let key = &key[..start];
250
251 match locales_filter {
252 Some(locales_filter) if !locales_filter.iter().any(|l| *l == locale) => {
253 return Ok(());
254 }
255 _ => (),
256 }
257
258 if let Some(active_keys) = active_keys
261 .as_mut()
262 .filter(|active_keys| active_keys.key_name == key)
263 {
264 active_keys.locales.insert(locale.to_string(), value);
265 } else {
266 unknown_keys.push(UnknownKey {
267 key,
268 locale: locale.to_string(),
269 value,
270 });
271 }
272
273 return Ok(());
274 }
275 }
276
277 if key == "X-Ubuntu-Gettext-Domain" {
278 *ubuntu_gettext_domain = Some(value.to_string());
279 return Ok(());
280 }
281
282 if let Some(active_keys) = active_keys.take() {
283 match active_group {
284 Some(active_group) => {
285 active_group.group.0.insert(
286 active_keys.key_name,
287 (active_keys.default_value, active_keys.locales),
288 );
289 }
290 None => return Err(DecodeError::KeyValueWithoutAGroup),
291 }
292 }
293 active_keys.replace(ActiveKeys {
294 key_name: key.trim().to_string(),
296 default_value: value,
297 locales: LocaleMap::default(),
298 });
299 }
300 Ok(())
301}
302
303#[inline]
305fn format_value(input: &str) -> Result<String, DecodeError> {
306 let input = if let Some(input) = input.strip_prefix(" ") {
307 input
308 } else {
309 input
310 };
311
312 let mut res = String::with_capacity(input.len());
313
314 let mut last: usize = 0;
315
316 for i in memchr::memchr_iter(b'\\', input.as_bytes()) {
317 if last > i {
319 continue;
320 }
321
322 if input.len() <= i + 1 {
324 return Err(DecodeError::InvalidValue);
325 }
326
327 if last < i {
328 res.push_str(&input[last..i]);
329 }
330
331 last = i + 2;
332
333 match input.as_bytes()[i + 1] {
334 b's' => res.push(' '),
335 b'n' => res.push('\n'),
336 b't' => res.push('\t'),
337 b'r' => res.push('\r'),
338 b'\\' => res.push('\\'),
339 _ => {
340 return Err(DecodeError::InvalidValue);
341 }
342 }
343 }
344
345 if last < input.len() {
346 res.push_str(&input[last..input.len()]);
347 }
348
349 Ok(res)
350}
351
352#[inline]
354fn add_generic_locales<L: AsRef<str>>(locales: &[L]) -> Vec<&str> {
355 let mut v = Vec::with_capacity(locales.len() + 1);
356
357 for l in locales {
358 let l = l.as_ref();
359
360 v.push(l);
361
362 if let Some(start) = memchr::memchr(b'_', l.as_bytes()) {
363 v.push(l.split_at(start).0)
364 }
365 }
366
367 v
368}