@photostructure/fs-metadata 1.0.1 → 1.2.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/CLAUDE.md +13 -0
  3. package/binding.gyp +1 -0
  4. package/claude.sh +29 -5
  5. package/dist/index.cjs +237 -129
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +55 -3
  8. package/dist/index.d.mts +55 -3
  9. package/dist/index.d.ts +55 -3
  10. package/dist/index.mjs +236 -130
  11. package/dist/index.mjs.map +1 -1
  12. package/doc/SECURITY_AUDIT_2025.md +1 -1
  13. package/doc/SECURITY_AUDIT_2026.md +361 -0
  14. package/doc/TPP-GUIDE.md +144 -0
  15. package/doc/system-volume-detection.md +268 -0
  16. package/package.json +12 -12
  17. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  18. package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
  19. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  20. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  21. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  22. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  23. package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  24. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  25. package/src/binding.cpp +11 -0
  26. package/src/common/volume_metadata.h +10 -3
  27. package/src/common/volume_mount_points.h +7 -1
  28. package/src/darwin/da_mutex.h +23 -0
  29. package/src/darwin/get_mount_point.cpp +96 -0
  30. package/src/darwin/get_mount_point.h +13 -0
  31. package/src/darwin/raii_utils.h +39 -0
  32. package/src/darwin/system_volume.h +156 -0
  33. package/src/darwin/volume_metadata.cpp +18 -2
  34. package/src/darwin/volume_mount_points.cpp +46 -14
  35. package/src/index.ts +49 -0
  36. package/src/linux/mtab.ts +6 -0
  37. package/src/mount_point_for_path.ts +54 -0
  38. package/src/options.ts +7 -17
  39. package/src/path.ts +16 -1
  40. package/src/system_volume.ts +5 -9
  41. package/src/test-utils/assert.ts +4 -0
  42. package/src/types/mount_point.ts +28 -1
  43. package/src/types/native_bindings.ts +7 -0
  44. package/src/volume_metadata.ts +117 -2
  45. package/src/windows/system_volume.h +21 -16
  46. package/src/windows/volume_metadata.cpp +13 -7
  47. package/src/windows/volume_mount_points.cpp +11 -7
@@ -1,8 +1,12 @@
1
1
  // src/volume_metadata.ts
2
2
 
3
+ import type { Stats } from "node:fs";
4
+ import { realpath } from "node:fs/promises";
5
+ import { dirname } from "node:path";
3
6
  import { mapConcurrent, withTimeout } from "./async";
4
7
  import { debug } from "./debuglog";
5
8
  import { WrappedError } from "./error";
9
+ import { statAsync } from "./fs";
6
10
  import { getLabelFromDevDisk, getUuidFromDevDisk } from "./linux/dev_disk";
7
11
  import { getLinuxMtabMetadata } from "./linux/mount_points";
