freedesktop_desktop_entry/
decoder.rs1use std::{
5 fs::{self},
6 path::{Path, PathBuf},
7};
8
9use crate::{DesktopEntry, Group};
10use crate::{Groups, LocaleMap};
11use thiserror::Error;
12
13#[derive(Debug, Error)]
14pub enum DecodeError {
15 #[error("path does not contain a valid app ID")]
16 AppID,
17 #[error(transparent)]
18 Io(#[from] std::io::Error),
19 #[error("MultipleGroupWithSameName")]
20 MultipleGroupWithSameName,
21 #[error("KeyValueWithoutAGroup")]
22 KeyValueWithoutAGroup,
23 #[error("InvalidKey. Accepted: A-Za-z0-9")]
24 InvalidKey,
25 #[error("KeyDoesNotExist, this can happens when a localized key has no default value")]
26 KeyDoesNotExist,
27 #[error("InvalidValue")]
28 InvalidValue,
29}
30
31impl DesktopEntry {
32 pub fn from_str<L>(
33 path: impl Into<PathBuf>,
34 input: &str,
35 locales_filter: Option<&[L]>,
36 ) -> Result<DesktopEntry, DecodeError>
37 where
38 L: AsRef<str>,
39 {
40 let path: PathBuf = path.into();
41
42 let appid = get_app_id(&path)?;
43
44 let mut groups = Groups::default();
45 let mut active_group: Option<ActiveGroup> = None;
46 let mut active_keys: Option<ActiveKeys> = None;
47 let mut ubuntu_gettext_domain = None;
48
49 let locales_filter = locales_filter.map(add_generic_locales);
50
51 for line in input.lines() {
52 process_line(
53 line,
54 &mut groups,
55 &mut active_group,
56 &mut active_keys,
57 &mut ubuntu_gettext_domain,
58 locales_filter.as_deref(),
59 )?
60 }
61
62 if let Some(active_keys) = active_keys.take() {
63 match &mut active_group {
64 Some(active_group) => {
65 active_group.group.0.insert(
66 active_keys.key_name,
67 (active_keys.default_value, active_keys.locales),
68 );
69 }
70 None => return Err(DecodeError::KeyValueWithoutAGroup),
71 }
72 }
73
74 if let Some(group) = active_group.take() {
75 if groups.0.insert(group.group_name, group.group).is_some() {
76 return Err(DecodeError::MultipleGroupWithSameName);
77 }
78 }
79
80 Ok(DesktopEntry {
81 appid: appid.to_string(),
82 groups,
83 path,
84 ubuntu_gettext_domain,
85 })
86 }
87
88 pub fn from_path<L>(
90 path: impl Into<PathBuf>,
91 locales_filter: Option<&[L]>,
92 ) -> Result<DesktopEntry, DecodeError>
93 where
94 L: AsRef<str>,
95 {
96 let path: PathBuf = path.into();
97 let input = fs::read_to_string(&path)?;
98 Self::from_str(path, &input, locales_filter)
99 }
100}
101
102fn get_app_id<P: AsRef<Path> + ?Sized>(path: &P) -> Result<&str, DecodeError> {
103 let appid = path
104 .as_ref()
105 .file_stem()
106 .ok_or(DecodeError::AppID)?
107 .to_str()
108 .ok_or(DecodeError::AppID)?;
109 Ok(appid)
110}
111
112#[derive(Debug)]
113struct ActiveGroup {
114 group_name: String,
115 group: Group,
116}
117
118#[derive(Debug)]
119struct ActiveKeys {
120 key_name: String,
121 default_value: String,
122 locales: LocaleMap,
123}
124
125#[inline]
126fn process_line<L: AsRef<str>>(
127 line: &str,
128 groups: &mut Groups,
129 active_group: &mut Option<ActiveGroup>,
130 active_keys: &mut Option<ActiveKeys>,
131 ubuntu_gettext_domain: &mut Option<String>,
132 locales_filter: Option<&[L]>,
133) -> Result<(), DecodeError> {
134 if line.trim().is_empty() || line.starts_with('#') {
135 return Ok(());
136 }
137
138 let line_bytes = line.as_bytes();
139
140 if line_bytes[0] == b'[' {
141 if let Some(end) = memchr::memrchr(b']', &line_bytes[1..]) {
142 let group_name = &line[1..end + 1];
143
144 if let Some(active_keys) = active_keys.take() {
145 match active_group {
146 Some(active_group) => {
147 active_group.group.0.insert(
148 active_keys.key_name,
149 (active_keys.default_value, active_keys.locales),
150 );
151 }
152 None => return Err(DecodeError::KeyValueWithoutAGroup),
153 }
154 }
155
156 if let Some(group) = active_group.take() {
157 if groups.0.insert(group.group_name, group.group).is_some() {
158 return Err(DecodeError::MultipleGroupWithSameName);
159 }
160 }
161
162 active_group.replace(ActiveGroup {
163 group_name: group_name.to_string(),
164 group: Group::default(),
165 });
166 }
167 } else if let Some(delimiter) = memchr::memchr(b'=', line_bytes) {
168 let key = &line[..delimiter];
169 let value = format_value(&line[delimiter + 1..])?;
170
171 if key.is_empty() {
172 return Err(DecodeError::InvalidKey);
173 }
174
175 if key.as_bytes()[key.len() - 1] == b']' {
177 if let Some(start) = memchr::memchr(b'[', key.as_bytes()) {
178 let locale = &key[start + 1..key.len() - 1];
183
184 match locales_filter {
185 Some(locales_filter)
186 if !locales_filter.iter().any(|l| l.as_ref() == locale) =>
187 {
188 return Ok(())
189 }
190 _ => (),
191 }
192
193 match active_keys {
194 Some(active_keys) => {
195 active_keys.locales.insert(locale.to_string(), value);
196 }
197 None => return Err(DecodeError::KeyDoesNotExist),
198 }
199
200 return Ok(());
201 }
202 }
203
204 if key == "X-Ubuntu-Gettext-Domain" {
205 *ubuntu_gettext_domain = Some(value.to_string());
206 return Ok(());
207 }
208
209 if let Some(active_keys) = active_keys.take() {
210 match active_group {
211 Some(active_group) => {
212 active_group.group.0.insert(
213 active_keys.key_name,
214 (active_keys.default_value, active_keys.locales),
215 );
216 }
217 None => return Err(DecodeError::KeyValueWithoutAGroup),
218 }
219 }
220 active_keys.replace(ActiveKeys {
221 key_name: key.trim().to_string(),
223 default_value: value,
224 locales: LocaleMap::default(),
225 });
226 }
227 Ok(())
228}
229
230#[inline]
232fn format_value(input: &str) -> Result<String, DecodeError> {
233 let input = if let Some(input) = input.strip_prefix(" ") {
234 input
235 } else {
236 input
237 };
238
239 let mut res = String::with_capacity(input.len());
240
241 let mut last: usize = 0;
242
243 for i in memchr::memchr_iter(b'\\', input.as_bytes()) {
244 if last > i {
246 continue;
247 }
248
249 if input.len() <= i + 1 {
251 return Err(DecodeError::InvalidValue);
252 }
253
254 if last < i {
255 res.push_str(&input[last..i]);
256 }
257
258 last = i + 2;
259
260 match input.as_bytes()[i + 1] {
261 b's' => res.push(' '),
262 b'n' => res.push('\n'),
263 b't' => res.push('\t'),
264 b'r' => res.push('\r'),
265 b'\\' => res.push('\\'),
266 _ => {
267 return Err(DecodeError::InvalidValue);
268 }
269 }
270 }
271
272 if last < input.len() {
273 res.push_str(&input[last..input.len()]);
274 }
275
276 Ok(res)
277}
278
279pub(crate) fn add_generic_locales<L: AsRef<str>>(locales: &[L]) -> Vec<&str> {
281 let mut v = Vec::with_capacity(locales.len() + 1);
282
283 for l in locales {
284 let l = l.as_ref();
285
286 v.push(l);
287
288 if let Some(start) = memchr::memchr(b'_', l.as_bytes()) {
289 v.push(l.split_at(start).0)
290 }
291 }
292
293 v
294}