@photostructure/fs-metadata 0.7.1 → 0.8.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 (45) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/CLAUDE.md +1 -1
  3. package/CONTRIBUTING.md +15 -0
  4. package/README.md +2 -1
  5. package/dist/index.cjs +11 -4
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +10 -5
  8. package/dist/index.d.mts +10 -5
  9. package/dist/index.d.ts +10 -5
  10. package/dist/index.mjs +10 -3
  11. package/dist/index.mjs.map +1 -1
  12. package/doc/LINUX_API_REFERENCE.md +310 -0
  13. package/doc/MACOS_API_REFERENCE.md +367 -31
  14. package/doc/WINDOWS_API_REFERENCE.md +35 -2
  15. package/doc/gotchas.md +28 -0
  16. package/package.json +15 -18
  17. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  18. package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
  19. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  20. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  21. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  22. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  23. package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  24. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  25. package/scripts/precommit.ts +4 -1
  26. package/src/common/fd_guard.h +71 -0
  27. package/src/{darwin → common}/path_security.h +8 -5
  28. package/src/common/volume_utils.h +51 -0
  29. package/src/darwin/hidden.cpp +47 -14
  30. package/src/darwin/raii_utils.h +8 -8
  31. package/src/darwin/volume_metadata.cpp +33 -39
  32. package/src/index.ts +3 -3
  33. package/src/linux/blkid_cache.cpp +5 -11
  34. package/src/linux/blkid_cache.h +21 -0
  35. package/src/linux/gio_utils.cpp +7 -23
  36. package/src/linux/gio_utils.h +16 -40
  37. package/src/linux/gio_volume_metadata.cpp +16 -88
  38. package/src/linux/volume_metadata.cpp +35 -27
  39. package/src/options.ts +16 -3
  40. package/src/types/options.ts +1 -1
  41. package/src/windows/drive_status.h +74 -49
  42. package/src/windows/error_utils.h +2 -2
  43. package/src/windows/security_utils.h +47 -2
  44. package/src/windows/thread_pool.h +29 -4
  45. package/src/windows/volume_metadata.cpp +17 -12
@@ -49,94 +49,22 @@ void addMountMetadata(const std::string &mountPoint, VolumeMetadata &metadata) {
49
49
  }
50
50
  }
51
51
 
