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