@photostructure/fs-metadata 0.6.0 → 0.7.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 (98) hide show
  1. package/CHANGELOG.md +11 -6
  2. package/CLAUDE.md +160 -136
  3. package/CODE_OF_CONDUCT.md +11 -11
  4. package/CONTRIBUTING.md +2 -2
  5. package/README.md +34 -84
  6. package/binding.gyp +98 -23
  7. package/claude.sh +23 -0
  8. package/dist/index.cjs +53 -22
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +5 -0
  11. package/dist/index.d.mts +5 -0
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.mjs +52 -21
  14. package/dist/index.mjs.map +1 -1
  15. package/{C++_REVIEW_TODO.md → doc/C++_REVIEW_TODO.md} +97 -25
  16. package/doc/GPG_RELEASE_HOWTO.md +505 -0
  17. package/doc/MACOS_API_REFERENCE.md +469 -0
  18. package/doc/SECURITY_AUDIT_2025.md +809 -0
  19. package/doc/SSH_RELEASE_HOWTO.md +207 -0
  20. package/doc/WINDOWS_API_REFERENCE.md +422 -0
  21. package/doc/WINDOWS_ARM64_SECURITY.md +161 -0
  22. package/doc/WINDOWS_DEBUG_GUIDE.md +96 -0
  23. package/doc/examples.md +267 -0
  24. package/doc/gotchas.md +297 -0
  25. package/doc/logo.png +0 -0
  26. package/doc/logo.svg +85 -0
  27. package/doc/macos-asan-sip-issue.md +71 -0
  28. package/doc/social.png +0 -0
  29. package/doc/social.svg +125 -0
  30. package/doc/windows-build.md +226 -0
  31. package/doc/windows-clang-tidy.md +72 -0
  32. package/doc/windows-memory-testing.md +108 -0
  33. package/doc/windows-prebuildify-arm64.md +232 -0
  34. package/jest.config.cjs +24 -0
  35. package/package.json +68 -44
  36. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  37. package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
  38. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  39. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  40. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  41. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  42. package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  43. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  44. package/scripts/check-memory.ts +186 -0
  45. package/scripts/clang-tidy.ts +832 -0
  46. package/scripts/install.cjs +42 -0
  47. package/scripts/is-platform.mjs +1 -1
  48. package/scripts/macos-asan.sh +155 -0
  49. package/scripts/post-build.mjs +3 -3
  50. package/scripts/prebuild-linux-glibc.sh +119 -0
  51. package/scripts/prebuildify-wrapper.ts +77 -0
  52. package/scripts/precommit.ts +70 -0
  53. package/scripts/sanitizers-test.sh +7 -1
  54. package/scripts/{configure.mjs → setup-native.mjs} +4 -1
  55. package/src/binding.cpp +1 -1
  56. package/src/common/error_utils.h +0 -6
  57. package/src/common/volume_metadata.h +6 -0
  58. package/src/darwin/hidden.cpp +73 -25
  59. package/src/darwin/path_security.h +149 -0
  60. package/src/darwin/raii_utils.h +104 -4
  61. package/src/darwin/volume_metadata.cpp +132 -58
  62. package/src/darwin/volume_mount_points.cpp +80 -47
  63. package/src/hidden.ts +36 -13
  64. package/src/linux/gio_mount_points.cpp +17 -18
  65. package/src/linux/gio_utils.cpp +92 -37
  66. package/src/linux/gio_utils.h +11 -5
  67. package/src/linux/gio_volume_metadata.cpp +111 -48
  68. package/src/linux/volume_metadata.cpp +67 -4
  69. package/src/object.ts +1 -0
  70. package/src/options.ts +6 -0
  71. package/src/path.ts +11 -0
  72. package/src/platform.ts +25 -0
  73. package/src/remote_info.ts +5 -3
  74. package/src/stack_path.ts +8 -6
  75. package/src/string_enum.ts +1 -0
  76. package/src/test-utils/benchmark-harness.ts +192 -0
  77. package/src/test-utils/debuglog-child.ts +30 -2
  78. package/src/test-utils/debuglog-enabled-child.ts +38 -8
  79. package/src/test-utils/jest-setup.ts +14 -0
  80. package/src/test-utils/memory-test-core.ts +336 -0
  81. package/src/test-utils/memory-test-runner.ts +108 -0
  82. package/src/test-utils/platform.ts +46 -1
  83. package/src/test-utils/worker-thread-helper.cjs +157 -26
  84. package/src/types/native_bindings.ts +1 -1
  85. package/src/types/options.ts +6 -0
  86. package/src/windows/drive_status.h +133 -163
  87. package/src/windows/error_utils.h +54 -3
  88. package/src/windows/fs_meta.h +1 -1
  89. package/src/windows/hidden.cpp +60 -43
  90. package/src/windows/security_utils.h +250 -0
  91. package/src/windows/string.h +68 -11
  92. package/src/windows/system_volume.h +1 -1
  93. package/src/windows/thread_pool.h +206 -0
  94. package/src/windows/volume_metadata.cpp +11 -6
  95. package/src/windows/volume_mount_points.cpp +8 -7
  96. package/src/windows/windows_arch.h +39 -0
  97. package/scripts/check-memory.mjs +0 -123
  98. package/scripts/clang-tidy.mjs +0 -73