52
- // OPTIONAL ENHANCEMENT: Try to get rich metadata from GVolumeMonitor
53
- // This may fail (thread safety violation), but that's OK - we have basic
54
- // data
55
- try {
56
- GVolumeMonitor *monitor = MountIterator::tryGetMonitor();
57
- if (monitor) {
58
- DEBUG_LOG(
59
- "[gio::addMountMetadata] attempting GVolumeMonitor enrichment");
60
-
61
- // Try to find matching GMount for this path
62
- GList *mounts = g_volume_monitor_get_mounts(monitor);
63
- if (mounts) {
64
- for (GList *l = mounts; l != nullptr; l = l->next) {
65
- GMount *mount = G_MOUNT(l->data);
66
- if (!mount || !G_IS_MOUNT(mount)) {
67
- continue;
68
- }
69
-
70
- GFile *root = g_mount_get_root(mount);
71
- if (root) {
72
- char *path = g_file_get_path(root);
73
- if (path && mountPoint == path) {
74
- // Found matching mount - try to get rich metadata
75
-
76
- // Try to get volume label
77
- if (metadata.label.empty()) {
78
- GVolume *volume = g_mount_get_volume(mount);
79
- if (volume) {
80
- char *label = g_volume_get_name(volume);
81
- if (label) {
82
- DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s, "
83
- "label: %s} (from GVolume)",
84
- path, label);
85
- metadata.label = label;
86
- g_free(label);
87
- }
88
- g_object_unref(volume);
89
- }
90
- }
91
-
92
- // Try to get mount name
93
- if (metadata.mountName.empty()) {
94
- char *mount_name = g_mount_get_name(mount);
95
- if (mount_name) {
96
- metadata.mountName = mount_name;
97
- g_free(mount_name);
98
- }
99
- }
100
-
101
- // Try to get URI
102
- if (metadata.uri.empty()) {
103
- GFile *location = g_mount_get_default_location(mount);
104
- if (location) {
105
- char *uri = g_file_get_uri(location);
106
- if (uri) {
107
- DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s, uri: "
108
- "%s} (from GMount)",
109
- path, uri);
110
- metadata.uri = uri;
111
- g_free(uri);
112
- }
113
- g_object_unref(location);
114
- }
115
- }
116
-
117
- g_free(path);
118
- g_object_unref(root);
119
- break; // Found our mount
120
- }
121
- if (path)
122
- g_free(path);
123
- g_object_unref(root);
124
- }
125
- }
126
-
127
- // Clean up mounts list - this time correctly without double-free
128
- g_list_free_full(mounts,
129
- reinterpret_cast<GDestroyNotify>(g_object_unref));
130
- }
131
-
132
- // Note: Don't unref monitor - it's a singleton
133
- }
134
- } catch (const std::exception &e) {
135
- DEBUG_LOG("[gio::addMountMetadata] GVolumeMonitor enrichment failed "
136
- "(expected, not critical): %s",
137
- e.what());
138
- // Ignore - we have basic metadata from Unix mount API
139
- }
52
+ // NOTE: GVolumeMonitor enrichment has been removed.
53
+ //
54
+ // According to GIO documentation:
55
+ // https://docs.gtk.org/gio/class.VolumeMonitor.html
56
+ // "GVolumeMonitor is not thread-default-context aware and so should not
57
+ // be used other than from the main thread, with no thread-default-context
58
+ // active."
59
+ //
60
+ // This function is called from Napi::AsyncWorker::Execute() which runs
61
+ // on a worker thread. Using GVolumeMonitor here causes race conditions
62
+ // leading to GLib-GObject-CRITICAL errors like:
63
+ // "g_object_ref: assertion '!object_already_finalized' failed"
64
+ //
65
+ // The basic metadata (fstype, mountFrom) from g_unix_mounts_get() is
66
+ // sufficient and thread-safe. Rich metadata (label, mountName, uri) can
67
+ // be obtained from blkid or other thread-safe sources.
140
68
 
141
69
  return false; // Stop iteration, we found our mount
142
70
  });
@@ -2,12 +2,15 @@
2
2
  #include "../common/volume_metadata.h"
3
3
  #include "../common/debug_log.h"
4
4
  #include "../common/error_utils.h"
5
+ #include "../common/fd_guard.h"
5
6
  #include "../common/metadata_worker.h"
7
+ #include "../common/path_security.h"
8
+ #include "../common/volume_utils.h"
6
9
  #include "blkid_cache.h"
7
10
  #include <fcntl.h> // for open(), O_DIRECTORY, O_RDONLY, O_CLOEXEC
8
11
  #include <memory>
9
12
  #include <sys/statvfs.h>
10
- #include <unistd.h> // for close()
13
+ #include <unistd.h>
11
14
 
12
15
  #ifdef ENABLE_GIO
13
16
  #include "gio_volume_metadata.h"
@@ -32,6 +35,18 @@ public:
32
35
  DEBUG_LOG("[LinuxMetadataWorker] starting statvfs for %s",
33
36
  mountPoint.c_str());
34
37
 
38
+ // Validate and canonicalize mount point using realpath()
39
+ // This prevents directory traversal attacks and resolves symlinks
40
+ std::string error;
41
+ std::string validated_mount_point =
42
+ ValidatePathForRead(mountPoint, error);
43
+ if (validated_mount_point.empty()) {
44
+ throw FSException(error);
45
+ }
46
+
47
+ DEBUG_LOG("[LinuxMetadataWorker] Using validated mount point: %s",
48
+ validated_mount_point.c_str());
49
+
35
50
  // SECURITY: Use file descriptor-based approach to prevent TOCTOU race
