@react-native-gpui/fs 0.0.0-stage → 0.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +36 -2
- package/desktop/Cargo.lock +1989 -0
- package/desktop/Cargo.toml +25 -0
- package/desktop/build.rs +3 -0
- package/desktop/src/dialogs.rs +394 -0
- package/desktop/src/dirs.rs +363 -0
- package/desktop/src/errors.rs +149 -0
- package/desktop/src/files.rs +640 -0
- package/desktop/src/handles.rs +697 -0
- package/desktop/src/lib.rs +258 -0
- package/desktop/src/links.rs +336 -0
- package/desktop/src/module.rs +187 -0
- package/desktop/src/paths.rs +231 -0
- package/desktop/src/watching.rs +392 -0
- package/package.json +37 -4
- package/react-native.config.js +9 -0
- package/src/NativeFs.ts +162 -0
- package/src/constants.ts +43 -0
- package/src/index.ts +769 -0
- package/src/promises.ts +771 -0
- package/src/shared.ts +539 -0
- package/src/stats.ts +196 -0
- package/src/watch.ts +141 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//! Shared state and helpers for the file system module.
|
|
2
|
+
//!
|
|
3
|
+
//! The trait's methods are split across sibling files by concern — `files`,
|
|
4
|
+
//! `dirs`, `handles`, `paths`, `dialogs`, `watching` — because they are large and
|
|
5
|
+
//! largely independent. Rust allows the trait to be implemented in several files
|
|
6
|
+
//! of one crate, so each file owns its own `impl` block.
|
|
7
|
+
//!
|
|
8
|
+
//! Everything they share is here: the open file table, the flag translation, and
|
|
9
|
+
//! the conversions the bridge's `f64` arguments need.
|
|
10
|
+
|
|
11
|
+
use std::fs::File;
|
|
12
|
+
use std::sync::atomic::AtomicU32;
|
|
13
|
+
use std::sync::Mutex;
|
|
14
|
+
|
|
15
|
+
use rn_gpui_module::{ModuleError, ModuleThread};
|
|
16
|
+
|
|
17
|
+
use crate::NativeFsEvents;
|
|
18
|
+
|
|
19
|
+
use crate::errors;
|
|
20
|
+
|
|
21
|
+
/// Flags, as `node:fs` numbers them: Node's table, not the platform's, so these
|
|
22
|
+
/// are translated rather than passed through to `OpenOptions`.
|
|
23
|
+
pub mod flags {
|
|
24
|
+
pub const RDONLY: i32 = 0;
|
|
25
|
+
pub const WRONLY: i32 = 1;
|
|
26
|
+
pub const RDWR: i32 = 2;
|
|
27
|
+
pub const CREAT: i32 = 64;
|
|
28
|
+
pub const EXCL: i32 = 128;
|
|
29
|
+
pub const TRUNC: i32 = 512;
|
|
30
|
+
pub const APPEND: i32 = 1024;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Access modes for `access`, matching `node:fs.constants`.
|
|
34
|
+
pub mod access {
|
|
35
|
+
pub const F_OK: i32 = 0;
|
|
36
|
+
pub const X_OK: i32 = 1;
|
|
37
|
+
pub const W_OK: i32 = 2;
|
|
38
|
+
pub const R_OK: i32 = 4;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// The kind numbers the JS layer turns into `Stats` predicates.
|
|
42
|
+
pub mod kind {
|
|
43
|
+
pub const FILE: f64 = 0.0;
|
|
44
|
+
pub const DIRECTORY: f64 = 1.0;
|
|
45
|
+
pub const SYMLINK: f64 = 2.0;
|
|
46
|
+
pub const OTHER: f64 = 3.0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub struct RNFsModule {
|
|
50
|
+
pub events: NativeFsEvents,
|
|
51
|
+
/// Open files, keyed by the number JS holds. The table belongs to the
|
|
52
|
+
/// instance, so a reload closes every descriptor when it drops.
|
|
53
|
+
///
|
|
54
|
+
/// Each entry records whether the descriptor was opened writable. The OS
|
|
55
|
+
/// reports a write to a read-only handle as `EBADF` on Unix and `EACCES` on
|
|
56
|
+
/// Windows, and Node reports `EBADF` on both, so the answer is taken from what
|
|
57
|
+
/// the handle was opened with rather than from the errno.
|
|
58
|
+
pub files: Mutex<std::collections::HashMap<u32, (File, bool)>>,
|
|
59
|
+
/// Never reused, so a stale `fd` from JS fails instead of closing an
|
|
60
|
+
/// unrelated file.
|
|
61
|
+
pub next_fd: AtomicU32,
|
|
62
|
+
/// Live watchers, keyed the same way.
|
|
63
|
+
pub watchers: Mutex<std::collections::HashMap<u32, crate::watching::Watcher>>,
|
|
64
|
+
pub next_watcher: AtomicU32,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
impl RNFsModule {
|
|
68
|
+
pub fn create(events: NativeFsEvents) -> Self {
|
|
69
|
+
Self {
|
|
70
|
+
events,
|
|
71
|
+
files: Mutex::new(std::collections::HashMap::new()),
|
|
72
|
+
next_fd: AtomicU32::new(1),
|
|
73
|
+
watchers: Mutex::new(std::collections::HashMap::new()),
|
|
74
|
+
next_watcher: AtomicU32::new(1),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub fn take_fd(&self) -> u32 {
|
|
79
|
+
self.next_fd
|
|
80
|
+
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub fn take_watcher_id(&self) -> u32 {
|
|
84
|
+
self.next_watcher
|
|
85
|
+
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// A clone of an open file, sharing its offset — which is what a descriptor
|
|
89
|
+
/// is. Holding the lock only for the lookup keeps a slow write from blocking
|
|
90
|
+
/// every other call on this thread.
|
|
91
|
+
pub fn file(&self, fd: f64) -> Result<File, ModuleError> {
|
|
92
|
+
self.file_and_writability(fd).map(|(file, _)| file)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// The handle and whether it can be written to.
|
|
96
|
+
pub fn file_and_writability(&self, fd: f64) -> Result<(File, bool), ModuleError> {
|
|
97
|
+
let id = descriptor(fd)?;
|
|
98
|
+
let files = self.files.lock().unwrap_or_else(|e| e.into_inner());
|
|
99
|
+
let (file, writable) = files.get(&id).ok_or_else(|| bad_fd(id))?;
|
|
100
|
+
let cloned = file
|
|
101
|
+
.try_clone()
|
|
102
|
+
.map_err(|error| errors::from_io(error, &id.to_string()))?;
|
|
103
|
+
Ok((cloned, *writable))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// The error Node raises for a descriptor it does not know.
|
|
108
|
+
pub fn bad_fd(id: u32) -> ModuleError {
|
|
109
|
+
errors::invalid("EBADF", format!("EBADF: bad file descriptor, {id}"))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// A descriptor from the bridge. Anything but a non-negative integer is a
|
|
113
|
+
/// programmer error, so it reports as a bad descriptor rather than a file error.
|
|
114
|
+
pub fn descriptor(value: f64) -> Result<u32, ModuleError> {
|
|
115
|
+
if value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= u32::MAX as f64 {
|
|
116
|
+
Ok(value as u32)
|
|
117
|
+
} else {
|
|
118
|
+
Err(bad_fd(0))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// A byte length from the bridge.
|
|
123
|
+
pub fn length(value: f64) -> Result<usize, ModuleError> {
|
|
124
|
+
if value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= u32::MAX as f64 {
|
|
125
|
+
Ok(value as usize)
|
|
126
|
+
} else {
|
|
127
|
+
Err(errors::invalid(
|
|
128
|
+
"EINVAL",
|
|
129
|
+
format!("EINVAL: invalid argument, length {value}"),
|
|
130
|
+
))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// A permission bitmask, clamped to the bits `std::fs` accepts. Zero means the
|
|
135
|
+
/// caller passed nothing, which Node treats as `0o666`.
|
|
136
|
+
pub fn mode(value: f64) -> u32 {
|
|
137
|
+
if value.is_finite() && value > 0.0 {
|
|
138
|
+
value as u32 & 0o7777
|
|
139
|
+
} else {
|
|
140
|
+
0o666
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// `node:fs` numeric flags as `OpenOptions`.
|
|
145
|
+
pub fn open_options(flags_value: f64, mode: u32) -> std::fs::OpenOptions {
|
|
146
|
+
let flags_value = flags_value as i32;
|
|
147
|
+
let mut options = std::fs::OpenOptions::new();
|
|
148
|
+
// Bit tests, not `matches!`: `matches!(value, A | B)` is the or-pattern
|
|
149
|
+
// `value == A || value == B`, which for Node's flags asks whether the whole
|
|
150
|
+
// mask equals 0 or 2. `'w'` is `WRONLY | CREAT | TRUNC` (577), so that test
|
|
151
|
+
// was false and every write opened read-only and failed `EINVAL`.
|
|
152
|
+
// `RDONLY` is zero, so "read" is the absence of `WRONLY` rather than a bit.
|
|
153
|
+
options
|
|
154
|
+
.read(flags_value & flags::WRONLY == 0)
|
|
155
|
+
.write(flags_value & (flags::WRONLY | flags::RDWR) != 0)
|
|
156
|
+
.create(flags_value & flags::CREAT != 0)
|
|
157
|
+
.create_new(flags_value & flags::EXCL != 0)
|
|
158
|
+
.truncate(flags_value & flags::TRUNC != 0)
|
|
159
|
+
.append(flags_value & flags::APPEND != 0);
|
|
160
|
+
#[cfg(unix)]
|
|
161
|
+
{
|
|
162
|
+
use std::os::unix::fs::OpenOptionsExt as _;
|
|
163
|
+
options.mode(mode);
|
|
164
|
+
}
|
|
165
|
+
#[cfg(not(unix))]
|
|
166
|
+
let _ = mode;
|
|
167
|
+
options
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Milliseconds since the epoch, as the bridge sends them.
|
|
171
|
+
pub fn system_time(ms: f64) -> std::time::SystemTime {
|
|
172
|
+
if ms.is_finite() && ms >= 0.0 {
|
|
173
|
+
std::time::UNIX_EPOCH + std::time::Duration::from_millis(ms as u64)
|
|
174
|
+
} else {
|
|
175
|
+
std::time::UNIX_EPOCH
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// A `SystemTime` as milliseconds, which is what `StatResult` carries.
|
|
180
|
+
pub fn millis(time: std::time::SystemTime) -> f64 {
|
|
181
|
+
time.duration_since(std::time::UNIX_EPOCH)
|
|
182
|
+
.map(|elapsed| elapsed.as_millis() as f64)
|
|
183
|
+
.unwrap_or(0.0)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The spec says `THREAD = Own`; this keeps the module compiled against that.
|
|
187
|
+
const _: ModuleThread = ModuleThread::Own;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
//! The well-known folders, as one `KnownPaths` object.
|
|
2
|
+
//!
|
|
3
|
+
//! `dirs` answers for the folders every platform has. The per-application three
|
|
4
|
+
//! — `appData`, `appCache`, `appLogs` — are this module's own convention, because
|
|
5
|
+
//! the host does not yet tell modules which app they belong to; see
|
|
6
|
+
//! `app_id` below. All three are created on the first read, as the design asks,
|
|
7
|
+
//! so a caller can write a log file without a `mkdir` first.
|
|
8
|
+
|
|
9
|
+
use std::path::{Path, PathBuf};
|
|
10
|
+
|
|
11
|
+
use rn_gpui_module::ModuleError;
|
|
12
|
+
|
|
13
|
+
use crate::KnownPaths;
|
|
14
|
+
|
|
15
|
+
/// The public folders plus the three per-application ones, created if needed.
|
|
16
|
+
///
|
|
17
|
+
/// Nothing here fails: a folder that does not exist is reported as the temp
|
|
18
|
+
/// directory instead. The design treats these as conveniences, and a caller
|
|
19
|
+
/// asking where to put a log file is better served by a working stand-in than by
|
|
20
|
+
/// a thrown error it cannot recover from — the alternative is a crash on a
|
|
21
|
+
/// system where, say, `Desktop` was never created.
|
|
22
|
+
pub fn known_paths() -> Result<KnownPaths, ModuleError> {
|
|
23
|
+
let temp = std::env::temp_dir();
|
|
24
|
+
let id = app_id();
|
|
25
|
+
|
|
26
|
+
let app_data = dirs::data_dir().map(|dir| dir.join(&id));
|
|
27
|
+
let app_cache = dirs::cache_dir().map(|dir| dir.join(&id));
|
|
28
|
+
let app_logs = logs_dir(&id);
|
|
29
|
+
|
|
30
|
+
Ok(KnownPaths {
|
|
31
|
+
home: fallback(dirs::home_dir(), &temp),
|
|
32
|
+
documents: fallback(dirs::document_dir(), &temp),
|
|
33
|
+
downloads: fallback(dirs::download_dir(), &temp),
|
|
34
|
+
desktop: fallback(dirs::desktop_dir(), &temp),
|
|
35
|
+
temp: path_string(&temp),
|
|
36
|
+
app_data: ensure_directory(app_data, &temp),
|
|
37
|
+
app_cache: ensure_directory(app_cache, &temp),
|
|
38
|
+
app_logs: ensure_directory(app_logs, &temp),
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// A folder as a string, with the temp directory standing in when the platform
|
|
43
|
+
/// cannot name it.
|
|
44
|
+
fn fallback(found: Option<PathBuf>, temp: &Path) -> String {
|
|
45
|
+
path_string(found.as_deref().unwrap_or(temp))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// A path as `KnownPaths` carries it. A non-UTF-8 path cannot be a JS string, so
|
|
49
|
+
/// the lossy form is used rather than failing: it is still a path the platform
|
|
50
|
+
/// will resolve, and the folder is only a suggestion to the caller.
|
|
51
|
+
fn path_string(path: &Path) -> String {
|
|
52
|
+
path.to_string_lossy().into_owned()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Creates a directory that the returned path promises exists, falling back to
|
|
56
|
+
/// the temp directory if it cannot be created.
|
|
57
|
+
///
|
|
58
|
+
/// A failure here means the platform gave no base directory at all, or that it
|
|
59
|
+
/// exists but is not writable; temp is the one directory a process can normally
|
|
60
|
+
/// count on, so the caller still gets somewhere to write.
|
|
61
|
+
fn ensure_directory(found: Option<PathBuf>, temp: &Path) -> String {
|
|
62
|
+
let Some(directory) = found else {
|
|
63
|
+
return path_string(temp);
|
|
64
|
+
};
|
|
65
|
+
match std::fs::create_dir_all(&directory) {
|
|
66
|
+
Ok(()) => path_string(&directory),
|
|
67
|
+
Err(_) => path_string(temp),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Where `appLogs` lives, which is the one app folder whose convention differs
|
|
72
|
+
/// by platform: macOS keeps logs under `~/Library/Logs`, the others under the
|
|
73
|
+
/// data directory. Both get the app id as their last component so two apps do
|
|
74
|
+
/// not share a log folder.
|
|
75
|
+
fn logs_dir(id: &str) -> Option<PathBuf> {
|
|
76
|
+
#[cfg(target_os = "macos")]
|
|
77
|
+
{
|
|
78
|
+
dirs::home_dir().map(|home| home.join("Library").join("Logs").join(id))
|
|
79
|
+
}
|
|
80
|
+
#[cfg(not(target_os = "macos"))]
|
|
81
|
+
{
|
|
82
|
+
dirs::data_dir().map(|dir| dir.join(id).join("logs"))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// The id the three per-app folders are namespaced by.
|
|
87
|
+
///
|
|
88
|
+
/// This is a **placeholder**. The design wants these paths to be per-app and
|
|
89
|
+
/// says the host already knows the id (`DesktopAppConfig::app_id`), but no host
|
|
90
|
+
/// function exposes it to modules yet: `host_info().app_id` does not exist. Until
|
|
91
|
+
/// that API lands, `RN_GPUI_APP_ID` is honoured when the app sets it, and the
|
|
92
|
+
/// executable's file stem is the fallback — stable for a given binary, which is
|
|
93
|
+
/// enough to keep the folders apart and across runs.
|
|
94
|
+
///
|
|
95
|
+
/// When `host_info().app_id` is added, this function should read it and the two
|
|
96
|
+
/// fallbacks should go, because a renamed binary would otherwise move the app's
|
|
97
|
+
/// data and cache out from under the user.
|
|
98
|
+
fn app_id() -> String {
|
|
99
|
+
let from_environment = std::env::var("RN_GPUI_APP_ID").ok();
|
|
100
|
+
let from_executable = std::env::current_exe().ok().and_then(|exe| {
|
|
101
|
+
exe.file_stem()
|
|
102
|
+
.map(|stem| stem.to_string_lossy().into_owned())
|
|
103
|
+
});
|
|
104
|
+
pick_app_id(from_environment.as_deref(), from_executable.as_deref())
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// The selection half of `app_id`, split out so the precedence and the fallback
|
|
108
|
+
/// can be tested without touching the environment, which other tests share.
|
|
109
|
+
fn pick_app_id(from_environment: Option<&str>, from_executable: Option<&str>) -> String {
|
|
110
|
+
[from_environment, from_executable]
|
|
111
|
+
.into_iter()
|
|
112
|
+
.flatten()
|
|
113
|
+
.map(sanitise)
|
|
114
|
+
.find(|id| !id.is_empty())
|
|
115
|
+
// A process whose executable has no usable name still needs folders, and
|
|
116
|
+
// the name only has to be stable, not accurate.
|
|
117
|
+
.unwrap_or_else(|| "rn-gpui-app".to_string())
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Strips the characters that cannot appear in a directory name on any of the
|
|
121
|
+
/// three platforms, so an id from the environment cannot escape its parent
|
|
122
|
+
/// directory or fail `create_dir_all` on Windows.
|
|
123
|
+
///
|
|
124
|
+
/// Leading and trailing dots and dashes go too: an id that was only separators
|
|
125
|
+
/// sanitises to nothing rather than to a directory named `-`, and one that began
|
|
126
|
+
/// with `..` cannot end up looking like a parent reference.
|
|
127
|
+
fn sanitise(id: &str) -> String {
|
|
128
|
+
id.trim()
|
|
129
|
+
.chars()
|
|
130
|
+
.map(|character| match character {
|
|
131
|
+
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0' => '-',
|
|
132
|
+
other => other,
|
|
133
|
+
})
|
|
134
|
+
.collect::<String>()
|
|
135
|
+
.trim_matches(|character| character == '.' || character == '-')
|
|
136
|
+
.to_string()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[cfg(test)]
|
|
140
|
+
mod tests {
|
|
141
|
+
use super::*;
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn every_known_path_is_a_non_empty_string() {
|
|
145
|
+
let paths = known_paths().expect("known paths");
|
|
146
|
+
for (name, value) in [
|
|
147
|
+
("home", &paths.home),
|
|
148
|
+
("documents", &paths.documents),
|
|
149
|
+
("downloads", &paths.downloads),
|
|
150
|
+
("desktop", &paths.desktop),
|
|
151
|
+
("temp", &paths.temp),
|
|
152
|
+
("appData", &paths.app_data),
|
|
153
|
+
("appCache", &paths.app_cache),
|
|
154
|
+
("appLogs", &paths.app_logs),
|
|
155
|
+
] {
|
|
156
|
+
assert!(!value.is_empty(), "{name} was empty");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[test]
|
|
161
|
+
fn the_app_folders_exist_after_the_first_read() {
|
|
162
|
+
let paths = known_paths().expect("known paths");
|
|
163
|
+
for (name, value) in [
|
|
164
|
+
("appData", &paths.app_data),
|
|
165
|
+
("appCache", &paths.app_cache),
|
|
166
|
+
("appLogs", &paths.app_logs),
|
|
167
|
+
] {
|
|
168
|
+
assert!(
|
|
169
|
+
Path::new(value).is_dir(),
|
|
170
|
+
"{name} was not created at {value}"
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[test]
|
|
176
|
+
fn the_app_folders_are_distinct() {
|
|
177
|
+
let paths = known_paths().expect("known paths");
|
|
178
|
+
let app_data = Path::new(&paths.app_data);
|
|
179
|
+
let app_cache = Path::new(&paths.app_cache);
|
|
180
|
+
let app_logs = Path::new(&paths.app_logs);
|
|
181
|
+
|
|
182
|
+
assert_ne!(app_data, app_cache);
|
|
183
|
+
assert_ne!(app_data, app_logs);
|
|
184
|
+
assert_ne!(app_cache, app_logs);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[test]
|
|
188
|
+
fn a_second_read_returns_the_same_folders() {
|
|
189
|
+
// Creating a directory that already exists has to keep succeeding, since
|
|
190
|
+
// the JS layer may call `getPaths()` more than once per run.
|
|
191
|
+
let first = known_paths().expect("known paths");
|
|
192
|
+
let second = known_paths().expect("known paths");
|
|
193
|
+
assert_eq!(first.app_data, second.app_data);
|
|
194
|
+
assert_eq!(first.app_cache, second.app_cache);
|
|
195
|
+
assert_eq!(first.app_logs, second.app_logs);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[test]
|
|
199
|
+
fn an_id_from_the_environment_names_the_app_folders() {
|
|
200
|
+
// Only the pure part is asserted: mutating the environment would race
|
|
201
|
+
// with the other tests in this module, which read it concurrently.
|
|
202
|
+
assert_eq!(sanitise("my-app"), "my-app");
|
|
203
|
+
assert_eq!(sanitise(" ../escape "), "escape");
|
|
204
|
+
assert_eq!(sanitise("a/b\\c"), "a-b-c");
|
|
205
|
+
assert_eq!(sanitise(" "), "");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#[test]
|
|
209
|
+
fn the_environment_id_wins_over_the_executable_name() {
|
|
210
|
+
assert_eq!(pick_app_id(Some("from-env"), Some("from-exe")), "from-env");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[test]
|
|
214
|
+
fn an_unusable_environment_id_falls_through_to_the_executable_name() {
|
|
215
|
+
assert_eq!(pick_app_id(Some(" "), Some("from-exe")), "from-exe");
|
|
216
|
+
// A separator-only id would escape its parent, which `sanitise` turns
|
|
217
|
+
// into a name rather than a path.
|
|
218
|
+
assert_eq!(pick_app_id(Some("/"), Some("from-exe")), "from-exe");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#[test]
|
|
222
|
+
fn no_id_at_all_still_yields_a_stable_name() {
|
|
223
|
+
assert_eq!(pick_app_id(None, None), "rn-gpui-app");
|
|
224
|
+
assert_eq!(pick_app_id(Some(""), Some("")), "rn-gpui-app");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn a_hostile_executable_name_cannot_escape_the_base_directory() {
|
|
229
|
+
assert_eq!(pick_app_id(None, Some("../../etc/passwd")), "etc-passwd");
|
|
230
|
+
}
|
|
231
|
+
}
|