@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,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 <memory>
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 IOOperation {
41
+ class DriveStatusChecker {
37
42
  private:
38
- HANDLE completionEvent;
39
- std::atomic<DriveStatus> result;
40
- std::string path;
41
- DWORD threadId;
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
- ~IOOperation() {
53
- if (completionEvent) {
54
- CloseHandle(completionEvent);
55
- }
56
- CleanupThread();
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
- void CleanupThread() {
60
- if (threadHandle) {
61
- DEBUG_LOG("[IOOperation] Cleaning up thread %lu", threadId);
62
- // Signal termination
63
- shouldTerminate.store(true, std::memory_order_release);
64
-
65
- // Wake up thread if it's waiting on something
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
- CloseHandle(threadHandle);
80
- threadHandle = NULL;
81
- DEBUG_LOG("[IOOperation] Thread cleanup complete");
63
+ default:
64
+ return DriveStatus::Unknown;
82
65
  }
83
66
  }
84
67
 
85
- static DWORD WINAPI WorkerThread(LPVOID param) {
86
- IOOperation *self = static_cast<IOOperation *>(param);
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
- DEBUG_LOG("[WorkerThread] Starting search on path: %s", searchPath.c_str());
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
- // Check if we should terminate before starting work
94
- if (self->shouldTerminate.load(std::memory_order_acquire)) {
95
- DEBUG_LOG("[WorkerThread] Thread %lu terminating before work",
96
- self->threadId);
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
- findHandle = FindFirstFileExA(
85
+ WIN32_FIND_DATAA findData;
86
+ HandleGuard findHandle(FindFirstFileExA(
101
87
  searchPath.c_str(), FindExInfoBasic, &findData, FindExSearchNameMatch,
102
- NULL, FIND_FIRST_EX_LARGE_FETCH | FIND_FIRST_EX_ON_DISK_ENTRIES_ONLY);
103
-
104
- if (findHandle == INVALID_HANDLE_VALUE) {
105
- self->result.store(self->MapErrorToDriveStatus(GetLastError()),
106
- std::memory_order_release);
107
- DEBUG_LOG("[WorkerThread] Search failed with error: %lu", GetLastError());
108
- } else {
109
- self->result.store(DriveStatus::Healthy, std::memory_order_release);
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
- SetEvent(self->completionEvent);
115
- DEBUG_LOG("[WorkerThread] Thread %lu exiting", self->threadId);
116
- return 0;
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
- DriveStatus CheckDriveWithTimeout(const std::string &checkPath,
120
- DWORD timeoutMs) {
121
- DEBUG_LOG("[CheckDriveStatus] Starting check for: %s timeout: %lu ms",
122
- checkPath.c_str(), timeoutMs);
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
- // Cleanup any previous thread
125
- CleanupThread();
110
+ // Use a shared state for timeout handling
111
+ auto state = std::make_shared<std::atomic<bool>>(false);
126
112
 
127
- path = checkPath;
128
- ResetEvent(completionEvent);
129
- shouldTerminate.store(false, std::memory_order_release);
113
+ GetGlobalThreadPool().Submit([path, promise, state, timeoutMs]() {
114
+ // Set up timeout
115
+ auto startTime = std::chrono::steady_clock::now();
130
116
 
131
- threadHandle = CreateThread(NULL, 0, WorkerThread, this, 0, &threadId);
132
- if (!threadHandle) {
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
- DEBUG_LOG("[CheckDriveStatus] Created thread %lu", threadId);
138
- DWORD waitResult = WaitForSingleObject(threadHandle, timeoutMs);
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
- if (waitResult == WAIT_TIMEOUT) {
141
- DEBUG_LOG("[CheckDriveStatus] Thread %lu timed out after %lu ms",
142
- threadId, timeoutMs);
143
- CleanupThread();
144
- return DriveStatus::Timeout;
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
- DEBUG_LOG("[CheckDriveStatus] Thread %lu completed normally", threadId);
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
- private:
155
- DriveStatus MapErrorToDriveStatus(DWORD error) {
156
- switch (error) {
157
- case ERROR_FILE_NOT_FOUND:
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
- case ERROR_BAD_NET_NAME:
164
- case ERROR_NETWORK_UNREACHABLE:
165
- case ERROR_NOT_CONNECTED:
166
- case ERROR_NETWORK_ACCESS_DENIED:
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
- default:
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<std::unique_ptr<PendingCheck>> pending_;
186
-
187
- public:
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
- std::vector<DriveStatus> WaitForResults() {
200
- std::vector<DriveStatus> results;
201
- results.reserve(pending_.size());
171
+ std::vector<std::future<DriveStatus>> futures;
172
+ futures.reserve(paths.size());
202
173
 
203
- // Start all operations
204
- for (auto &check : pending_) {
205
- check->status =
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 in original order
210
- for (auto &check : pending_) {
211
- DEBUG_LOG("[ParallelDriveStatus] Result for %s: %s", check->path.c_str(),
212
- DriveStatusToString(check->status).c_str());
213
- results.push_back(check->status);
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
- // Update CheckDriveStatus to support checking multiple paths
202
+ // Compatibility wrapper for existing code
222
203
  inline std::vector<DriveStatus>
223
204
  CheckDriveStatus(const std::vector<std::string> &paths,
224
- DWORD timeoutMs = 1000) {
225
- try {
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 = 1000) {
240
- std::vector<std::string> paths{path};
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
- LocalFree(messageBuffer);
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
 
@@ -3,7 +3,7 @@
3
3
  #pragma once
4
4
  #include "../common/volume_metadata.h"
5
5
  #include "../common/volume_mount_points.h"
6
- #include <windows.h> // for MAX_PATH
6
+ #include "windows_arch.h"
7
7
 
8
8
  namespace FSMeta {
9
9
 
@@ -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 <windows.h>
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 = value ? (attributes | FILE_ATTRIBUTE_HIDDEN)
52
- : (attributes & ~FILE_ATTRIBUTE_HIDDEN);
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
- // Add path validation to prevent directory traversal
74
- if (path.find("..") != std::string::npos) {
75
- throw FSException("Invalid path containing '..'",
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
- auto wpath = PathConverter::ToWString(path);
80
- FileAttributeHandler handler(wpath);
81
- result = handler.isHidden();
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
- // Add path validation to prevent directory traversal
113
- if (path.find("..") != std::string::npos) {
114
- throw FSException("Invalid path containing '..'",
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 = PathConverter::ToWString(path);
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
- std::string path = info[0].As<Napi::String>();
149
- auto *worker = new GetHiddenWorker(env, std::move(path), deferred);
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(env, std::move(path), value, deferred);
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();