36
51
  // condition
37
52
  //
@@ -45,23 +60,18 @@ public:
45
60
  // O_DIRECTORY: Ensures we're opening a directory, fails if not
46
61
  // O_RDONLY: Read-only access (sufficient for fstatvfs)
47
62
  // O_CLOEXEC: Close on exec (prevents fd leaks in multithreaded programs)
48
- int fd = open(mountPoint.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC);
63
+ int fd = open(validated_mount_point.c_str(),
64
+ O_DIRECTORY | O_RDONLY | O_CLOEXEC);
49
65
  if (fd < 0) {
50
66
  int error = errno;
51
67
  DEBUG_LOG("[LinuxMetadataWorker] open failed for %s: %s (%d)",
52
- mountPoint.c_str(), strerror(error), error);
53
- throw FSException(CreatePathErrorMessage("open", mountPoint, error));
68
+ validated_mount_point.c_str(), strerror(error), error);
69
+ throw FSException(
70
+ CreatePathErrorMessage("open", validated_mount_point, error));
54
71
  }
55
72
 
56
73
  // RAII guard to ensure file descriptor is always closed
57
- struct FdGuard {
58
- int fd;
59
- ~FdGuard() {
60
- if (fd >= 0) {
61
- close(fd);
62
- }
63
- }
64
- } fd_guard{fd};
74
+ FdGuard fd_guard(fd);
65
75
 
66
76
  // Use fstatvfs on the file descriptor instead of statvfs on the path
67
77
  // The fd holds a reference to the filesystem, preventing TOCTOU issues
@@ -69,9 +79,9 @@ public:
69
79
  if (fstatvfs(fd, &vfs) != 0) {
70
80
  int error = errno;
71
81
  DEBUG_LOG("[LinuxMetadataWorker] fstatvfs failed for %s: %s (%d)",
72
- mountPoint.c_str(), strerror(error), error);
82
+ validated_mount_point.c_str(), strerror(error), error);
73
83
  throw FSException(
74
- CreatePathErrorMessage("fstatvfs", mountPoint, error));
84
+ CreatePathErrorMessage("fstatvfs", validated_mount_point, error));
75
85
  }
76
86
 
77
87
  // fd_guard will automatically close the file descriptor when this
@@ -83,16 +93,14 @@ public:
83
93
  const uint64_t freeBlocks = static_cast<uint64_t>(vfs.f_bfree);
84
94
 
85
95
  // Check for overflow before multiplication
86
- if (blockSize > 0) {
87
- if (totalBlocks > std::numeric_limits<uint64_t>::max() / blockSize) {
88
- throw FSException("Total volume size calculation would overflow");
89
- }
90
- if (availBlocks > std::numeric_limits<uint64_t>::max() / blockSize) {
91
- throw FSException("Available space calculation would overflow");
92
- }
93
- if (freeBlocks > std::numeric_limits<uint64_t>::max() / blockSize) {
94
- throw FSException("Free space calculation would overflow");
95
- }
96
+ if (WouldOverflow(blockSize, totalBlocks)) {
97
+ throw FSException("Total volume size calculation would overflow");
98
+ }
99
+ if (WouldOverflow(blockSize, availBlocks)) {
100
+ throw FSException("Available space calculation would overflow");
101
+ }
102
+ if (WouldOverflow(blockSize, freeBlocks)) {
103
+ throw FSException("Free space calculation would overflow");
96
104
  }
97
105
 
98
106
  metadata.remote = false;
@@ -108,11 +116,11 @@ public:
108
116
  #ifdef ENABLE_GIO
109
117
  try {
110
118
  DEBUG_LOG("[LinuxMetadataWorker] collecting GIO metadata for %s",
111
- mountPoint.c_str());
112
- gio::addMountMetadata(mountPoint, metadata);
119
+ validated_mount_point.c_str());
120
+ gio::addMountMetadata(validated_mount_point, metadata);
113
121
  } catch (const std::exception &e) {
114
122
  DEBUG_LOG("[LinuxMetadataWorker] GIO error for %s: %s",
115
- mountPoint.c_str(), e.what());
123
+ validated_mount_point.c_str(), e.what());
116
124
  metadata.status = std::string("GIO warning: ") + e.what();
