1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
#[cfg(target_os = "android")]
use std::path::PathBuf;
use std::{borrow::Cow, io, ops::ControlFlow, path::Path};
#[cfg(windows)]
use directories::BaseDirs;
use crate::{
EnvPath, OsCow, ProjectDirs,
os_cow::{self, into_os_cow},
parser::{FULL_COLON, HALF_COLON},
};
/// Implement additional methods for EnvPath when the `project` feature is
/// enabled
///
/// If you see a method(function) with a parameter name containing **_** prefix
/// (e.g. **_name**) in some methods, do not delete it. This may be a
/// platform-specific parameter, so to avoid the "unused variable" warning, I've
/// added the "_" prefix.
impl EnvPath<'_> {
// Method to extract project name information from a string
pub(crate) fn get_project_name(c0: &str) -> Option<(&str, &str, Cow<str>)> {
// Find the first and last occurrence of parentheses in the string
let (start, end) = (c0.find('(')?, c0.rfind(')')?);
// Extract the content within the parentheses
let content = &c0[start + 1..end];
// Split the content by periods and trim each part
let parts = content
.split('.')
.map(|x| x.trim())
.collect::<Vec<_>>();
// Extract the qualifier, organization, and application from the parts
let (qualifier, organization, application) = match parts.len() {
0 => return None, // If there are no parts, return None
1 => (parts[0], "", Cow::from(parts[0])), /* If there is only one part, use */
// it as the application name
2 => (parts[0], "", Cow::from(parts[1])), /* If there are two parts, use */
// the first as the qualifier and
// the second as the application
// name
3 => (parts[0], parts[1], Cow::from(parts[2])), /* If there are three */
// parts, use the first as
// the qualifier, the
// second as the
// organization, and the
// third as the application
// name
_ => (parts[0], parts[1], Cow::from(parts[2..].concat())), /* If there are more than three parts, use the first as the qualifier, the second as the organization, and the rest as the application name */
};
Some((qualifier, organization, application))
}
// Method to set the project path
pub(crate) fn set_proj_path<'a>(
name: &str,
proj: Option<&ProjectDirs>,
) -> OsCow<'a> {
match () {
#[cfg(target_os = "android")]
() => into_os_cow(name), /* If the target OS is Android, use the input name */
// as the path
#[allow(unreachable_patterns)]
() => match proj {
Some(s) => into_os_cow(s.project_path()), /* If a ProjectDirs object is */
// provided, use its project
// path
_ => into_os_cow(name), // Otherwise, use the input name as the path
},
}
}
/// The function returns an `io::Result<ProjectDirs>`, which is created using
/// the [ProjectDirs::from()](directories::ProjectDirs::from) method.
///
/// Note: `directories::ProjectDirs` is different from `$proj` of EnvPath!
///
/// | Parameter | Description |
/// | --------- | ------------------------------------------------------------------------------------- |
/// | qual | The qualifier of the project, which is a string reference. |
/// | org | The organization responsible for the project, also as a string reference. |
/// | app | The name of the application associated with the project, again as a string reference. |
///
/// Here's a table explaining the parts of the string "org.moz.ff" and what
/// they represent:
///
/// | Part | Abbreviation | Meaning |
/// |------|-------------|--------------|
/// | org | qual | qualifier |
/// | moz | org | organization |
/// | ff | app | application |
pub fn new_project<Q, O, A>(qual: Q, org: O, app: A) -> io::Result<ProjectDirs>
where
Q: AsRef<str>,
O: AsRef<str>,
A: AsRef<str>,
{
ProjectDirs::from(qual.as_ref(), org.as_ref(), app.as_ref()).ok_or_else(|| {
io::Error::new(
io::ErrorKind::Unsupported,
"Cannot generate ProjectDirs for your platform.",
)
})
}
// Method to set the project directory
pub(crate) fn set_proj_dir<'a, F>(
proj: Option<&ProjectDirs>,
f: F,
_android_iter: &[&str],
) -> OsCow<'a>
where
F: Fn(&ProjectDirs) -> &Path,
{
match () {
#[cfg(target_os = "android")]
() => into_os_cow(PathBuf::from_iter(_android_iter)),
#[allow(unreachable_patterns)]
() => proj.and_then(|s| into_os_cow(f(s))),
/* Otherwise, use the configuration directory
* provided by the ProjectDirs */
}
}
/// This contains more complex parsing rules than `parse_dir_rules()`.
/// For example:
/// `$proj(com.a.b): state ? cfg ?? data ? local-data ? (com. x. y. z): data
/// ?? local-data ? cfg`
///
/// If the value of the state path for the project (com.a.b) exists, then
/// break, otherwise continue parsing. Next is cfg, which, because it is
/// followed by `?`, so not only must the value exist, but the path must exist
/// as well. If one of the two does not exist, the parsing continues. If
/// neither `data` nor `local-data` has a value, then we're at the (com.x.y.z)
/// project and continue parsing.
fn parse_proj_dir_rules<'a>(
first: &str,
remain: &'a str,
separator: char,
) -> ControlFlow<OsCow<'a>, OsCow<'a>> {
use ControlFlow::{Break, Continue};
let mut last_chunk = String::with_capacity(20);
remain
.split_terminator(separator)
.enumerate()
.map(|(u, x)| (u, x.trim()))
.try_fold(None, |acc: OsCow, (u, x)| match (acc, x.is_empty()) {
(None, true) => Continue(None),
(None, false) => {
let c0 = match (u, x.find('(')) {
(0, None) => {
// dbg!(&chunk, &x);
last_chunk = first.to_owned();
first
}
(_, Some(_)) => {
last_chunk = x.to_owned();
x
}
_ => &last_chunk,
};
// dbg!(&c0);
let Some((name, proj)) = Self::set_proj_name_opt_tuple(c0) else {
return Break(None);
};
let ident = match x
.rsplit([FULL_COLON, HALF_COLON])
.map(|x| x.trim())
.next()
{
Some(x) => x,
_ => return Break(None),
};
// dbg!(&name, &proj, &ident);
// dbg!(&ident);
Continue(Self::match_proj_dirs(ident, &name, proj.as_ref()))
}
(p, false) => Break(p),
(Some(p), true) => match Path::new(&p) {
x if x.exists() => Break(Some(p)),
_ => Continue(None),
},
})
}
pub(crate) fn handle_project_dirs<'a>(
first_chunk: &'a str,
remain: &'a str,
) -> OsCow<'a> {
use ControlFlow::{Break, Continue};
match Self::get_question_mark_separator(remain) {
' ' => {
let (name, proj) = Self::set_proj_name_opt_tuple(first_chunk)?;
Self::match_proj_dirs(remain, &name, proj.as_ref())
}
sep => match Self::parse_proj_dir_rules(first_chunk, remain, sep) {
Break(x) | Continue(x) => x,
},
}
}
pub(crate) fn set_proj_name_opt_tuple(
chunk: &str,
) -> Option<(String, Option<ProjectDirs>)> {
// Extract the project name information from the first chunk
// If the project name information cannot be extracted, return None
let (qual, org, app) = Self::get_project_name(chunk)?;
// Create a ProjectDirs object using the project name information
let proj = ProjectDirs::from(qual, org, &app);
// Construct the project name by joining the qualifier, organization, and
// application
Some((
[qual, org, &app]
.into_iter()
.filter(|x| !x.is_empty())
.collect::<Vec<_>>()
.join("."),
proj,
))
}
// Method to handle a project directory request
pub(crate) fn match_proj_dirs<'a>(
ident: &'a str,
name: &str,
proj: Option<&ProjectDirs>,
) -> OsCow<'a> {
// Define a closure to convert an Option<Path> to an OsCow
let and_then_cow = |s: Option<&Path>| s.and_then(into_os_cow);
// Determine which project directory is being requested and set the
// corresponding path
let proj_path = || Self::set_proj_path(name, proj);
match ident {
"path" => proj_path(), // Set the project path
"cache" => Self::set_proj_dir(
proj,
ProjectDirs::cache_dir,
&["/", "data", "data", name, "cache"],
),
"cfg" | "config" => Self::set_proj_dir(
proj,
ProjectDirs::config_dir,
&["/", "data", "data", name, "files"],
),
"data" => {
Self::set_proj_dir(proj, ProjectDirs::data_dir, &["/", "data", "data", name])
}
"local-data" | "local_data" => Self::set_proj_dir(
proj,
ProjectDirs::data_local_dir,
&[os_cow::AND_SD, "Android", "data", name],
),
"local-cfg" | "local_cfg" | "local_config" => Self::set_proj_dir(
proj,
ProjectDirs::config_local_dir,
&[os_cow::AND_SD, "Android", "data", name, "files"],
),
"pref" | "preference" => Self::set_proj_dir(
proj,
ProjectDirs::preference_dir,
&["/", "data", "data", name, "files"],
),
"runtime" => proj.and_then(|x| and_then_cow(x.runtime_dir())),
"state" => proj.and_then(|x| and_then_cow(x.state_dir())),
"cli-data" | "cli_data" => proj.and_then(|p| into_os_cow(p.data_local_dir())),
"cli-cfg" | "cli_cfg" | "cli_config" => {
proj.and_then(|p| into_os_cow(p.config_local_dir()))
}
"cli-cache" | "cli_cache" => proj.and_then(|p| into_os_cow(p.cache_dir())),
#[cfg(windows)]
"local-low" | "local_low" => {
let opt = BaseDirs::new().and_then(|p| {
proj_path().map(|x| {
p.data_local_dir()
.join("LocalLow")
.join(x)
})
});
opt.and_then(into_os_cow)
}
"empty" => os_cow::from_str(""),
x if Self::starts_with_remix_expr(x) => Self::parse_remix_expr(x),
_ => None,
// If an unknown directory is requested, return None
}
}
}
#[cfg(test)]
mod tests {
use crate::EnvPath;
#[test]
fn test_proj_dir() {
let path = EnvPath::from([
"$project(me. tmm. store-demo): cfg ? runtime ?? ( me. tmm. wasm-module ): data ?? state ? cfg ?",
])
.de();
dbg!(path.display());
}
#[test]
fn test_proj_dir_question_mark() {
let path = EnvPath::from([
"$project(me. tmm. store-demo): local-cfg ? runtime ?? ( me. tmm. wasm-module ): data ?? state ?? cfg",
])
.de();
dbg!(path.display());
let path2 = EnvPath::from(["
$proj (com . moz . ff ):runtimes ? data ?? state ??
(com . gg . cr): cfg ?? cache ?
(com . ms . eg): local-data ? data
"])
.de();
dbg!(path2.display());
}
#[test]
fn proj_env() {
let p = EnvPath::new(["$proj (org.x) : data ?? env * HOME"]);
dbg!(p.display());
}
#[test]
fn remix_proj_and_env() {
let p = EnvPath::new(["$env: user ?? proj * (com.xy.z):cfg ? HOME"]);
dbg!(p);
let p2 = EnvPath::new(["$proj * (org. a . b ): runtimes ? env * HOME"]);
dbg!(p2);
}
}