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