117
125
  }
118
126
  #endif
package/src/options.ts CHANGED
@@ -1,17 +1,30 @@
1
1
  // src/options.ts
2
2
 
3
3
  import { availableParallelism } from "node:os";
4
+ import { env } from "node:process";
4
5
  import { compactValues, isObject } from "./object";
5
6
  import { isWindows } from "./platform";
6
7
  import type { Options } from "./types/options";
7
8
 
9
+ const DefaultTimeoutMs = 5_000;
10
+
8
11
  /**
9
- * Default timeout in milliseconds for {@link Options.timeoutMs}.
12
+ * Get the default timeout in milliseconds for {@link Options.timeoutMs}.
13
+ *
14
+ * This can be overridden by setting the `FS_METADATA_TIMEOUT_MS` environment
15
+ * variable to a positive integer.
10
16
  *
11
17
  * Note that this timeout may be insufficient for some devices, like spun-down
12
18
  * optical drives or network shares that need to spin up or reconnect.
19
+ *
20
+ * @returns The timeout from env var if valid, otherwise 5000ms
13
21
  */
14
- export const TimeoutMsDefault = 5_000 as const;
22
+ export function getTimeoutMsDefault(): number {
23
+ const value = env["FS_METADATA_TIMEOUT_MS"];
24
+ if (value == null) return DefaultTimeoutMs;
25
+ const parsed = parseInt(value, 10);
26
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DefaultTimeoutMs;
27
+ }
15
28
 
16
29
  /**
17
30
  * System paths and globs that indicate system volumes
@@ -104,7 +117,7 @@ export const SkipNetworkVolumesDefault = false;
104
117
  * @see {@link optionsWithDefaults} for creating an options object with default values
105
118
  */
