@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.
- package/CHANGELOG.md +11 -6
- package/CLAUDE.md +160 -136
- package/CODE_OF_CONDUCT.md +11 -11
- package/CONTRIBUTING.md +2 -2
- package/README.md +34 -84
- package/binding.gyp +98 -23
- package/claude.sh +23 -0
- package/dist/index.cjs +53 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +52 -21
- package/dist/index.mjs.map +1 -1
- package/{C++_REVIEW_TODO.md → doc/C++_REVIEW_TODO.md} +97 -25
- package/doc/GPG_RELEASE_HOWTO.md +505 -0
- package/doc/MACOS_API_REFERENCE.md +469 -0
- package/doc/SECURITY_AUDIT_2025.md +809 -0
- package/doc/SSH_RELEASE_HOWTO.md +207 -0
- package/doc/WINDOWS_API_REFERENCE.md +422 -0
- package/doc/WINDOWS_ARM64_SECURITY.md +161 -0
- package/doc/WINDOWS_DEBUG_GUIDE.md +96 -0
- package/doc/examples.md +267 -0
- package/doc/gotchas.md +297 -0
- package/doc/logo.png +0 -0
- package/doc/logo.svg +85 -0
- package/doc/macos-asan-sip-issue.md +71 -0
- package/doc/social.png +0 -0
- package/doc/social.svg +125 -0
- package/doc/windows-build.md +226 -0
- package/doc/windows-clang-tidy.md +72 -0
- package/doc/windows-memory-testing.md +108 -0
- package/doc/windows-prebuildify-arm64.md +232 -0
- package/jest.config.cjs +24 -0
- package/package.json +68 -44
- package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
- package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
- package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/scripts/check-memory.ts +186 -0
- package/scripts/clang-tidy.ts +832 -0
- package/scripts/install.cjs +42 -0
- package/scripts/is-platform.mjs +1 -1
- package/scripts/macos-asan.sh +155 -0
- package/scripts/post-build.mjs +3 -3
- package/scripts/prebuild-linux-glibc.sh +119 -0
- package/scripts/prebuildify-wrapper.ts +77 -0
- package/scripts/precommit.ts +70 -0
- package/scripts/sanitizers-test.sh +7 -1
- package/scripts/{configure.mjs → setup-native.mjs} +4 -1
- package/src/binding.cpp +1 -1
- package/src/common/error_utils.h +0 -6
- package/src/common/volume_metadata.h +6 -0
- package/src/darwin/hidden.cpp +73 -25
- package/src/darwin/path_security.h +149 -0
- package/src/darwin/raii_utils.h +104 -4
- package/src/darwin/volume_metadata.cpp +132 -58
- package/src/darwin/volume_mount_points.cpp +80 -47
- package/src/hidden.ts +36 -13
- package/src/linux/gio_mount_points.cpp +17 -18
- package/src/linux/gio_utils.cpp +92 -37
- package/src/linux/gio_utils.h +11 -5
- package/src/linux/gio_volume_metadata.cpp +111 -48
- package/src/linux/volume_metadata.cpp +67 -4
- package/src/object.ts +1 -0
- package/src/options.ts +6 -0
- package/src/path.ts +11 -0
- package/src/platform.ts +25 -0
- package/src/remote_info.ts +5 -3
- package/src/stack_path.ts +8 -6
- package/src/string_enum.ts +1 -0
- package/src/test-utils/benchmark-harness.ts +192 -0
- package/src/test-utils/debuglog-child.ts +30 -2
- package/src/test-utils/debuglog-enabled-child.ts +38 -8
- package/src/test-utils/jest-setup.ts +14 -0
- package/src/test-utils/memory-test-core.ts +336 -0
- package/src/test-utils/memory-test-runner.ts +108 -0
- package/src/test-utils/platform.ts +46 -1
- package/src/test-utils/worker-thread-helper.cjs +157 -26
- package/src/types/native_bindings.ts +1 -1
- package/src/types/options.ts +6 -0
- package/src/windows/drive_status.h +133 -163
- package/src/windows/error_utils.h +54 -3
- package/src/windows/fs_meta.h +1 -1
- package/src/windows/hidden.cpp +60 -43
- package/src/windows/security_utils.h +250 -0
- package/src/windows/string.h +68 -11
- package/src/windows/system_volume.h +1 -1
- package/src/windows/thread_pool.h +206 -0
- package/src/windows/volume_metadata.cpp +11 -6
- package/src/windows/volume_mount_points.cpp +8 -7
- package/src/windows/windows_arch.h +39 -0
- package/scripts/check-memory.mjs +0 -123
- 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 <
|
|
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)
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
-
//
|
|
139
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
//
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} catch (const std::exception &e) {
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
default:
|
|
84
123
|
mp.status = "error";
|
|
85
|
-
mp.error =
|
|
86
|
-
DEBUG_LOG("[GetVolumeMountPointsWorker]
|
|
124
|
+
mp.error = "Unexpected future status";
|
|
125
|
+
DEBUG_LOG("[GetVolumeMountPointsWorker] Unexpected status: %s",
|
|
126
|
+
mp.mountPoint.c_str());
|
|
127
|
+
break;
|
|
87
128
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
default:
|
|
129
|
+
} catch (const std::exception &e) {
|
|
91
130
|
mp.status = "error";
|
|
92
|
-
mp.error = "
|
|
93
|
-
DEBUG_LOG("[GetVolumeMountPointsWorker]
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|