@@ -1,22 +1,27 @@
1
1
  // src/darwin/volume_metadata.cpp
2
+ // Thread-safe implementation with DiskArbitration mutex synchronization
2
3
 
3
4
  #include "../common/debug_log.h"
4
5
  #include "./fs_meta.h"
6
+ #include "./path_security.h"
5
7
  #include "./raii_utils.h"
6
8
 
7
9
  #include <CoreFoundation/CoreFoundation.h>
8
10
  #include <DiskArbitration/DiskArbitration.h>
9
- #include <IOKit/IOBSD.h>
10
- #include <IOKit/storage/IOMedia.h>
11
- #include <IOKit/storage/IOStorageProtocolCharacteristics.h>
11
+ #include <fcntl.h> // For open(), O_RDONLY, O_DIRECTORY
12
12
  #include <memory>
13
+ #include <mutex>
13
14
  #include <string>
14
15
  #include <sys/mount.h>
15
16
  #include <sys/param.h>
16
17
  #include <sys/statvfs.h>
18
+ #include <unistd.h> // For close()
17
19
 
18
20
  namespace FSMeta {
19
21
 
22
+ // Global mutex for DiskArbitration operations
23
+ static std::mutex g_diskArbitrationMutex;
24
+
20
25
  // Helper function to convert CFString to std::string
21
26
  static std::string CFStringToString(CFStringRef cfString) {
22
27
  if (!cfString) {
@@ -24,16 +29,43 @@ static std::string CFStringToString(CFStringRef cfString) {
24
29
  }
25
30
 
26
31
  CFIndex length = CFStringGetLength(cfString);
32
+ if (length == 0) {
33
+ return "";
34
+ }
35
+
36
+ // Check for overflow when calculating buffer size
27
37
  CFIndex maxSize =
28
- CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
38
+ CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8);
39
+ if (maxSize == kCFNotFound || maxSize > INT_MAX - 1) {
40
+ return "";
41
+ }
42
+ maxSize += 1; // For null terminator
43
+
29
44
  std::string result(maxSize, '\0');
30
45
 
31
- if (!CFStringGetCString(cfString, &result[0], maxSize,
32
- kCFStringEncodingUTF8)) {
46
+ Boolean success =
47
+ CFStringGetCString(cfString, &result[0], maxSize, kCFStringEncodingUTF8);
48
+ if (!success) {
49
+ // Log the failure for debugging
50
+ // Common reasons: encoding issue, buffer too small, or malformed string
51
+ DEBUG_LOG("[CFStringToString] Conversion failed - likely encoding issue or "
52
+ "buffer too small");
53
+ DEBUG_LOG("[CFStringToString] maxSize: %ld, string length: %ld", maxSize,
54
+ CFStringGetLength(cfString));
33
55
  return "";
34
56
  }
35
57
 
36
- result.resize(strlen(result.c_str()));
58
+ // Find actual string length by looking for null terminator
59
+ // This is safer than using strlen which could read past buffer
60
+ size_t actualLength = 0;
61
+ for (size_t i = 0; i < static_cast<size_t>(maxSize); ++i) {
62
+ if (result[i] == '\0') {
63
+ actualLength = i;
64
+ break;
65
+ }
66
+ }
67
+
68
+ result.resize(actualLength);
37
69
  return result;
38
70
  }
39
71
 
@@ -48,10 +80,32 @@ public:
48
80
  DEBUG_LOG("[GetVolumeMetadataWorker] Executing for mount point: %s",
49
81
  mountPoint.c_str());
50
82
  try {
83
+ // Validate and canonicalize mount point using realpath()
84
+ // This follows Apple's Secure Coding Guide recommendations
85
+ std::string error;
86
+ std::string validated_mount_point =
87
+ ValidatePathForRead(mountPoint, error);
88
+ if (validated_mount_point.empty()) {
89
+ SetError(error);
90
+ return;
91
+ }
92
+
93
+ // Use validated path for all subsequent operations
94
+ DEBUG_LOG("[GetVolumeMetadataWorker] Using validated mount point: %s",
95
+ validated_mount_point.c_str());
96
+
97
+ // Temporarily store original mountPoint and replace with validated one
98
+ std::string original_mount_point = mountPoint;
99
+ mountPoint = validated_mount_point;
100
+
51
101
  if (!GetBasicVolumeInfo()) {
102
+ mountPoint = original_mount_point; // Restore for error reporting
52
103
  return;
53
104
  }
54
- GetDiskArbitrationInfo();
105
+ GetDiskArbitrationInfoSafe();
106
+
107
+ // Restore original for consistency
108
+ mountPoint = original_mount_point;
55
109
  } catch (const std::exception &e) {
56
110
  DEBUG_LOG("[GetVolumeMetadataWorker] Exception: %s", e.what());
57
111
  SetError(e.what());
@@ -64,23 +118,53 @@ private:
64
118
  bool GetBasicVolumeInfo() {
65
119
  DEBUG_LOG("[GetVolumeMetadataWorker] Getting basic volume info for: %s",
66
120
  mountPoint.c_str());
121
+
122
+ // Use file descriptors to prevent TOCTOU race conditions
123
+ // Open the mount point directory with O_DIRECTORY to ensure it's a
124
+ // directory
125
+ int fd = open(mountPoint.c_str(), O_RDONLY | O_DIRECTORY);
126
+ if (fd < 0) {
127
+ int error = errno;
128
+ DEBUG_LOG("[GetVolumeMetadataWorker] open failed: %s (%d)",
129
+ strerror(error), error);
130
+ SetError(CreatePathErrorMessage("open", mountPoint, error));
131
+ return false;
132
+ }
133
+
134
+ // RAII wrapper for file descriptor to ensure it's always closed
135
+ struct FdGuard {
136
+ int fd;
137
+ ~FdGuard() {
138
+ if (fd >= 0) {
139
+ close(fd);
140
+ }
141
+ }
142
+ } fd_guard{fd};
143
+
67
144
  struct statvfs vfs;
68
145
  struct statfs fs;
69
146
 
70
- if (statvfs(mountPoint.c_str(), &vfs) != 0) {
71
- DEBUG_LOG("[GetVolumeMetadataWorker] statvfs failed: %s (%d)",
72
- strerror(errno), errno);
73
- SetError(CreatePathErrorMessage("statvfs", mountPoint, errno));
147
+ // Use fstatvfs and fstatfs on the file descriptor to prevent TOCTOU
148
+ // The fd holds a reference to the filesystem, preventing mount changes
149
+ if (fstatvfs(fd, &vfs) != 0) {
150
+ int error = errno;
151
+ DEBUG_LOG("[GetVolumeMetadataWorker] fstatvfs failed: %s (%d)",
152
+ strerror(error), error);
153
+ SetError(CreatePathErrorMessage("fstatvfs", mountPoint, error));
74
154
  return false;
75
155
  }
76
156
 
77
- if (statfs(mountPoint.c_str(), &fs) != 0) {
78
- DEBUG_LOG("[GetVolumeMetadataWorker] statfs failed: %s (%d)",
79
- strerror(errno), errno);
80
- SetError(CreatePathErrorMessage("statfs", mountPoint, errno));
157
+ if (fstatfs(fd, &fs) != 0) {
158
+ int error = errno;
159
+ DEBUG_LOG("[GetVolumeMetadataWorker] fstatfs failed: %s (%d)",
160
+ strerror(error), error);
161
+ SetError(CreatePathErrorMessage("fstatfs", mountPoint, error));
81
162
  return false;
82
163
  }
83
164
 
165
+ // fd_guard will automatically close the file descriptor when this function
166
+ // returns
167
+
84
168
  // Calculate sizes using uint64_t to prevent overflow
85
169
  const uint64_t blockSize = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
86
170
  const uint64_t totalBlocks = static_cast<uint64_t>(vfs.f_blocks);
@@ -123,7 +207,7 @@ private:
123
207
  return true;
124
208
  }
125
209
 
126
- void GetDiskArbitrationInfo() {
210
+ void GetDiskArbitrationInfoSafe() {
127
211
  DEBUG_LOG("[GetVolumeMetadataWorker] Getting Disk Arbitration info for: %s",
128
212
  mountPoint.c_str());
129
213
 
@@ -135,8 +219,18 @@ private:
135
219
  return;
136
220
  }
137
221
 
138
- // Create session on current thread
139
- CFReleaser<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
222
+ // THREAD SAFETY NOTE:
223
+ // Apple's DiskArbitration Programming Guide recommends scheduling DASession
224
+ // on a run loop or dispatch queue before using it. We use a dedicated
225
+ // serial dispatch queue (not the main queue) to avoid deadlock in Node.js
226
+ // context while following Apple's documented usage pattern.
227
+ //
228
+ // The mutex serializes DA operations across worker threads for extra
229
+ // safety.
230
+ std::lock_guard<std::mutex> lock(g_diskArbitrationMutex);
231
+
232
+ // Create session with RAII wrapper that handles unscheduling before release
233
+ DASessionRAII session(DASessionCreate(kCFAllocatorDefault));
140
234
  if (!session.isValid()) {
141
235
  DEBUG_LOG("[GetVolumeMetadataWorker] Failed to create DA session");
142
236
  metadata.status = "partial";
@@ -144,41 +238,18 @@ private:
144
238
  return;
145
239
  }
146
240
 
147
- try {
148
- // Get thread-local runloop or create new one if needed
149
- CFRunLoopRef runLoop = CFRunLoopGetCurrent();
150
- if (!runLoop) {
151
- // If no runloop exists, create a new one for this thread
152
- CFRunLoopRun();
153
- runLoop = CFRunLoopGetCurrent();
154
- if (!runLoop) {
155
- throw std::runtime_error("Failed to create thread-local runloop");
156
- }
157
- }
158
-
159
- // Schedule session with our runloop
160
- DASessionScheduleWithRunLoop(session.get(), runLoop,
161
- kCFRunLoopDefaultMode);
162
-
163
- // Use RAII to ensure cleanup
164
- struct RunLoopCleaner {
165
- DASessionRef session;
166
- CFRunLoopRef runLoop;
167
- bool shouldStop;
168
- RunLoopCleaner(DASessionRef s, CFRunLoopRef l, bool stop = false)
169
- : session(s), runLoop(l), shouldStop(stop) {}
170
- ~RunLoopCleaner() {
171
- DASessionUnscheduleFromRunLoop(session, runLoop,
172
- kCFRunLoopDefaultMode);
173
- if (shouldStop) {
174
- CFRunLoopStop(runLoop);
175
- }
176
- }
177
- } scopeGuard(session.get(), runLoop, !CFRunLoopGetCurrent());
241
+ // Schedule session on a dedicated serial dispatch queue
242
+ // This follows Apple's documented pattern: create session, schedule it, use
243
+ // it. We use a background queue (not main queue) to avoid deadlock in
244
+ // Node.js. The RAII wrapper will automatically unschedule in its
245
+ // destructor.
246
+ static dispatch_queue_t da_queue =
247
+ dispatch_queue_create("com.photostructure.fs-metadata.diskarbitration",
248
+ DISPATCH_QUEUE_SERIAL);
178
249
 
179
- // Run the run loop briefly to ensure DA is ready
180
- CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true);
250
+ session.scheduleOnQueue(da_queue);
181
251
 
252
+ try {
182
253
  CFReleaser<DADiskRef> disk(DADiskCreateFromBSDName(
183
254
  kCFAllocatorDefault, session.get(), metadata.mountFrom.c_str()));
184
255
 
@@ -186,23 +257,22 @@ private:
186
257
  DEBUG_LOG("[GetVolumeMetadataWorker] Failed to create disk reference");
187
258
  metadata.status = "partial";
188
259
  metadata.error = "Failed to create disk reference";
260
+ // RAII wrapper will automatically unschedule on function exit
189
261
  return;
190
262
  }
191
263
 
264
+ // Now safe to call DADiskCopyDescription with properly scheduled session
192
265
  CFReleaser<CFDictionaryRef> description(
193
266
  DADiskCopyDescription(disk.get()));
194
267
  if (!description.isValid()) {
195
268
  DEBUG_LOG("[GetVolumeMetadataWorker] Failed to get disk description");
196
269
  metadata.status = "partial";
197
270
  metadata.error = "Failed to get disk description";
271
+ // RAII wrapper will automatically unschedule on function exit
198
272
  return;
199
273
  }
200
274
 
201
- // Ensure we have a complete description before continuing
202
- CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
203
-
204
- // Process description synchronously since we're already on the right
205
- // thread
275
+ // Process description immediately
206
276
  ProcessDiskDescription(description.get());
207
277
 
208
278
  if (metadata.status != "partial") {
@@ -214,6 +284,8 @@ private:
214
284
  metadata.status = "error";
215
285
  metadata.error = e.what();
216
286
  }
287
+
288
+ // RAII wrapper automatically unschedules and releases session here
217
289
  }
218
290
 
219
291
  void ProcessDiskDescription(CFDictionaryRef description) {
@@ -248,7 +320,9 @@ private:
248
320
  DEBUG_LOG("[GetVolumeMetadataWorker] Processing network volume");
249
321
  CFBooleanRef isNetworkVolume = (CFBooleanRef)CFDictionaryGetValue(
250
322
  description, kDADiskDescriptionVolumeNetworkKey);
251
- metadata.remote = CFBooleanGetValue(isNetworkVolume);
323
+ if (isNetworkVolume) {
324
+ metadata.remote = CFBooleanGetValue(isNetworkVolume);
325
+ }
252
326
 
253
327
  CFURLRef url = (CFURLRef)CFDictionaryGetValue(
254
328
  description, kDADiskDescriptionVolumePathKey);
@@ -31,6 +31,11 @@ public:
31
31
  // separately and our error handling already covers mount state changes
32
32
  // See https://github.com/swiftlang/swift-corelibs-foundation/issues/4649
33
33
 
34
+ // getmntinfo_r_np is the thread-safe version of getmntinfo().
35
+ // The "_r" suffix indicates "reentrant" (thread-safe).
36
+ // The "_np" suffix indicates "non-portable" (Apple-specific).
37
+ // This function allocates a new buffer that we must free (handled by
38
+ // RAII).
34
39
  int count = getmntinfo_r_np(mntbuf.ptr(), MNT_NOWAIT);
35
40
 
36
41
  if (count <= 0) {
@@ -43,64 +48,92 @@ public:
43
48
  }
44
49
  }
45
50
 
46
- for (int i = 0; i < count; i++) {
47
- MountPoint mp;
48
- mp.mountPoint = mntbuf.get()[i].f_mntonname;
49
- mp.fstype = mntbuf.get()[i].f_fstypename;
50
- mp.error = ""; // Initialize error field
51
+ // Process mount points in batches to limit concurrent threads
52
+ const size_t maxConcurrentChecks = 4; // Limit concurrent access checks
51
53
 
52
- DEBUG_LOG("[GetVolumeMountPointsWorker] Checking mount point: %s",
53
- mp.mountPoint.c_str());
54
+ for (size_t i = 0; i < static_cast<size_t>(count);
55
+ i += maxConcurrentChecks) {
56
+ std::vector<std::future<std::pair<std::string, bool>>> futures;
57
+ std::vector<MountPoint> batchMountPoints;
58
+
59
+ // Create batch of mount points and launch their checks
60
+ for (size_t j = i;
61
+ j < static_cast<size_t>(count) && j < i + maxConcurrentChecks;
62
+ j++) {
63
+ MountPoint mp;
64
+ mp.mountPoint = mntbuf.get()[j].f_mntonname;
65
+ mp.fstype = mntbuf.get()[j].f_fstypename;
66
+ mp.error = ""; // Initialize error field
67
+
68
+ DEBUG_LOG("[GetVolumeMountPointsWorker] Checking mount point: %s",
69
+ mp.mountPoint.c_str());
54
70
 
55
- try {
56
- // Use RAII to manage future
57
- auto future = std::make_shared<std::future<bool>>(
71
+ batchMountPoints.push_back(mp);
72
+
73
+ // Launch async check
74
+ futures.push_back(
58
75
  std::async(std::launch::async, [path = mp.mountPoint]() {
59
- // Use faccessat for better security
60
- return faccessat(AT_FDCWD, path.c_str(), R_OK, AT_EACCESS) == 0;
76
+ // faccessat is preferred over access() for security:
77
+ // - AT_FDCWD: Use current working directory as base
78
+ // - AT_EACCESS: Check using effective user/group IDs (not real
79
+ // IDs) This prevents TOCTOU attacks and privilege escalation
80
+ // issues
81
+ bool accessible =
82
+ faccessat(AT_FDCWD, path.c_str(), R_OK, AT_EACCESS) == 0;
83
+ return std::make_pair(path, accessible);
61
84
  }));
85
+ }
62
86
 
63
- auto status = future->wait_for(std::chrono::milliseconds(timeoutMs_));
64
-
65
- switch (status) {
66
- case std::future_status::timeout:
67
- mp.status = "disconnected";
68
- mp.error = "Access check timed out";
69
- DEBUG_LOG("[GetVolumeMountPointsWorker] Access check timed out: %s",
70
- mp.mountPoint.c_str());
71
- break;
72
-
73
- case std::future_status::ready:
74
- try {
75
- bool isAccessible = future->get();
76
- mp.status = isAccessible ? "healthy" : "inaccessible";
77
- if (!isAccessible) {
78
- mp.error = "Path is not accessible";
87
+ // Process results for this batch
88
+ for (size_t k = 0; k < futures.size(); k++) {
89
+ auto &mp = batchMountPoints[k];
90
+ try {
91
+ auto status =
92
+ futures[k].wait_for(std::chrono::milliseconds(timeoutMs_));
93
+
94
+ switch (status) {
95
+ case std::future_status::timeout:
96
+ mp.status = "disconnected";
97
+ mp.error = "Access check timed out";
98
+ DEBUG_LOG(
99
+ "[GetVolumeMountPointsWorker] Access check timed out: %s",
100
+ mp.mountPoint.c_str());
101
+ break;
102
+
103
+ case std::future_status::ready:
104
+ try {
105
+ auto result = futures[k].get();
106
+ bool isAccessible = result.second;
107
+ mp.status = isAccessible ? "healthy" : "inaccessible";
108
+ if (!isAccessible) {
109
+ mp.error = "Path is not accessible";
110
+ }
111
+ DEBUG_LOG("[GetVolumeMountPointsWorker] Access check %s: %s",
112
+ isAccessible ? "succeeded" : "failed",
113
+ mp.mountPoint.c_str());
114
+ } catch (const std::exception &e) {
115
+ mp.status = "error";
116
+ mp.error = std::string("Access check failed: ") + e.what();
117
+ DEBUG_LOG("[GetVolumeMountPointsWorker] Exception: %s",
118
+ e.what());
79
119
  }
80
- DEBUG_LOG("[GetVolumeMountPointsWorker] Access check %s: %s",
81
- isAccessible ? "succeeded" : "failed",
82
- mp.mountPoint.c_str());
83
- } catch (const std::exception &e) {
120
+ break;
121
+
122
+ default:
84
123
  mp.status = "error";
85
- mp.error = std::string("Access check failed: ") + e.what();
86
- DEBUG_LOG("[GetVolumeMountPointsWorker] Exception: %s", e.what());
124
+ mp.error = "Unexpected future status";
125
+ DEBUG_LOG("[GetVolumeMountPointsWorker] Unexpected status: %s",
126
+ mp.mountPoint.c_str());
127
+ break;
87
128
  }
88
- break;
89
-
90
- default:
129
+ } catch (const std::exception &e) {
91
130
  mp.status = "error";
92
- mp.error = "Unexpected future status";
93
- DEBUG_LOG("[GetVolumeMountPointsWorker] Unexpected status: %s",
94
- mp.mountPoint.c_str());
95
- break;
131
+ mp.error = std::string("Mount point check failed: ") + e.what();
132
+ DEBUG_LOG("[GetVolumeMountPointsWorker] Exception: %s", e.what());
96
133
  }
97
- } catch (const std::exception &e) {
98
- mp.status = "error";
99
- mp.error = std::string("Mount point check failed: ") + e.what();
100
- DEBUG_LOG("[GetVolumeMountPointsWorker] Exception: %s", e.what());
101
- }
102
134
 
103
- mountPoints_.push_back(std::move(mp));
135
+ mountPoints_.push_back(std::move(mp));
136
+ }
104
137
  }
105
138
  } catch (const std::exception &e) {
106
139
  SetError(std::string("Failed to process mount points: ") + e.what());
package/src/hidden.ts CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  import { rename } from "node:fs/promises";
4
4
  import { basename, dirname, join } from "node:path";
5
+ import { debug } from "./debuglog";
5
6
  import { WrappedError } from "./error";
6
- import { canStatAsync, statAsync } from "./fs";
7
+ import { statAsync } from "./fs";
7
8
  import { isRootDirectory, normalizePath } from "./path";
8
9
  import { isWindows } from "./platform";
9
10
  import type { HiddenMetadata } from "./types/hidden_metadata";
@@ -47,14 +48,24 @@ export async function isHiddenImpl(
47
48
  pathname: string,
48
49
  nativeFn: NativeBindingsFn,
49
50
  ): Promise<boolean> {
51
+ debug("isHiddenImpl called with pathname: %s", pathname);
50
52
  const norm = normalizePath(pathname);
51
53
  if (norm == null) {
52
54
  throw new Error("Invalid pathname: " + JSON.stringify(pathname));
53
55
  }
54
- return (
55
- (LocalSupport.dotPrefix && isPosixHidden(norm)) ||
56
- (LocalSupport.systemFlag && isSystemHidden(norm, nativeFn))
56
+ debug("Normalized path: %s", norm);
57
+ debug(
58
+ "LocalSupport: dotPrefix=%s, systemFlag=%s",
59
+ LocalSupport.dotPrefix,
60
+ LocalSupport.systemFlag,
57
61
  );
62
+
63
+ const result =
64
+ (LocalSupport.dotPrefix && isPosixHidden(norm)) ||
65
+ (LocalSupport.systemFlag && (await isSystemHidden(norm, nativeFn)));
66
+
67
+ debug("isHiddenImpl returning: %s", result);
68
+ return result;
58
69
  }
59
70
 
60
71
  export async function isHiddenRecursiveImpl(
@@ -108,20 +119,32 @@ async function isSystemHidden(
108
119
  pathname: string,
109
120
  nativeFn: NativeBindingsFn,
110
121
  ): Promise<boolean> {
122
+ debug("isSystemHidden called with pathname: %s", pathname);
111
123
  if (!LocalSupport.systemFlag) {
124
+ debug("systemFlag not supported on this platform");
112
125
  // not supported on this platform
113
126
  return false;
114
127
  }
115
- if (isWindows && isRootDirectory(pathname)) {
116
- // windows `attr` thinks all drive letters don't exist.
117
- return false;
118
- }
119
128
 
120
- // don't bother the native bindings if the file doesn't exist:
121
- return (
122
- (await canStatAsync(pathname)) &&
123
- (await (await nativeFn()).isHidden(pathname))
124
- );
129
+ // Let the native function handle all validation, including root directories
130
+ // This ensures security checks are performed before any other checks
131
+ const native = await nativeFn();
132
+ debug("Calling native isHidden for: %s", pathname);
133
+
134
+ try {
135
+ const isHidden = await native.isHidden(pathname);
136
+ debug("Native isHidden returned: %s", isHidden);
137
+ return isHidden;
138
+ } catch (error) {
139
+ debug("Native isHidden threw error: %s", error);
140
+ // Handle non-existent paths by returning false (consistent with Windows behavior)
141
+ const errorStr = String(error);
142
+ if (errorStr.includes("Path not found")) {
143
+ debug("Path not found, returning false");
144
+ return false;
145
+ }
146
+ throw error;
147
+ }
125
148
  }
126
149
 
127
150
  /**
@@ -21,25 +21,24 @@ void GioMountPointsWorker::Execute() {
21
21
  try {
22
22
  DEBUG_LOG("[GioMountPoints] processing mounts");
23
23
 
24
- MountIterator::forEachMount([this](GMount * /*mount*/, GFile *root) {
25
- const GCharPtr path(g_file_get_path(root));
26
- if (path) {
27
- const GFileInfoPtr info(g_file_query_filesystem_info(
28
- root, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, nullptr, nullptr));
29
- if (info) {
30
- const char *fs_type_str = g_file_info_get_attribute_string(
31
- info.get(), G_FILE_ATTRIBUTE_FILESYSTEM_TYPE);
32
- if (fs_type_str) {
33
- const GCharPtr fs_type(g_strdup(fs_type_str));
34
- DEBUG_LOG("[GioMountPoints] found {mountPoint: %s, fsType: %s}",
35
- path.get(), fs_type.get());
36
- MountPoint point{};
37
- point.mountPoint = path.get();
38
- point.fstype = fs_type.get();
39
- mountPoints.push_back(point);
40
- }
41
- }
24
+ // Use thread-safe g_unix_mounts_get() API
25
+ MountIterator::forEachMount([this](GUnixMountEntry *entry) {
26
+ // Get mount path and filesystem type from thread-safe Unix mount API
27
+ const char *mount_path = g_unix_mount_get_mount_path(entry);
28
+ const char *fs_type = g_unix_mount_get_fs_type(entry);
29
+
30
+ if (mount_path && fs_type) {
31
+ DEBUG_LOG("[GioMountPoints] found {mountPoint: %s, fsType: %s}",
32
+ mount_path, fs_type);
33
+
34
+ MountPoint point{};
35
+ point.mountPoint = mount_path;
36
+ point.fstype = fs_type;
37
+ mountPoints.push_back(point);
38
+ } else {
39
+ DEBUG_LOG("[GioMountPoints] skipping mount with null path or fstype");
42
40
  }
41
+
43
42
  return true; // Continue iteration
44
43
  });
45
44