106
119
  export const OptionsDefault: Options = {
107
- timeoutMs: TimeoutMsDefault,
120
+ timeoutMs: getTimeoutMsDefault(),
108
121
  maxConcurrency: availableParallelism(),
109
122
  systemPathPatterns: [...SystemPathPatternsDefault],
110
123
  systemFsTypes: [...SystemFsTypesDefault],
@@ -12,7 +12,7 @@ export interface Options {
12
12
  *
13
13
  * Disable timeouts by setting this to 0.
14
14
  *
15
- * @see {@link TimeoutMsDefault}.
15
+ * @see {@link getTimeoutMsDefault}.
16
16
  */
17
17
  timeoutMs: number;
18
18
 
@@ -4,13 +4,9 @@
4
4
  #include "security_utils.h"
5
5
  #include "thread_pool.h"
6
6
  #include "windows_arch.h"
7
- #include <atomic>
8
7
  #include <chrono>
9
- #include <condition_variable>
10
8
  #include <future>
11
- #include <mutex>
12
9
  #include <string>
13
- #include <thread>
14
10
  #include <vector>
15
11
 
16
12
  namespace FSMeta {
@@ -83,7 +79,10 @@ private:
83
79
  searchPath += "*";
84
80
 
85
81
  WIN32_FIND_DATAA findData;
86
- HandleGuard findHandle(FindFirstFileExA(
82
+ // Use FindHandleGuard - search handles MUST be closed with FindClose,
83
+ // not CloseHandle. See:
84
+ // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findclose
85
+ FindHandleGuard findHandle(FindFirstFileExA(
87
86
  searchPath.c_str(), FindExInfoBasic, &findData, FindExSearchNameMatch,
88
87
  nullptr,
89
88
  FIND_FIRST_EX_LARGE_FETCH | FIND_FIRST_EX_ON_DISK_ENTRIES_ONLY));
@@ -96,69 +95,77 @@ private:
96
95
  }
97
96
 
98
97
  // Successfully opened - drive is healthy
99
- FindClose(findHandle.release());
98
+ // FindHandleGuard destructor will call FindClose automatically
100
99
  DEBUG_LOG("[DriveStatusChecker] Drive %s is healthy", path.c_str());
101
100
  return DriveStatus::Healthy;
102
101
  }
103
102
 
104
103
  public:
105
- static std::future<DriveStatus> CheckDriveAsync(const std::string &path,
106
- DWORD timeoutMs = 5000) {
104
+ // Submit a drive check to the thread pool and return a future.
105
+ // The caller is responsible for enforcing timeout via future.wait_for().
106
+ // This design avoids detached threads and race conditions.
107
+ static std::future<DriveStatus> CheckDriveAsync(const std::string &path) {
107
108
  auto promise = std::make_shared<std::promise<DriveStatus>>();
108
109
  auto future = promise->get_future();
109
110
 
110
- // Use a shared state for timeout handling
111
- auto state = std::make_shared<std::atomic<bool>>(false);
112
-
113
- GetGlobalThreadPool().Submit([path, promise, state, timeoutMs]() {
114
- // Set up timeout
115
- auto startTime = std::chrono::steady_clock::now();
116
-
117
- // Perform the check
118
- DriveStatus status = CheckDriveInternal(path);
119
-
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
- }
125
-
126
- // Only set the promise if we haven't been cancelled
127
- if (!state->load()) {
111
+ GetGlobalThreadPool().Submit([path, promise]() {
112
+ try {
113
+ DriveStatus status = CheckDriveInternal(path);
128
114
  promise->set_value(status);
115
+ } catch (const std::exception &e) {
116
+ DEBUG_LOG("[DriveStatusChecker] Exception in CheckDriveInternal: %s",
117
+ e.what());
118
+ // Set exception instead of value so caller can handle it
119
+ try {
120
+ promise->set_exception(std::current_exception());
121
+ } catch (...) {
122
+ // Promise may have been abandoned if caller timed out
123
+ }
124
+ } catch (...) {
125
+ DEBUG_LOG(
126
+ "[DriveStatusChecker] Unknown exception in CheckDriveInternal");
127
+ try {
128
+ promise->set_exception(std::current_exception());
129
+ } catch (...) {
130
+ // Promise may have been abandoned if caller timed out
131
+ }
129
132
  }
130
133
  });
131
134
 
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();
144
- }
145
-
146
135
  return future;
147
136
  }
148
137
 
138
+ // Overload that accepts timeoutMs for API compatibility (timeout is enforced
139
+ // in CheckDrive, not here)
140
+ static std::future<DriveStatus> CheckDriveAsync(const std::string &path,
141
+ DWORD /*timeoutMs*/) {
142
+ return CheckDriveAsync(path);
143
+ }
144
+
149
145
  static DriveStatus CheckDrive(const std::string &path,
150
146
  DWORD timeoutMs = 5000) {
151
147
  try {
152
- auto future = CheckDriveAsync(path, timeoutMs);
148
+ auto future = CheckDriveAsync(path);
149
+
150
+ // Use wait_for to enforce timeout - no detached threads needed!
151
+ // The worker thread continues running but we return Timeout to caller.
152
+ // The promise will eventually be satisfied (or abandoned).
153
+ auto waitResult = future.wait_for(std::chrono::milliseconds(timeoutMs));
153
154
 
154
- if (future.wait_for(std::chrono::milliseconds(timeoutMs)) ==
155
- std::future_status::timeout) {
155
+ if (waitResult == std::future_status::timeout) {
156
+ DEBUG_LOG("[DriveStatusChecker] Timeout waiting for drive %s",
157
+ path.c_str());
156
158
  return DriveStatus::Timeout;
157
159
  }
158
160
 
161
+ // Future is ready - get the result (may throw if worker set exception)
159
162
  return future.get();
163
+ } catch (const std::exception &e) {
164
+ DEBUG_LOG("[DriveStatusChecker] Exception checking drive %s: %s",
165
+ path.c_str(), e.what());
166
+ return DriveStatus::Unknown;
160
167
  } catch (...) {
161
- DEBUG_LOG("[DriveStatusChecker] Exception checking drive %s",
168
+ DEBUG_LOG("[DriveStatusChecker] Unknown exception checking drive %s",
162
169
  path.c_str());
163
170
  return DriveStatus::Unknown;
164
171
  }
@@ -172,24 +179,42 @@ public:
172
179
  futures.reserve(paths.size());
173
180
 
174
181
  // Launch all checks concurrently
182
+ auto startTime = std::chrono::steady_clock::now();
175
183
  for (const auto &path : paths) {
176
- futures.push_back(CheckDriveAsync(path, timeoutMs));
184
+ futures.push_back(CheckDriveAsync(path));
177
185
  }
178
186
 
179
- // Collect results
187
+ // Collect results with timeout
180
188
  std::vector<DriveStatus> results;
181
189
  results.reserve(paths.size());
182
190
 
183
191
  for (size_t i = 0; i < futures.size(); ++i) {
184
192
  try {
185
- if (futures[i].wait_for(std::chrono::milliseconds(timeoutMs)) ==
186
- std::future_status::timeout) {
193
+ // Calculate remaining time for this future
194
+ auto elapsed = std::chrono::steady_clock::now() - startTime;
195
+ auto elapsedMs =
196
+ std::chrono::duration_cast<std::chrono::milliseconds>(elapsed)
197
+ .count();
198
+ auto remainingMs = (elapsedMs < static_cast<long long>(timeoutMs))
199
+ ? static_cast<DWORD>(timeoutMs - elapsedMs)
200
+ : 0;
201
+
202
+ if (remainingMs == 0 ||
203
+ futures[i].wait_for(std::chrono::milliseconds(remainingMs)) ==
204
+ std::future_status::timeout) {
205
+ DEBUG_LOG("[DriveStatusChecker] Timeout waiting for drive %s",
206
+ paths[i].c_str());
187
207
  results.push_back(DriveStatus::Timeout);
188
208
  } else {
189
209
  results.push_back(futures[i].get());
190
210
  }
211
+ } catch (const std::exception &e) {
212
+ DEBUG_LOG(
213
+ "[DriveStatusChecker] Exception getting result for drive %s: %s",
214
+ paths[i].c_str(), e.what());
215
+ results.push_back(DriveStatus::Unknown);
191
216
  } catch (...) {
192
- DEBUG_LOG("[DriveStatusChecker] Exception getting result for drive %s",
217
+ DEBUG_LOG("[DriveStatusChecker] Unknown exception for drive %s",
193
218
  paths[i].c_str());
194
219
  results.push_back(DriveStatus::Unknown);
195
220
  }
@@ -62,8 +62,8 @@ private:
62
62
  size_t size = FormatMessageA(
63
63
  FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
64
64
  FORMAT_MESSAGE_IGNORE_INSERTS,
65
- NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
66
- (LPSTR)&messageBuffer, 0, NULL);
65
+ nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
66
+ (LPSTR)&messageBuffer, 0, nullptr);
67
67
 
68
68
  // RAII guard ensures LocalFree is called even if exception thrown
69
69
  LocalFreeGuard guard(messageBuffer);
@@ -124,7 +124,7 @@ public:
124
124
 
125
125
  // Check if process has required privileges
126
126
  static bool HasRequiredPrivileges() {
127
- HANDLE token;
127
+ HANDLE token = nullptr;
128
128
  if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
129
129
  return false;
130
130
  }
@@ -169,7 +169,8 @@ public:
169
169
  }
170
170
  };
171
171
 
172
- // RAII wrapper for HANDLE resources
172
+ // RAII wrapper for HANDLE resources (uses CloseHandle)
173
+ // For search handles from FindFirstFile*, use FindHandleGuard instead
173
174
  class HandleGuard {
174
175
  HANDLE handle;
175
176
 
@@ -213,6 +214,48 @@ public:
213
214
  }
214
215
  };
215
216
 
217
+ // RAII wrapper for search handles from FindFirstFile/FindFirstFileEx
218
+ // These handles MUST be closed with FindClose, not CloseHandle.
219
+ // See:
220
+ // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findclose
221
+ class FindHandleGuard {
222
+ HANDLE handle;
223
+
224
+ public:
225
+ explicit FindHandleGuard(HANDLE h) : handle(h) {}
226
+
227
+ ~FindHandleGuard() {
228
+ if (handle != INVALID_HANDLE_VALUE) {
229
+ FindClose(handle);
230
+ }
231
+ }
232
+
233
+ FindHandleGuard(FindHandleGuard &&other) noexcept : handle(other.handle) {
234
+ other.handle = INVALID_HANDLE_VALUE;
235
+ }
236
+
237
+ FindHandleGuard &operator=(FindHandleGuard &&other) noexcept {
238
+ if (this != &other) {
239
+ if (handle != INVALID_HANDLE_VALUE) {
240
+ FindClose(handle);
241
+ }
242
+ handle = other.handle;
243
+ other.handle = INVALID_HANDLE_VALUE;
244
+ }
245
+ return *this;
246
+ }
247
+
248
+ // Delete copy operations
249
+ FindHandleGuard(const FindHandleGuard &) = delete;
250
+ FindHandleGuard &operator=(const FindHandleGuard &) = delete;
251
+
252
+ HANDLE get() const { return handle; }
253
+
254
+ // Check if handle is valid (FindFirstFile returns INVALID_HANDLE_VALUE on
255
+ // failure, not NULL)
256
+ explicit operator bool() const { return handle != INVALID_HANDLE_VALUE; }
257
+ };
258
+
216
259
  // RAII wrapper for critical sections
217
260
  class CriticalSectionGuard {
218
261
  CRITICAL_SECTION cs;
@@ -244,6 +287,8 @@ public:
244
287
  // Delete copy/move
245
288
  Lock(const Lock &) = delete;
246
289
  Lock &operator=(const Lock &) = delete;
290
+ Lock(Lock &&) = delete;
291
+ Lock &operator=(Lock &&) = delete;
247
292
  };
248
293
  };
249
294
 
@@ -6,6 +6,8 @@
6
6
  #include <functional>
7
7
  #include <memory>
8
8
  #include <queue>
9
+ #include <stdexcept>
10
+ #include <string>
9
11
  #include <thread>
10
12
  #include <vector>
11
13
 
@@ -16,22 +18,45 @@ class WorkQueue {
16
18
  private:
17
19
  std::queue<std::function<void()>> tasks;
18
20
  CRITICAL_SECTION cs;
19
- HANDLE workAvailable;
21
+ HANDLE workAvailable = nullptr;
20
22
  std::atomic<bool> shutdown{false};
23
+ bool initialized = false;
21
24
 
22
25
  public:
23
26
  WorkQueue() {
24
27
  InitializeCriticalSection(&cs);
28
+ // CreateEvent returns NULL on failure, not INVALID_HANDLE_VALUE.
29
+ // See:
30
+ // https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa
25
31
  workAvailable = CreateEvent(nullptr, FALSE, FALSE, nullptr);
32
+ if (workAvailable == nullptr) {
33
+ DWORD error = GetLastError();
34
+ DeleteCriticalSection(&cs);
35
+ DEBUG_LOG("[WorkQueue] CreateEvent failed with error: %lu", error);
36
+ throw std::runtime_error("WorkQueue: CreateEvent failed with error " +
37
+ std::to_string(error));
38
+ }
39
+ initialized = true;
26
40
  }
27
41
 
28
42
  ~WorkQueue() {
29
43
  shutdown = true;
30
- SetEvent(workAvailable);
31
- CloseHandle(workAvailable);
32
- DeleteCriticalSection(&cs);
44
+ if (workAvailable != nullptr) {
45
+ SetEvent(workAvailable);
46
+ CloseHandle(workAvailable);
47
+ workAvailable = nullptr;
48
+ }
49
+ if (initialized) {
50
+ DeleteCriticalSection(&cs);
51
+ }
33
52
  }
34
53
 
54
+ // Delete copy/move operations - WorkQueue manages non-copyable resources
55
+ WorkQueue(const WorkQueue &) = delete;
56
+ WorkQueue &operator=(const WorkQueue &) = delete;
57
+ WorkQueue(WorkQueue &&) = delete;
58
+ WorkQueue &operator=(WorkQueue &&) = delete;
59
+
35
60
  void Push(std::function<void()> task) {
36
61
  EnterCriticalSection(&cs);
37
62
  tasks.push(std::move(task));
@@ -19,12 +19,12 @@ namespace {
19
19
  class WNetConnection {
20
20
  std::string drivePath;
21
21
  std::unique_ptr<char[]> buffer;
22
- DWORD bufferSize;
22
+ DWORD bufferSize = MAX_PATH;
23
23
  bool isValid = false;
24
24
 
25
25
  public:
26
26
  explicit WNetConnection(const std::string &path)
27
- : drivePath(path.substr(0, 2)), bufferSize(MAX_PATH) {
27
+ : drivePath(path.substr(0, 2)) {
28
28
 
29
29
  // Allocate initial buffer
30
30
  buffer = std::make_unique<char[]>(bufferSize);
@@ -41,6 +41,7 @@ public:
41
41
  isValid = (result == NO_ERROR);
42
42
  }
43
43
 
44
+ ~WNetConnection() = default;
44
45
  WNetConnection(WNetConnection &&) noexcept = default;
45
46
  WNetConnection &operator=(WNetConnection &&) noexcept = default;
46
47
 
@@ -85,12 +86,14 @@ inline std::string GetVolumeGUID(const std::string &mountPoint) {
85
86
  // RAII wrapper for volume information
86
87
  class VolumeInfo {
87
88
  static constexpr DWORD VOLUME_NAME_SIZE = MAX_PATH + 1; // 261 characters
88
- char volumeName[VOLUME_NAME_SIZE];
89
- char fstype[VOLUME_NAME_SIZE];
90
- DWORD serialNumber;
91
- DWORD maxComponentLen;
92
- DWORD fsFlags;
93
- bool valid;
89
+ // Initialize all members to prevent reading uninitialized memory
90
+ // if GetVolumeInformationA fails with ERROR_NOT_READY
91
+ char volumeName[VOLUME_NAME_SIZE] = {0};
92
+ char fstype[VOLUME_NAME_SIZE] = {0};
93
+ DWORD serialNumber = 0;
94
+ DWORD maxComponentLen = 0;
95
+ DWORD fsFlags = 0;
96
+ bool valid = false;
94
97
 
95
98
  public:
96
99
  explicit VolumeInfo(const std::string &mountPoint) {
@@ -111,10 +114,12 @@ public:
111
114
 
112
115
  // RAII wrapper for disk space information
113
116
  class DiskSpaceInfo {
114
- ULARGE_INTEGER totalBytes;
115
- ULARGE_INTEGER freeBytes;
116
- ULARGE_INTEGER totalFreeBytes;
117
- bool valid;
117
+ // Initialize all members to prevent reading uninitialized memory
118
+ // if GetDiskFreeSpaceExA fails with ERROR_NOT_READY
119
+ ULARGE_INTEGER totalBytes = {0};
120
+ ULARGE_INTEGER freeBytes = {0};
121
+ ULARGE_INTEGER totalFreeBytes = {0};
122
+ bool valid = false;
118
123
 
119
124
  public:
120
125
  explicit DiskSpaceInfo(const std::string &mountPoint) {