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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
//! What:
//! The filesystem identity recorded with every real `index.lock` a transaction creates,
//! spelled exactly as the incumbent's `@monochromatic-dev/module-fs-id` spells it.
//! Why:
//! Recovery removes a real `index.lock` only when its device,
//! inode and filesystem
//! identity all match the journal;
//! the incumbent compares the recorded `fsId` with the one
//! it resolves now,
//! so both wrappers must resolve the same text for the same filesystem
//! (`package/module/fs-id/src/platform-resolvers.ts`,
//! `parsers.ts`).
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const { value } = await resolveFsId({ path: lockPath, emitDiagnostics: false });
//! ```
/// Trimming with the incumbent's `.trim()` semantics.
use super::js_text::trim_javascript;
/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
use std::path::{Path, PathBuf};
/// Longest accepted platform payload.
const MAX_PAYLOAD_LENGTH: usize = 512;
/// Characters a normalized payload may hold.
const SAFE_PAYLOAD_CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyz0123456789-.";
/// Characters a macOS device-node path may hold.
const SAFE_DEVICE_CHARACTERS: &str =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/_-.";
/// What:
/// The mechanism that supplied an identity,
/// which names its prefix.
/// Why:
/// `SOURCE_PREFIXES` in the incumbent:
/// stable sources and degraded fallbacks are told
/// apart in the text itself.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type FsIdSource = 'fs-uuid' | 'volume-uuid' | 'volume-serial' | 'f-fsid' | 'device-number';
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FsIdSource {
/// A Linux filesystem UUID from `findmnt`.
FsUuid,
/// A macOS Volume UUID from `diskutil`.
VolumeUuid,
/// A Windows volume serial number.
VolumeSerial,
/// A Linux `f_fsid` from `stat --file-system`.
FFsid,
/// A device number.
DeviceNumber,
}
/// What:
/// The prefix of one source.
/// Why:
/// The identity text is `<prefix><payload>`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// SOURCE_PREFIXES[source]
/// ```
pub fn source_prefix(source: FsIdSource) -> &'static str {
match source {
FsIdSource::FsUuid => return "fs-uuid_",
FsIdSource::VolumeUuid => return "volume-uuid_",
FsIdSource::VolumeSerial => return "volume-serial_",
FsIdSource::FFsid => return "f-fsid_",
FsIdSource::DeviceNumber => return "device-number_",
}
}
/// What:
/// Normalize one command-supplied payload:
/// trim,
/// lowercase,
/// then accept only non-empty
/// safe ASCII up to 512 characters that is not a lone `-`.
/// Why:
/// `normalizeIdentityPayload`;
/// anything else is no identity.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function normalizeIdentityPayload(value: string): string; // throws when unsafe
/// ```
pub fn normalize_identity_payload(value: &str) -> Option<String> {
let normalized: String = trim_javascript(value).to_lowercase();
// `chars().count()` counts characters, as JavaScript's `length` counts code units; both
// are equal for the ASCII text that alone can pass.
let length: usize = normalized.chars().count();
if length == 0
|| length > MAX_PAYLOAD_LENGTH
|| normalized == "-"
|| trim_javascript(normalized.as_str()) != normalized
{
return None;
}
for character in normalized.chars() {
if !SAFE_PAYLOAD_CHARACTERS.contains(character) {
return None;
}
}
return Some(normalized);
}
/// What:
/// The identity text `<prefix><payload>` for a normalized payload.
/// Why:
/// `createFsId`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// createFsId({ source, payload })
/// ```
pub fn create_fs_id(source: FsIdSource, payload: &str) -> Option<String> {
return Some(format!(
"{}{}",
source_prefix(source),
normalize_identity_payload(payload)?
));
}
/// What:
/// The mounted device node in POSIX `df -P` output:
/// the first field of the last
/// non-empty row that starts with `/dev/` and holds only safe characters.
/// Why:
/// `parseDfDevice` on macOS.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function parseDfDevice(output: string): string; // throws when none
/// ```
pub fn parse_df_device(output: &str) -> Option<String> {
let mut found: Option<String> = None;
for line in output.split('\n') {
let trimmed: &str = trim_javascript(line);
if trimmed.is_empty() {
continue;
}
let end: usize = trimmed.find([' ', '\t']).unwrap_or(trimmed.len());
let device: &str = &trimmed[..end];
if device.starts_with("/dev/") && device.chars().all(is_safe_device_character) {
// A later row wins: the incumbent scans the rows in reverse and takes the first.
found = Some(String::from(device));
}
}
return found;
}
/// What:
/// Whether a character may appear in a device-node path.
/// Why:
/// A named predicate keeps the scan free of closures.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// SAFE_DEVICE_CHARACTERS.includes(character)
/// ```
fn is_safe_device_character(character: char) -> bool {
return SAFE_DEVICE_CHARACTERS.contains(character);
}
/// What:
/// The `VolumeUUID` string of a `diskutil info -plist` property list,
/// normalized.
/// Why:
/// `parseDiskutilVolumeUuid` on macOS.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function parseDiskutilVolumeUuid(output: string): string; // throws when absent
/// ```
pub fn parse_diskutil_volume_uuid(output: &str) -> Option<String> {
let key: &str = "<key>VolumeUUID</key>";
let open: &str = "<string>";
let close: &str = "</string>";
let key_index: usize = output.find(key)?;
let open_index: usize = key_index + key.len() + output[key_index + key.len()..].find(open)?;
let value_start: usize = open_index + open.len();
let value_end: usize = value_start + output[value_start..].find(close)?;
return normalize_identity_payload(&output[value_start..value_end]);
}
/// What:
/// The uppercase drive root `X:\` of a Windows path,
/// or nothing for a path without one.
/// Why:
/// `windowsDriveRoot`:
/// the drive letter feeds a fixed PowerShell query.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function windowsDriveRoot(path: string): string; // throws without a drive root
/// ```
pub fn windows_drive_root(path: &str) -> Option<String> {
let mut characters: std::str::Chars<'_> = path.chars();
let letter: char = characters.next()?.to_ascii_lowercase();
let colon: char = characters.next()?;
let separator: char = characters.next()?;
if !letter.is_ascii_lowercase() || colon != ':' || (separator != '\\' && separator != '/') {
return None;
}
return Some(format!("{}:\\", letter.to_ascii_uppercase()));
}
/// What:
/// Run one platform command and return its standard output as text,
/// or nothing when it
/// cannot start or exits unsuccessfully.
/// Why:
/// The incumbent's `nano-spawn` call throws on either;
/// both send it to the fallback.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const { stdout } = await nanoSpawn(command, args);
/// ```
fn command_output(command: &str, arguments: &[&std::ffi::OsStr]) -> Option<String> {
let output: std::process::Output = std::process::Command::new(command)
.args(arguments)
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
return Some(String::from_utf8_lossy(output.stdout.as_slice()).into_owned());
}
/// What:
/// The Linux identity:
/// the filesystem UUID from `findmnt`,
/// or the degraded `f_fsid`
/// from GNU `stat`.
/// Why:
/// `resolveLinuxFsId`,
/// step for step.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function resolveLinuxFsId({ path }): Promise<FsIdResolution>;
/// ```
#[cfg_attr(
not(any(target_os = "linux", test)),
allow(dead_code, reason = "Linux only in the release build")
)]
fn linux_fs_id(path: &Path) -> Option<String> {
let target: &std::ffi::OsStr = path.as_os_str();
if let Some(output) = command_output(
"findmnt",
&[
std::ffi::OsStr::new("--target"),
target,
std::ffi::OsStr::new("--output=UUID"),
std::ffi::OsStr::new("--noheadings"),
],
) && let Some(identity) = create_fs_id(FsIdSource::FsUuid, output.as_str())
{
return Some(identity);
}
let fallback: String = command_output(
"stat",
&[
std::ffi::OsStr::new("--file-system"),
std::ffi::OsStr::new("--format=%i"),
target,
],
)?;
return create_fs_id(FsIdSource::FFsid, fallback.as_str());
}
/// What:
/// The filesystem identity of the filesystem holding `path`,
/// or nothing when no
/// mechanism answers.
/// Why:
/// The incumbent canonicalizes the path first and then asks the platform's mechanism;
/// on macOS and Windows the commands are the incumbent's own.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// (await resolveFsId({ path })).value
/// ```
pub fn filesystem_identity(path: &Path) -> Option<String> {
let canonical: PathBuf = std::fs::canonicalize(path).ok()?;
#[cfg(target_os = "linux")]
{
return linux_fs_id(canonical.as_path());
}
#[cfg(target_os = "macos")]
{
return darwin_fs_id(canonical.as_path());
}
#[cfg(windows)]
{
return windows_fs_id(canonical.as_path());
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
{
let _ = canonical;
return None;
}
}
/// What:
/// The macOS identity:
/// the Volume UUID through `df -P` and `diskutil info -plist`,
/// or the
/// degraded device number from BSD `stat -f %d`.
/// Why:
/// `resolveDarwinFsId`,
/// step for step.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function resolveDarwinFsId({ path }): Promise<FsIdResolution>;
/// ```
#[cfg(target_os = "macos")]
fn darwin_fs_id(path: &Path) -> Option<String> {
let target: &std::ffi::OsStr = path.as_os_str();
if let Some(mounts) = command_output("df", &[std::ffi::OsStr::new("-P"), target])
&& let Some(device) = parse_df_device(mounts.as_str())
&& let Some(plist) = command_output(
"diskutil",
&[
std::ffi::OsStr::new("info"),
std::ffi::OsStr::new("-plist"),
std::ffi::OsStr::new(device.as_str()),
],
)
&& let Some(uuid) = parse_diskutil_volume_uuid(plist.as_str())
{
return Some(format!("{}{uuid}", source_prefix(FsIdSource::VolumeUuid)));
}
let number: String = command_output(
"stat",
&[
std::ffi::OsStr::new("-f"),
std::ffi::OsStr::new("%d"),
target,
],
)?;
return create_fs_id(FsIdSource::DeviceNumber, number.as_str());
}
/// What:
/// The Windows identity:
/// the volume serial number through PowerShell.
/// Why:
/// `resolveWindowsFsId`'s preferred step;
/// its device-number fallback is not ported
/// (recorded in `doc/handover/cli-git-native-transactions.md`).
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function resolveWindowsFsId({ path }): Promise<FsIdResolution>;
/// ```
#[cfg(windows)]
fn windows_fs_id(path: &Path) -> Option<String> {
let text: String = path.to_string_lossy().into_owned();
if let Some(root) = windows_drive_root(text.as_str()) {
let query: String = format!(
"(Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='{}'\").VolumeSerialNumber",
&root[..2]
);
if let Some(output) = command_output(
"powershell.exe",
&[
std::ffi::OsStr::new("-NoLogo"),
std::ffi::OsStr::new("-NoProfile"),
std::ffi::OsStr::new("-NonInteractive"),
std::ffi::OsStr::new("-Command"),
std::ffi::OsStr::new(query.as_str()),
],
) && let Some(identity) = create_fs_id(FsIdSource::VolumeSerial, output.as_str())
{
return Some(identity);
}
}
// The incumbent falls back to Node's device number, the volume serial number that only an
// unstable standard-library interface exposes; without it no identity is recorded, and the
// landing that needs one stops instead of recording a different spelling.
return None;
}
/// Parser controls and the Linux resolver stay out of the release executable.
#[cfg(test)]
#[path = "filesystem_identity_tests.rs"]
mod tests;