8
12
  import {
@@ -11,8 +15,8 @@ import {
11
15
  } from "./linux/mtab";
12
16
  import { compactValues } from "./object";
13
17
  import { IncludeSystemVolumesDefault, optionsWithDefaults } from "./options";
14
- import { normalizePath } from "./path";
15
- import { isLinux, isWindows } from "./platform";
18
+ import { isAncestorOrSelf, normalizePath } from "./path";
19
+ import { isLinux, isMacOS, isWindows } from "./platform";
16
20
  import { extractRemoteInfo, isRemoteFsType } from "./remote_info";
17
21
  import { isBlank, isNotBlank } from "./string";
18
22
  import { assignSystemVolume } from "./system_volume";
@@ -153,6 +157,117 @@ async function _getVolumeMetadata(
153
157
  return compactValues(result) as VolumeMetadata;
154
158
  }
155
159
 
160
+ /**
161
+ * Get volume metadata for an arbitrary file or directory path.
162
+ *
163
+ * Unlike {@link getVolumeMetadataImpl}, this accepts any path — not just mount
164
+ * points. It resolves symlinks and correctly handles macOS APFS firmlinks
165
+ * (e.g. `/Users` → `/System/Volumes/Data`), mirroring what `df` does.
166
+ *
167
+ * On macOS, the native `fstatfs()` call returns `f_mntonname` (the canonical
168
+ * mount point), exposed here as `mountName`. This is used to resolve firmlinks
169
+ * without `stat().dev`, which does NOT follow firmlinks.
170
+ *
171
+ * On Linux and Windows, `stat().dev` device IDs are reliable (no firmlinks),
172
+ * so mount point discovery uses device ID + path prefix matching.
173
+ */
174
+ export async function getVolumeMetadataForPathImpl(
175
+ pathname: string,
176
+ opts: Options,
177
+ nativeFn: NativeBindingsFn,
178
+ ): Promise<VolumeMetadata> {
179
+ if (isBlank(pathname)) {
180
+ throw new TypeError("Invalid pathname: got " + JSON.stringify(pathname));
181
+ }
182
+
183
+ // realpath() resolves POSIX symlinks. APFS firmlinks are NOT resolved by
184
+ // realpath(), but fstatfs() follows them — handled below.
185
+ const resolved = await realpath(pathname);
186
+
187
+ // getVolumeMetadataImpl requires a directory path, not a file.
188
+ const resolvedStat = await statAsync(resolved);
189
+ const dir = resolvedStat.isDirectory() ? resolved : dirname(resolved);
190
+
191
+ if (isMacOS) {
192
+ // On macOS, native fstatfs() sets mountName = f_mntonname, which is the
193
+ // canonical mount point even through APFS firmlinks. Probe the dir to get
194
+ // it, then re-query with the canonical mount point so the result has
195
+ // mountPoint set correctly.
196
+ const probe = await getVolumeMetadataImpl(
197
+ { ...opts, mountPoint: dir },
198
+ nativeFn,
199
+ );
200
+ const canonicalMountPoint = isNotBlank(probe.mountName)
201
+ ? probe.mountName
202
+ : dir;
203
+ if (canonicalMountPoint === dir) return probe;
204
+ return getVolumeMetadataImpl(
205
+ { ...opts, mountPoint: canonicalMountPoint },
206
+ nativeFn,
207
+ );
208
+ }
209
+
210
+ // Linux/Windows: stat().dev is reliable (no firmlinks). Find the mount point
211
+ // by comparing device IDs, using path prefix as a tiebreaker for bind mounts
212
+ // or GIO mounts that share the same device id.
213
+ const mountPoint = await findMountPointByDeviceId(
214
+ resolved,
215
+ resolvedStat,
216
+ opts,
217
+ nativeFn,
218
+ );
219
+
220
+ return getVolumeMetadataImpl({ ...opts, mountPoint }, nativeFn);
221
+ }
222
+
223
+ /**
224
+ * Find the mount point for a resolved path using device ID matching.
225
+ * Used on Linux and Windows where stat().dev is reliable (no firmlinks).
226
+ *
227
+ * Compares device IDs of mount points against the target path's device ID,
228
+ * using path prefix as a tiebreaker for bind mounts or GIO mounts that share
229
+ * the same device id. The longest prefix match wins.
230
+ */
231
+ export async function findMountPointByDeviceId(
232
+ resolved: string,
233
+ resolvedStat: Stats,
234
+ opts: Options,
235
+ nativeFn: NativeBindingsFn,
236
+ ): Promise<string> {
237
+ const targetDev = resolvedStat.dev;
238
+ const mountPoints = await getVolumeMountPointsImpl(
239
+ { ...opts, includeSystemVolumes: true },
240
+ nativeFn,
241
+ );
242
+
243
+ const prefixMatches: string[] = [];
244
+ const deviceMatches: string[] = [];
245
+
246
+ await Promise.all(
247
+ mountPoints.map(async ({ mountPoint }) => {
248
+ try {
249
+ const mpDev = (await statAsync(mountPoint)).dev;
250
+ if (mpDev !== targetDev) return;
251
+ if (isAncestorOrSelf(mountPoint, resolved)) {
252
+ prefixMatches.push(mountPoint);
253
+ } else {
254
+ deviceMatches.push(mountPoint);
255
+ }
256
+ } catch {
257
+ // skip inaccessible mount points
258
+ }
259
+ }),
260
+ );
261
+
262
+ const candidates = prefixMatches.length > 0 ? prefixMatches : deviceMatches;
263
+ if (candidates.length === 0) {
264
+ throw new Error(
265
+ "No mount point found for path: " + JSON.stringify(resolved),
266
+ );
267
+ }
268
+ return candidates.reduce((a, b) => (a.length >= b.length ? a : b));
269
+ }
270
+
156
271
  export async function getAllVolumeMetadataImpl(
157
272
  opts: Required<Options> & {
158
273
  includeSystemVolumes?: boolean;
@@ -18,7 +18,12 @@
18
18
 
19
19
  namespace FSMeta {
20
20
 
21
- inline bool IsSystemVolume(const std::wstring &drive) {
21
+ // Check if a drive is a system volume using SHGetFolderPathW (Windows dir)
22
+ // and optionally FILE_SUPPORTS_SYSTEM_PATHS/FILES volume flags.
23
+ //
24
+ // volumeFlags: if non-zero, uses pre-fetched flags to avoid a redundant
25
+ // GetVolumeInformationW call. Pass 0 to have this function query flags itself.
26
+ inline bool IsSystemVolume(const std::wstring &drive, DWORD volumeFlags = 0) {
22
27
  WCHAR systemRoot[MAX_PATH];
23
28
  if (SUCCEEDED(
24
29
  SHGetFolderPathW(nullptr, CSIDL_WINDOWS, nullptr, 0, systemRoot))) {
@@ -33,22 +38,22 @@ inline bool IsSystemVolume(const std::wstring &drive) {
33
38
  }
34
39
 
35
40
  // Modern volume properties check
36
- DWORD volumeFlags = 0;
37
- wchar_t fileSystemName[MAX_PATH + 1] = {0};
38
-
39
- if (GetVolumeInformationW(drive.c_str(), nullptr, 0, nullptr, nullptr,
40
- &volumeFlags, fileSystemName, MAX_PATH)) {
41
-
42
- // Check for modern system volume indicators
43
- if ((volumeFlags & FILE_SUPPORTS_SYSTEM_PATHS) ||
44
- (volumeFlags & FILE_SUPPORTS_SYSTEM_FILES)) {
45
- DEBUG_LOG("[IsSystemVolume] %ls has system volume flags (0x%08X)",
46
- drive.c_str(), volumeFlags);
47
- return true;
41
+ if (volumeFlags == 0) {
42
+ wchar_t fileSystemName[MAX_PATH + 1] = {0};
43
+ if (!GetVolumeInformationW(drive.c_str(), nullptr, 0, nullptr, nullptr,
44
+ &volumeFlags, fileSystemName, MAX_PATH)) {
45
+ DEBUG_LOG("[IsSystemVolume] %ls GetVolumeInformationW failed: %lu",
46
+ drive.c_str(), GetLastError());
47
+ DEBUG_LOG("[IsSystemVolume] %ls is not a system volume", drive.c_str());
48
+ return false;
48
49
  }
49
- } else {
50
- DEBUG_LOG("[IsSystemVolume] %ls GetVolumeInformationW failed: %lu",
51
- drive.c_str(), GetLastError());
50
+ }
51
+
52
+ if ((volumeFlags & FILE_SUPPORTS_SYSTEM_PATHS) ||
53
+ (volumeFlags & FILE_SUPPORTS_SYSTEM_FILES)) {
54
+ DEBUG_LOG("[IsSystemVolume] %ls has system volume flags (0x%08X)",
55
+ drive.c_str(), volumeFlags);
56
+ return true;
52
57
  }
53
58
 
54
59
  DEBUG_LOG("[IsSystemVolume] %ls is not a system volume", drive.c_str());
@@ -110,6 +110,7 @@ public:
110
110
  const char *getVolumeName() const { return volumeName; }
111
111
  const char *getFileSystem() const { return fstype; }
112
112
  DWORD getSerialNumber() const { return serialNumber; }
113
+ DWORD getFlags() const { return fsFlags; }
113
114
  };
114
115
 
115
116
  // RAII wrapper for disk space information
@@ -165,17 +166,13 @@ private:
165
166
  return; // Don't try to get additional info for non-healthy drives
166
167
  }
167
168
 
168
- std::wstring widePath = SecurityUtils::SafeStringToWide(mountPoint);
169
- metadata.isSystemVolume = IsSystemVolume(widePath.c_str());
170
-
171
- DEBUG_LOG("[GetVolumeMetadata] %s {isSystemVolume: %s}",
172
- mountPoint.c_str(), metadata.isSystemVolume ? "true" : "false");
173
-
174
- // Get volume information
169
+ // Get volume information first so we can reuse its flags for
170
+ // IsSystemVolume, avoiding a redundant GetVolumeInformationW call.
175
171
  VolumeInfo volInfo(mountPoint);
176
172
  if (volInfo.isValid()) {
177
173
  metadata.label = volInfo.getVolumeName();
178
174
  metadata.fstype = volInfo.getFileSystem();
175
+ metadata.isReadOnly = (volInfo.getFlags() & FILE_READ_ONLY_VOLUME) != 0;
179
176
  DEBUG_LOG("[GetVolumeMetadata] %s {label: %s, fstype: %s}",
180
177
  mountPoint.c_str(), metadata.label.c_str(),
181
178
  metadata.fstype.c_str());
@@ -207,6 +204,15 @@ private:
207
204
  }
208
205
  }
209
206
 
207
+ // Check system volume using pre-fetched flags from VolumeInfo to
208
+ // avoid a redundant GetVolumeInformationW call.
209
+ std::wstring widePath = SecurityUtils::SafeStringToWide(mountPoint);
210
+ metadata.isSystemVolume =
211
+ IsSystemVolume(widePath, volInfo.isValid() ? volInfo.getFlags() : 0);
212
+
213
+ DEBUG_LOG("[GetVolumeMetadata] %s {isSystemVolume: %s}",
214
+ mountPoint.c_str(), metadata.isSystemVolume ? "true" : "false");
215
+
210
216
  // Check if drive is remote
211
217
  metadata.remote = (GetDriveTypeA(mountPoint.c_str()) == DRIVE_REMOTE);
212
218
  DEBUG_LOG("[GetVolumeMetadata] %s {remote: %s}", mountPoint.c_str(),
@@ -84,21 +84,25 @@ public:
84
84
  mp.mountPoint = paths[i];
85
85
  mp.status = DriveStatusToString(statuses[i]);
86
86
 
87
+ std::wstring widePath = SecurityUtils::SafeStringToWide(paths[i]);
88
+ DWORD fsFlags = 0;
89
+
87
90
  if (statuses[i] == DriveStatus::Healthy) {
88
91
  WCHAR fsName[MAX_PATH + 1] = {0};
89
92
 
90
- if (GetVolumeInformationW(
91
- SecurityUtils::SafeStringToWide(paths[i]).c_str(), nullptr, 0,
92
- nullptr, nullptr, nullptr, fsName, MAX_PATH)) {
93
+ if (GetVolumeInformationW(widePath.c_str(), nullptr, 0, nullptr,
94
+ nullptr, &fsFlags, fsName, MAX_PATH)) {
93
95
  mp.fstype = WideToUtf8(fsName);
96
+ mp.isReadOnly = (fsFlags & FILE_READ_ONLY_VOLUME) != 0;
94
97
  DEBUG_LOG("[GetVolumeMountPoints] drive %s filesystem: %s",
95
98
  paths[i].c_str(), mp.fstype.c_str());
96
99
  }
97
- }
98
100
 
99
- // Check if this is a system volume
100
- mp.isSystemVolume =
101
- IsSystemVolume(SecurityUtils::SafeStringToWide(paths[i]));
101
+ // Only check system volume for healthy drives — IsSystemVolume
102
+ // may call GetVolumeInformationW, which hangs on dead drives and
103
+ // would defeat the async timeout protection from CheckDriveStatus.
104
+ mp.isSystemVolume = IsSystemVolume(widePath, fsFlags);
105
+ }
102
106
  mountPoints_.push_back(std::move(mp));
103
107
  }
104
108