@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,12 +1,17 @@
|
|
|
1
1
|
// src/windows/drive_status.h
|
|
2
|
-
|
|
3
2
|
#pragma once
|
|
4
3
|
#include "../common/debug_log.h"
|
|
4
|
+
#include "security_utils.h"
|
|
5
|
+
#include "thread_pool.h"
|
|
6
|
+
#include "windows_arch.h"
|
|
5
7
|
#include <atomic>
|
|
6
|
-
#include <
|
|
8
|
+
#include <chrono>
|
|
9
|
+
#include <condition_variable>
|
|
10
|
+
#include <future>
|
|
11
|
+
#include <mutex>
|
|
7
12
|
#include <string>
|
|
13
|
+
#include <thread>
|
|
8
14
|
#include <vector>
|
|
9
|
-
#include <windows.h>
|
|
10
15
|
|
|
11
16
|
namespace FSMeta {
|
|
12
17
|
|
|
@@ -33,212 +38,177 @@ inline std::string DriveStatusToString(DriveStatus status) {
|
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
class
|
|
41
|
+
class DriveStatusChecker {
|
|
37
42
|
private:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
std::atomic<bool> shouldTerminate;
|
|
43
|
-
HANDLE threadHandle; // Store thread handle as member
|
|
44
|
-
|
|
45
|
-
public:
|
|
46
|
-
IOOperation()
|
|
47
|
-
: result{DriveStatus::Unknown}, threadId(0), shouldTerminate{false},
|
|
48
|
-
threadHandle(NULL) {
|
|
49
|
-
completionEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
|
|
50
|
-
}
|
|
43
|
+
static DriveStatus MapErrorToDriveStatus(DWORD error) {
|
|
44
|
+
switch (error) {
|
|
45
|
+
case ERROR_SUCCESS:
|
|
46
|
+
return DriveStatus::Healthy;
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
case ERROR_FILE_NOT_FOUND:
|
|
49
|
+
case ERROR_PATH_NOT_FOUND:
|
|
50
|
+
case ERROR_ACCESS_DENIED:
|
|
51
|
+
case ERROR_LOGON_FAILURE:
|
|
52
|
+
case ERROR_SHARING_VIOLATION:
|
|
53
|
+
return DriveStatus::Inaccessible;
|
|
58
54
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
SetEvent(completionEvent);
|
|
67
|
-
|
|
68
|
-
// Give thread chance to exit gracefully (increased timeout)
|
|
69
|
-
DWORD waitResult = WaitForSingleObject(threadHandle, 1000);
|
|
70
|
-
if (waitResult != WAIT_OBJECT_0) {
|
|
71
|
-
DEBUG_LOG("[IOOperation] WARNING: Thread %lu did not exit gracefully "
|
|
72
|
-
"after 1000ms",
|
|
73
|
-
threadId);
|
|
74
|
-
// NEVER use TerminateThread - it's extremely dangerous
|
|
75
|
-
// Instead, log the issue and abandon the thread
|
|
76
|
-
// The thread will eventually exit when the process terminates
|
|
77
|
-
}
|
|
55
|
+
case ERROR_BAD_NET_NAME:
|
|
56
|
+
case ERROR_NETWORK_UNREACHABLE:
|
|
57
|
+
case ERROR_NOT_CONNECTED:
|
|
58
|
+
case ERROR_NETWORK_ACCESS_DENIED:
|
|
59
|
+
case ERROR_BAD_NETPATH:
|
|
60
|
+
case ERROR_NO_NET_OR_BAD_PATH:
|
|
61
|
+
return DriveStatus::Disconnected;
|
|
78
62
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
DEBUG_LOG("[IOOperation] Thread cleanup complete");
|
|
63
|
+
default:
|
|
64
|
+
return DriveStatus::Unknown;
|
|
82
65
|
}
|
|
83
66
|
}
|
|
84
67
|
|
|
85
|
-
static
|
|
86
|
-
|
|
87
|
-
std::string searchPath = self->path + "*";
|
|
88
|
-
HANDLE findHandle = INVALID_HANDLE_VALUE;
|
|
89
|
-
WIN32_FIND_DATAA findData;
|
|
68
|
+
static DriveStatus CheckDriveInternal(const std::string &path) {
|
|
69
|
+
DEBUG_LOG("[DriveStatusChecker] Checking drive: %s", path.c_str());
|
|
90
70
|
|
|
91
|
-
|
|
71
|
+
// Validate path
|
|
72
|
+
if (!SecurityUtils::IsPathSecure(path)) {
|
|
73
|
+
DEBUG_LOG("[DriveStatusChecker] Path failed security check: %s",
|
|
74
|
+
path.c_str());
|
|
75
|
+
return DriveStatus::Inaccessible;
|
|
76
|
+
}
|
|
92
77
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return 0;
|
|
78
|
+
// Ensure path ends with backslash for FindFirstFileEx
|
|
79
|
+
std::string searchPath = path;
|
|
80
|
+
if (!searchPath.empty() && searchPath.back() != '\\') {
|
|
81
|
+
searchPath += '\\';
|
|
98
82
|
}
|
|
83
|
+
searchPath += "*";
|
|
99
84
|
|
|
100
|
-
|
|
85
|
+
WIN32_FIND_DATAA findData;
|
|
86
|
+
HandleGuard findHandle(FindFirstFileExA(
|
|
101
87
|
searchPath.c_str(), FindExInfoBasic, &findData, FindExSearchNameMatch,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
DEBUG_LOG("[
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
FindClose(findHandle);
|
|
111
|
-
DEBUG_LOG("[WorkerThread] Search completed successfully");
|
|
88
|
+
nullptr,
|
|
89
|
+
FIND_FIRST_EX_LARGE_FETCH | FIND_FIRST_EX_ON_DISK_ENTRIES_ONLY));
|
|
90
|
+
|
|
91
|
+
if (!findHandle) {
|
|
92
|
+
DWORD error = GetLastError();
|
|
93
|
+
DEBUG_LOG("[DriveStatusChecker] FindFirstFileEx failed for %s: %lu",
|
|
94
|
+
path.c_str(), error);
|
|
95
|
+
return MapErrorToDriveStatus(error);
|
|
112
96
|
}
|
|
113
97
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
98
|
+
// Successfully opened - drive is healthy
|
|
99
|
+
FindClose(findHandle.release());
|
|
100
|
+
DEBUG_LOG("[DriveStatusChecker] Drive %s is healthy", path.c_str());
|
|
101
|
+
return DriveStatus::Healthy;
|
|
117
102
|
}
|
|
118
103
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
104
|
+
public:
|
|
105
|
+
static std::future<DriveStatus> CheckDriveAsync(const std::string &path,
|
|
106
|
+
DWORD timeoutMs = 5000) {
|
|
107
|
+
auto promise = std::make_shared<std::promise<DriveStatus>>();
|
|
108
|
+
auto future = promise->get_future();
|
|
123
109
|
|
|
124
|
-
//
|
|
125
|
-
|
|
110
|
+
// Use a shared state for timeout handling
|
|
111
|
+
auto state = std::make_shared<std::atomic<bool>>(false);
|
|
126
112
|
|
|
127
|
-
path
|
|
128
|
-
|
|
129
|
-
|
|
113
|
+
GetGlobalThreadPool().Submit([path, promise, state, timeoutMs]() {
|
|
114
|
+
// Set up timeout
|
|
115
|
+
auto startTime = std::chrono::steady_clock::now();
|
|
130
116
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
DEBUG_LOG("[CheckDriveStatus] Failed to create thread");
|
|
134
|
-
return DriveStatus::Unknown;
|
|
135
|
-
}
|
|
117
|
+
// Perform the check
|
|
118
|
+
DriveStatus status = CheckDriveInternal(path);
|
|
136
119
|
|
|
137
|
-
|
|
138
|
-
|
|
120
|
+
// Check if we've timed out
|
|
121
|
+
auto elapsed = std::chrono::steady_clock::now() - startTime;
|
|
122
|
+
if (elapsed > std::chrono::milliseconds(timeoutMs)) {
|
|
123
|
+
status = DriveStatus::Timeout;
|
|
124
|
+
}
|
|
139
125
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
126
|
+
// Only set the promise if we haven't been cancelled
|
|
127
|
+
if (!state->load()) {
|
|
128
|
+
promise->set_value(status);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Handle timeout in the caller
|
|
133
|
+
if (timeoutMs != INFINITE) {
|
|
134
|
+
std::thread([promise, state, timeoutMs]() {
|
|
135
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(timeoutMs));
|
|
136
|
+
if (!state->exchange(true)) {
|
|
137
|
+
try {
|
|
138
|
+
promise->set_value(DriveStatus::Timeout);
|
|
139
|
+
} catch (...) {
|
|
140
|
+
// Promise already satisfied
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}).detach();
|
|
145
144
|
}
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
CloseHandle(threadHandle);
|
|
149
|
-
DEBUG_LOG("[CheckDriveStatus] CloseHandle %lu completed", threadId);
|
|
150
|
-
threadHandle = NULL;
|
|
151
|
-
return result.load(std::memory_order_acquire);
|
|
146
|
+
return future;
|
|
152
147
|
}
|
|
153
148
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
case ERROR_PATH_NOT_FOUND:
|
|
159
|
-
case ERROR_ACCESS_DENIED:
|
|
160
|
-
case ERROR_LOGON_FAILURE:
|
|
161
|
-
return DriveStatus::Inaccessible;
|
|
149
|
+
static DriveStatus CheckDrive(const std::string &path,
|
|
150
|
+
DWORD timeoutMs = 5000) {
|
|
151
|
+
try {
|
|
152
|
+
auto future = CheckDriveAsync(path, timeoutMs);
|
|
162
153
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
case ERROR_BAD_NETPATH:
|
|
168
|
-
return DriveStatus::Disconnected;
|
|
154
|
+
if (future.wait_for(std::chrono::milliseconds(timeoutMs)) ==
|
|
155
|
+
std::future_status::timeout) {
|
|
156
|
+
return DriveStatus::Timeout;
|
|
157
|
+
}
|
|
169
158
|
|
|
170
|
-
|
|
159
|
+
return future.get();
|
|
160
|
+
} catch (...) {
|
|
161
|
+
DEBUG_LOG("[DriveStatusChecker] Exception checking drive %s",
|
|
162
|
+
path.c_str());
|
|
171
163
|
return DriveStatus::Unknown;
|
|
172
164
|
}
|
|
173
165
|
}
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
class ParallelDriveStatus {
|
|
177
|
-
private:
|
|
178
|
-
struct PendingCheck {
|
|
179
|
-
std::string path;
|
|
180
|
-
std::unique_ptr<IOOperation> op;
|
|
181
|
-
DWORD timeoutMs;
|
|
182
|
-
DriveStatus status;
|
|
183
|
-
};
|
|
184
166
|
|
|
185
|
-
std::vector<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
void Submit(const std::string &path, DWORD timeoutMs = 1000) {
|
|
189
|
-
auto check = std::make_unique<PendingCheck>();
|
|
190
|
-
check->path = path;
|
|
191
|
-
check->op = std::make_unique<IOOperation>();
|
|
192
|
-
check->timeoutMs = timeoutMs;
|
|
193
|
-
check->status = DriveStatus::Unknown;
|
|
194
|
-
|
|
195
|
-
DEBUG_LOG("[ParallelDriveStatus] Submitting check for: %s", path.c_str());
|
|
196
|
-
pending_.push_back(std::move(check));
|
|
197
|
-
}
|
|
167
|
+
static std::vector<DriveStatus>
|
|
168
|
+
CheckMultipleDrives(const std::vector<std::string> &paths,
|
|
169
|
+
DWORD timeoutMs = 5000) {
|
|
198
170
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
results.reserve(pending_.size());
|
|
171
|
+
std::vector<std::future<DriveStatus>> futures;
|
|
172
|
+
futures.reserve(paths.size());
|
|
202
173
|
|
|
203
|
-
//
|
|
204
|
-
for (auto &
|
|
205
|
-
|
|
206
|
-
check->op->CheckDriveWithTimeout(check->path, check->timeoutMs);
|
|
174
|
+
// Launch all checks concurrently
|
|
175
|
+
for (const auto &path : paths) {
|
|
176
|
+
futures.push_back(CheckDriveAsync(path, timeoutMs));
|
|
207
177
|
}
|
|
208
178
|
|
|
209
|
-
// Collect results
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
179
|
+
// Collect results
|
|
180
|
+
std::vector<DriveStatus> results;
|
|
181
|
+
results.reserve(paths.size());
|
|
182
|
+
|
|
183
|
+
for (size_t i = 0; i < futures.size(); ++i) {
|
|
184
|
+
try {
|
|
185
|
+
if (futures[i].wait_for(std::chrono::milliseconds(timeoutMs)) ==
|
|
186
|
+
std::future_status::timeout) {
|
|
187
|
+
results.push_back(DriveStatus::Timeout);
|
|
188
|
+
} else {
|
|
189
|
+
results.push_back(futures[i].get());
|
|
190
|
+
}
|
|
191
|
+
} catch (...) {
|
|
192
|
+
DEBUG_LOG("[DriveStatusChecker] Exception getting result for drive %s",
|
|
193
|
+
paths[i].c_str());
|
|
194
|
+
results.push_back(DriveStatus::Unknown);
|
|
195
|
+
}
|
|
214
196
|
}
|
|
215
197
|
|
|
216
|
-
pending_.clear();
|
|
217
198
|
return results;
|
|
218
199
|
}
|
|
219
200
|
};
|
|
220
201
|
|
|
221
|
-
//
|
|
202
|
+
// Compatibility wrapper for existing code
|
|
222
203
|
inline std::vector<DriveStatus>
|
|
223
204
|
CheckDriveStatus(const std::vector<std::string> &paths,
|
|
224
|
-
DWORD timeoutMs =
|
|
225
|
-
|
|
226
|
-
ParallelDriveStatus checker;
|
|
227
|
-
for (const auto &path : paths) {
|
|
228
|
-
checker.Submit(path, timeoutMs);
|
|
229
|
-
}
|
|
230
|
-
return checker.WaitForResults();
|
|
231
|
-
} catch (...) {
|
|
232
|
-
DEBUG_LOG("[CheckDriveStatus] caught unexpected exception");
|
|
233
|
-
return std::vector<DriveStatus>(paths.size(), DriveStatus::Unknown);
|
|
234
|
-
}
|
|
205
|
+
DWORD timeoutMs = 5000) {
|
|
206
|
+
return DriveStatusChecker::CheckMultipleDrives(paths, timeoutMs);
|
|
235
207
|
}
|
|
236
208
|
|
|
237
|
-
// Keep single path version for backwards compatibility
|
|
238
209
|
inline DriveStatus CheckDriveStatus(const std::string &path,
|
|
239
|
-
DWORD timeoutMs =
|
|
240
|
-
|
|
241
|
-
return CheckDriveStatus(paths, timeoutMs)[0];
|
|
210
|
+
DWORD timeoutMs = 5000) {
|
|
211
|
+
return DriveStatusChecker::CheckDrive(path, timeoutMs);
|
|
242
212
|
}
|
|
243
213
|
|
|
244
214
|
} // namespace FSMeta
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// src/windows/error_utils.h
|
|
2
2
|
#pragma once
|
|
3
|
+
#include "../common/debug_log.h"
|
|
4
|
+
#include "windows_arch.h"
|
|
3
5
|
#include <sstream>
|
|
4
6
|
#include <stdexcept>
|
|
5
7
|
#include <string>
|
|
6
|
-
#include <windows.h>
|
|
7
8
|
|
|
8
9
|
namespace FSMeta {
|
|
9
10
|
|
|
@@ -16,27 +17,77 @@ public:
|
|
|
16
17
|
: std::runtime_error(FormatWindowsError(operation, errorCode)) {}
|
|
17
18
|
|
|
18
19
|
private:
|
|
20
|
+
// RAII wrapper for LocalFree - ensures cleanup even if exception thrown
|
|
21
|
+
// Prevents memory leaks when FormatMessageA allocates a buffer
|
|
22
|
+
struct LocalFreeGuard {
|
|
23
|
+
LPVOID ptr;
|
|
24
|
+
|
|
25
|
+
explicit LocalFreeGuard(LPVOID p) : ptr(p) {}
|
|
26
|
+
|
|
27
|
+
~LocalFreeGuard() {
|
|
28
|
+
if (ptr) {
|
|
29
|
+
LocalFree(ptr);
|
|
30
|
+
DEBUG_LOG("[LocalFreeGuard] LocalFree called on FormatMessage buffer");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Prevent copying to ensure single ownership
|
|
35
|
+
LocalFreeGuard(const LocalFreeGuard &) = delete;
|
|
36
|
+
LocalFreeGuard &operator=(const LocalFreeGuard &) = delete;
|
|
37
|
+
|
|
38
|
+
// Allow moving for flexibility
|
|
39
|
+
LocalFreeGuard(LocalFreeGuard &&other) noexcept : ptr(other.ptr) {
|
|
40
|
+
other.ptr = nullptr;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
LocalFreeGuard &operator=(LocalFreeGuard &&other) noexcept {
|
|
44
|
+
if (this != &other) {
|
|
45
|
+
if (ptr) {
|
|
46
|
+
LocalFree(ptr);
|
|
47
|
+
}
|
|
48
|
+
ptr = other.ptr;
|
|
49
|
+
other.ptr = nullptr;
|
|
50
|
+
}
|
|
51
|
+
return *this;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
19
55
|
static std::string FormatWindowsError(const std::string &operation,
|
|
20
56
|
DWORD error) {
|
|
21
57
|
if (error == 0) {
|
|
22
58
|
return operation + " failed with an unknown error";
|
|
23
59
|
}
|
|
24
60
|
|
|
25
|
-
LPVOID messageBuffer;
|
|
61
|
+
LPVOID messageBuffer = nullptr;
|
|
26
62
|
size_t size = FormatMessageA(
|
|
27
63
|
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
|
28
64
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
29
65
|
NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
30
66
|
(LPSTR)&messageBuffer, 0, NULL);
|
|
31
67
|
|
|
68
|
+
// RAII guard ensures LocalFree is called even if exception thrown
|
|
69
|
+
LocalFreeGuard guard(messageBuffer);
|
|
70
|
+
|
|
32
71
|
if (size == 0 || !messageBuffer) {
|
|
72
|
+
DWORD formatError = GetLastError();
|
|
73
|
+
DEBUG_LOG("[FormatWindowsError] FormatMessageA failed for error %lu: "
|
|
74
|
+
"FormatMessage error=%lu, size=%zu",
|
|
75
|
+
error, formatError, size);
|
|
33
76
|
return operation + " failed with error code: " + std::to_string(error);
|
|
34
77
|
}
|
|
35
78
|
|
|
79
|
+
// Now safe: guard will free messageBuffer even if string construction
|
|
80
|
+
// throws
|
|
36
81
|
std::string errorMessage((LPSTR)messageBuffer, size);
|
|
37
|
-
|
|
82
|
+
|
|
83
|
+
// Trim trailing newlines/carriage returns that Windows adds
|
|
84
|
+
while (!errorMessage.empty() &&
|
|
85
|
+
(errorMessage.back() == '\r' || errorMessage.back() == '\n')) {
|
|
86
|
+
errorMessage.pop_back();
|
|
87
|
+
}
|
|
38
88
|
|
|
39
89
|
return operation + " failed: " + errorMessage;
|
|
90
|
+
// guard destructor automatically calls LocalFree here
|
|
40
91
|
}
|
|
41
92
|
};
|
|
42
93
|
|
package/src/windows/fs_meta.h
CHANGED
package/src/windows/hidden.cpp
CHANGED
|
@@ -1,36 +1,12 @@
|
|
|
1
1
|
// src/windows/hidden.cpp
|
|
2
2
|
#include "hidden.h"
|
|
3
|
+
#include "../common/debug_log.h"
|
|
3
4
|
#include "error_utils.h"
|
|
4
|
-
#include
|
|
5
|
+
#include "security_utils.h"
|
|
5
6
|
|
|
6
7
|
namespace FSMeta {
|
|
7
8
|
|
|
8
9
|
namespace {
|
|
9
|
-
// Utility class for path conversion
|
|
10
|
-
class PathConverter {
|
|
11
|
-
public:
|
|
12
|
-
static std::wstring ToWString(const std::string &path) {
|
|
13
|
-
if (path.empty()) {
|
|
14
|
-
return std::wstring();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Pre-calculate required buffer size
|
|
18
|
-
int wlen = MultiByteToWideChar(CP_UTF8, 0, path.c_str(),
|
|
19
|
-
static_cast<int>(path.length()), nullptr, 0);
|
|
20
|
-
if (wlen == 0) {
|
|
21
|
-
throw FSException("Path conversion", GetLastError());
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Reserve exact size needed
|
|
25
|
-
std::wstring wpath(wlen, 0);
|
|
26
|
-
if (!MultiByteToWideChar(CP_UTF8, 0, path.c_str(),
|
|
27
|
-
static_cast<int>(path.length()), &wpath[0],
|
|
28
|
-
wlen)) {
|
|
29
|
-
throw FSException("Path conversion", GetLastError());
|
|
30
|
-
}
|
|
31
|
-
return wpath;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
10
|
|
|
35
11
|
// RAII wrapper for file attributes
|
|
36
12
|
class FileAttributeHandler {
|
|
@@ -48,8 +24,9 @@ public:
|
|
|
48
24
|
bool isHidden() const { return (attributes & FILE_ATTRIBUTE_HIDDEN) != 0; }
|
|
49
25
|
|
|
50
26
|
void setHidden(bool value) {
|
|
51
|
-
DWORD newAttrs =
|
|
52
|
-
|
|
27
|
+
DWORD newAttrs =
|
|
28
|
+
value ? (attributes | static_cast<DWORD>(FILE_ATTRIBUTE_HIDDEN))
|
|
29
|
+
: (attributes & ~static_cast<DWORD>(FILE_ATTRIBUTE_HIDDEN));
|
|
53
30
|
|
|
54
31
|
if (!SetFileAttributesW(path.c_str(), newAttrs)) {
|
|
55
32
|
throw FSException("SetFileAttributes", GetLastError());
|
|
@@ -61,7 +38,7 @@ public:
|
|
|
61
38
|
|
|
62
39
|
class GetHiddenWorker : public Napi::AsyncWorker {
|
|
63
40
|
const std::string path;
|
|
64
|
-
bool result;
|
|
41
|
+
bool result = false;
|
|
65
42
|
Napi::Promise::Deferred deferred;
|
|
66
43
|
|
|
67
44
|
public:
|
|
@@ -70,28 +47,68 @@ public:
|
|
|
70
47
|
|
|
71
48
|
void Execute() override {
|
|
72
49
|
try {
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
50
|
+
// Debug: Log the input path
|
|
51
|
+
DEBUG_LOG("[GetHiddenWorker] Checking path: %s", path.c_str());
|
|
52
|
+
|
|
53
|
+
// Enhanced security validation
|
|
54
|
+
if (!SecurityUtils::IsPathSecure(path)) {
|
|
55
|
+
DEBUG_LOG("[GetHiddenWorker] Path failed security check: %s",
|
|
56
|
+
path.c_str());
|
|
57
|
+
throw FSException("Security validation failed: invalid path",
|
|
76
58
|
ERROR_INVALID_PARAMETER);
|
|
77
59
|
}
|
|
78
60
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
DEBUG_LOG("[GetHiddenWorker] Path passed security check");
|
|
62
|
+
|
|
63
|
+
auto wpath = SecurityUtils::SafeStringToWide(path);
|
|
64
|
+
DEBUG_LOG("[GetHiddenWorker] Converted to wide string");
|
|
65
|
+
|
|
66
|
+
// Check if file exists before checking attributes
|
|
67
|
+
DWORD attributes = GetFileAttributesW(wpath.c_str());
|
|
68
|
+
if (attributes == INVALID_FILE_ATTRIBUTES) {
|
|
69
|
+
DWORD error = GetLastError();
|
|
70
|
+
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) {
|
|
71
|
+
DEBUG_LOG("[GetHiddenWorker] File not found: %s", path.c_str());
|
|
72
|
+
result = false; // Non-existent files are not hidden
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Other errors should throw
|
|
76
|
+
throw FSException("GetFileAttributes", error);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check if it's a root directory
|
|
80
|
+
bool isRoot =
|
|
81
|
+
(wpath.length() == 3 && wpath[1] == L':' && wpath[2] == L'\\');
|
|
82
|
+
if (isRoot) {
|
|
83
|
+
DEBUG_LOG(
|
|
84
|
+
"[GetHiddenWorker] Root directory detected: %s, attributes: 0x%X",
|
|
85
|
+
path.c_str(), attributes);
|
|
86
|
+
// Windows may report root directories as hidden/system, but we'll
|
|
87
|
+
// report the actual state The tests will need to be updated to reflect
|
|
88
|
+
// actual Windows behavior
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
result = (attributes & FILE_ATTRIBUTE_HIDDEN) != 0;
|
|
92
|
+
DEBUG_LOG("[GetHiddenWorker] Result: %s",
|
|
93
|
+
result ? "hidden" : "not hidden");
|
|
82
94
|
} catch (const FSException &e) {
|
|
95
|
+
DEBUG_LOG("[GetHiddenWorker] Caught FSException: %s", e.what());
|
|
83
96
|
SetError(e.what());
|
|
84
97
|
} catch (const std::exception &e) {
|
|
98
|
+
DEBUG_LOG("[GetHiddenWorker] Caught std::exception: %s", e.what());
|
|
85
99
|
SetError(std::string("Unexpected error: ") + e.what());
|
|
86
100
|
}
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
void OnOK() override {
|
|
104
|
+
DEBUG_LOG("[GetHiddenWorker] OnOK called, result=%s",
|
|
105
|
+
result ? "true" : "false");
|
|
90
106
|
Napi::HandleScope scope(Env());
|
|
91
107
|
deferred.Resolve(Napi::Boolean::New(Env(), result));
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
void OnError(const Napi::Error &e) override {
|
|
111
|
+
DEBUG_LOG("[GetHiddenWorker] OnError called with: %s", e.Message().c_str());
|
|
95
112
|
Napi::HandleScope scope(Env());
|
|
96
113
|
deferred.Reject(e.Value());
|
|
97
114
|
}
|
|
@@ -109,13 +126,13 @@ public:
|
|
|
109
126
|
|
|
110
127
|
void Execute() override {
|
|
111
128
|
try {
|
|
112
|
-
//
|
|
113
|
-
if (path
|
|
114
|
-
throw FSException("
|
|
129
|
+
// Enhanced security validation
|
|
130
|
+
if (!SecurityUtils::IsPathSecure(path)) {
|
|
131
|
+
throw FSException("Security validation failed: invalid path",
|
|
115
132
|
ERROR_INVALID_PARAMETER);
|
|
116
133
|
}
|
|
117
134
|
|
|
118
|
-
auto wpath =
|
|
135
|
+
auto wpath = SecurityUtils::SafeStringToWide(path);
|
|
119
136
|
FileAttributeHandler handler(wpath);
|
|
120
137
|
handler.setHidden(value);
|
|
121
138
|
} catch (const FSException &e) {
|
|
@@ -145,8 +162,8 @@ Napi::Promise GetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
|
145
162
|
throw Napi::TypeError::New(env, "String path expected");
|
|
146
163
|
}
|
|
147
164
|
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
auto *worker = new GetHiddenWorker(
|
|
166
|
+
env, info[0].As<Napi::String>().Utf8Value(), deferred);
|
|
150
167
|
worker->Queue();
|
|
151
168
|
|
|
152
169
|
return deferred.Promise();
|
|
@@ -165,10 +182,10 @@ Napi::Promise SetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
|
165
182
|
throw Napi::TypeError::New(env, "String path and boolean value expected");
|
|
166
183
|
}
|
|
167
184
|
|
|
168
|
-
std::string path = info[0].As<Napi::String>();
|
|
169
185
|
bool value = info[1].As<Napi::Boolean>();
|
|
170
186
|
|
|
171
|
-
auto *worker = new SetHiddenWorker(
|
|
187
|
+
auto *worker = new SetHiddenWorker(
|
|
188
|
+
env, info[0].As<Napi::String>().Utf8Value(), value, deferred);
|
|
172
189
|
worker->Queue();
|
|
173
190
|
|
|
174
191
|
return deferred.Promise();
|