@photostructure/fs-metadata 0.6.1 → 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 (89) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/CLAUDE.md +141 -315
  3. package/CODE_OF_CONDUCT.md +11 -11
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +34 -103
  6. package/binding.gyp +97 -22
  7. package/claude.sh +23 -0
  8. package/dist/index.cjs +51 -21
  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 +51 -21
  14. package/dist/index.mjs.map +1 -1
  15. package/doc/C++_REVIEW_TODO.md +97 -25
  16. package/doc/GPG_RELEASE_HOWTO.md +44 -13
  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 +28 -24
  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 +9 -2
  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 +23 -0
  35. package/package.json +61 -36
  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 +690 -99
  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 +12 -1
  51. package/scripts/prebuildify-wrapper.ts +77 -0
  52. package/scripts/precommit.ts +45 -20
  53. package/scripts/sanitizers-test.sh +1 -1
  54. package/src/common/volume_metadata.h +6 -0
  55. package/src/darwin/hidden.cpp +73 -25
  56. package/src/darwin/path_security.h +149 -0
  57. package/src/darwin/raii_utils.h +104 -4
  58. package/src/darwin/volume_metadata.cpp +132 -58
  59. package/src/darwin/volume_mount_points.cpp +80 -47
  60. package/src/hidden.ts +36 -13
  61. package/src/linux/gio_mount_points.cpp +17 -18
  62. package/src/linux/gio_utils.cpp +92 -37
  63. package/src/linux/gio_utils.h +11 -5
  64. package/src/linux/gio_volume_metadata.cpp +111 -48
  65. package/src/linux/volume_metadata.cpp +67 -4
  66. package/src/object.ts +1 -0
  67. package/src/options.ts +6 -0
  68. package/src/path.ts +11 -0
  69. package/src/remote_info.ts +5 -3
  70. package/src/stack_path.ts +8 -6
  71. package/src/string_enum.ts +1 -0
  72. package/src/test-utils/memory-test-core.ts +336 -0
  73. package/src/test-utils/memory-test-runner.ts +108 -0
  74. package/src/test-utils/platform.ts +46 -1
  75. package/src/test-utils/worker-thread-helper.cjs +154 -27
  76. package/src/types/native_bindings.ts +1 -1
  77. package/src/types/options.ts +6 -0
  78. package/src/windows/drive_status.h +133 -163
  79. package/src/windows/error_utils.h +54 -3
  80. package/src/windows/fs_meta.h +1 -1
  81. package/src/windows/hidden.cpp +60 -43
  82. package/src/windows/security_utils.h +250 -0
  83. package/src/windows/string.h +68 -11
  84. package/src/windows/system_volume.h +1 -1
  85. package/src/windows/thread_pool.h +206 -0
  86. package/src/windows/volume_metadata.cpp +11 -6
  87. package/src/windows/volume_mount_points.cpp +8 -7
  88. package/src/windows/windows_arch.h +39 -0
  89. package/scripts/check-memory.mjs +0 -123
@@ -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();
@@ -0,0 +1,250 @@
1
+ // src/windows/security_utils.h
2
+ #pragma once
3
+ #include "windows_arch.h"
4
+ #include <algorithm>
5
+ #include <cctype>
6
+ #include <pathcch.h>
7
+ #include <sddl.h>
8
+ #include <stdexcept>
9
+ #include <string>
10
+ #include <strsafe.h>
11
+ #include <vector>
12
+
13
+ #pragma comment(lib, "Pathcch.lib")
14
+
15
+ namespace FSMeta {
16
+
17
+ class SecurityUtils {
18
+ public:
19
+ // Path validation to prevent security vulnerabilities
20
+ static bool IsPathSecure(const std::string &path) {
21
+ // Check for empty path
22
+ if (path.empty()) {
23
+ return false;
24
+ }
25
+
26
+ // Check for excessive length (prevent buffer overflow)
27
+ // Windows 10+ supports paths up to 32,768 characters (PATHCCH_MAX_CCH)
28
+ // UTF-8 worst case: 3 bytes per wide character
29
+ if (path.length() > PATHCCH_MAX_CCH * 3) {
30
+ return false;
31
+ }
32
+
33
+ // Check for null bytes
34
+ if (path.find('\0') != std::string::npos) {
35
+ return false;
36
+ }
37
+
38
+ // Check for directory traversal - be more strict
39
+ // Check if path starts with ..
40
+ if (path.size() >= 2 && path.substr(0, 2) == "..") {
41
+ return false;
42
+ }
43
+
44
+ // Check for basic directory traversal
45
+ if (path.find("..\\") != std::string::npos ||
46
+ path.find("../") != std::string::npos ||
47
+ path.find("\\..") != std::string::npos ||
48
+ path.find("/..") != std::string::npos) {
49
+ return false;
50
+ }
51
+
52
+ // Check for device names that could be exploited
53
+ static const std::vector<std::string> deviceNames = {
54
+ "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4",
55
+ "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3",
56
+ "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"};
57
+
58
+ std::string upperPath = path;
59
+ std::transform(upperPath.begin(), upperPath.end(), upperPath.begin(),
60
+ ::toupper);
61
+
62
+ for (const auto &device : deviceNames) {
63
+ // Check for device name in path components
64
+ if (upperPath.find("\\" + device) != std::string::npos ||
65
+ upperPath.find("/" + device) != std::string::npos) {
66
+ // Check if it's followed by a backslash, forward slash, dot, or end of
67
+ // string
68
+ size_t pos = upperPath.find("\\" + device);
69
+ if (pos == std::string::npos) {
70
+ pos = upperPath.find("/" + device);
71
+ }
72
+ if (pos != std::string::npos) {
73
+ size_t endPos = pos + 1 + device.length();
74
+ if (endPos >= upperPath.length() || upperPath[endPos] == '\\' ||
75
+ upperPath[endPos] == '/' || upperPath[endPos] == '.') {
76
+ return false;
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ // Check for alternate data streams (except drive letter colon)
83
+ size_t colonPos = path.find(':');
84
+ if (colonPos != std::string::npos) {
85
+ // Allow only if it's a drive letter at position 1
86
+ if (!(colonPos == 1 && isalpha(path[0]) &&
87
+ (path.length() == 2 || path[2] == '\\' || path[2] == '/'))) {
88
+ return false; // Alternate data stream attempt
89
+ }
90
+ // Check for multiple colons
91
+ if (path.find(':', colonPos + 1) != std::string::npos) {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ // Check for UNC path injection
97
+ if (path.size() >= 4) {
98
+ if ((path.substr(0, 4) == "\\\\?\\") ||
99
+ (path.substr(0, 4) == "\\\\.\\")) {
100
+ return false; // Device namespace paths are dangerous
101
+ }
102
+ }
103
+
104
+ return true;
105
+ }
106
+
107
+ // Safe path normalization with long path support (up to 32,768 characters)
108
+ // Uses PathCchCanonicalizeEx to support Windows 10+ long paths
109
+ static std::wstring NormalizePath(const std::wstring &path) {
110
+ // Use PATHCCH_MAX_CCH (32,768) instead of MAX_PATH (260)
111
+ wchar_t canonicalPath[PATHCCH_MAX_CCH];
112
+ HRESULT hr = PathCchCanonicalizeEx(
113
+ canonicalPath, PATHCCH_MAX_CCH, path.c_str(),
114
+ PATHCCH_ALLOW_LONG_PATHS // Enable long path support for Windows 10+
115
+ );
116
+
117
+ if (FAILED(hr)) {
118
+ throw std::runtime_error("Failed to canonicalize path: HRESULT " +
119
+ std::to_string(hr));
120
+ }
121
+
122
+ return std::wstring(canonicalPath);
123
+ }
124
+
125
+ // Check if process has required privileges
126
+ static bool HasRequiredPrivileges() {
127
+ HANDLE token;
128
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
129
+ return false;
130
+ }
131
+
132
+ TOKEN_ELEVATION elevation;
133
+ DWORD size = sizeof(elevation);
134
+ BOOL result = GetTokenInformation(token, TokenElevation, &elevation,
135
+ sizeof(elevation), &size);
136
+
137
+ CloseHandle(token);
138
+ return result && elevation.TokenIsElevated;
139
+ }
140
+
141
+ // Safe string conversion with validation
142
+ // Default max length supports long paths (PATHCCH_MAX_CCH = 32,768 wide
143
+ // chars) UTF-8 worst case: 3 bytes per wide character
144
+ static std::wstring SafeStringToWide(const std::string &str,
145
+ size_t maxLength = PATHCCH_MAX_CCH * 3) {
146
+ if (str.empty()) {
147
+ return L"";
148
+ }
149
+
150
+ if (str.length() > maxLength) {
151
+ throw std::invalid_argument("String exceeds maximum allowed length");
152
+ }
153
+
154
+ // Validate UTF-8 sequence before conversion
155
+ int requiredSize = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
156
+ str.c_str(), -1, nullptr, 0);
157
+
158
+ if (requiredSize == 0) {
159
+ throw std::runtime_error("Invalid UTF-8 sequence");
160
+ }
161
+
162
+ std::wstring result(requiredSize - 1, L'\0');
163
+ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, str.c_str(), -1,
164
+ &result[0], requiredSize)) {
165
+ throw std::runtime_error("Failed to convert string");
166
+ }
167
+
168
+ return result;
169
+ }
170
+ };
171
+
172
+ // RAII wrapper for HANDLE resources
173
+ class HandleGuard {
174
+ HANDLE handle;
175
+
176
+ public:
177
+ explicit HandleGuard(HANDLE h) : handle(h) {}
178
+
179
+ ~HandleGuard() {
180
+ if (handle && handle != INVALID_HANDLE_VALUE) {
181
+ CloseHandle(handle);
182
+ }
183
+ }
184
+
185
+ HandleGuard(HandleGuard &&other) noexcept : handle(other.handle) {
186
+ other.handle = nullptr;
187
+ }
188
+
189
+ HandleGuard &operator=(HandleGuard &&other) noexcept {
190
+ if (this != &other) {
191
+ if (handle && handle != INVALID_HANDLE_VALUE) {
192
+ CloseHandle(handle);
193
+ }
194
+ handle = other.handle;
195
+ other.handle = nullptr;
196
+ }
197
+ return *this;
198
+ }
199
+
200
+ // Delete copy operations
201
+ HandleGuard(const HandleGuard &) = delete;
202
+ HandleGuard &operator=(const HandleGuard &) = delete;
203
+
204
+ HANDLE get() const { return handle; }
205
+ HANDLE release() {
206
+ HANDLE h = handle;
207
+ handle = nullptr;
208
+ return h;
209
+ }
210
+
211
+ explicit operator bool() const {
212
+ return handle && handle != INVALID_HANDLE_VALUE;
213
+ }
214
+ };
215
+
216
+ // RAII wrapper for critical sections
217
+ class CriticalSectionGuard {
218
+ CRITICAL_SECTION cs;
219
+
220
+ public:
221
+ CriticalSectionGuard() { InitializeCriticalSection(&cs); }
222
+
223
+ ~CriticalSectionGuard() { DeleteCriticalSection(&cs); }
224
+
225
+ // Delete copy/move operations
226
+ CriticalSectionGuard(const CriticalSectionGuard &) = delete;
227
+ CriticalSectionGuard &operator=(const CriticalSectionGuard &) = delete;
228
+ CriticalSectionGuard(CriticalSectionGuard &&) = delete;
229
+ CriticalSectionGuard &operator=(CriticalSectionGuard &&) = delete;
230
+
231
+ void Enter() { EnterCriticalSection(&cs); }
232
+ void Leave() { LeaveCriticalSection(&cs); }
233
+ BOOL TryEnter() { return TryEnterCriticalSection(&cs); }
234
+
235
+ // RAII lock helper
236
+ class Lock {
237
+ CriticalSectionGuard &guard;
238
+
239
+ public:
240
+ explicit Lock(CriticalSectionGuard &g) : guard(g) { guard.Enter(); }
241
+
242
+ ~Lock() { guard.Leave(); }
243
+
244
+ // Delete copy/move
245
+ Lock(const Lock &) = delete;
246
+ Lock &operator=(const Lock &) = delete;
247
+ };
248
+ };
249
+
250
+ } // namespace FSMeta
@@ -1,22 +1,55 @@
1
1
  // src/windows/string.h
2
2
 
3
3
  #pragma once
4
+ #include "../common/debug_log.h"
5
+ #include "windows_arch.h"
6
+ #include <climits>
7
+ #include <pathcch.h>
8
+ #include <stdexcept>
4
9
  #include <string>
5
- #include <windows.h>
6
10
 
7
11
  namespace FSMeta {
8
12
 
13
+ // Maximum reasonable size for string conversions (1MB)
14
+ // Prevents allocation of excessive memory due to overflow or malicious input
15
+ constexpr int MAX_STRING_CONVERSION_SIZE = 1024 * 1024;
16
+
9
17
  inline std::string WideToUtf8(const WCHAR *wide) {
10
18
  if (!wide || wide[0] == 0)
11
19
  return "";
12
20
 
21
+ // Get required buffer size
13
22
  int size =
14
23
  WideCharToMultiByte(CP_UTF8, 0, wide, -1, nullptr, 0, nullptr, nullptr);
15
- if (size <= 0)
24
+
25
+ // Validate size is reasonable
26
+ if (size <= 0) {
27
+ DEBUG_LOG("[WideToUtf8] WideCharToMultiByte returned invalid size: %d",
28
+ size);
16
29
  return "";
30
+ }
31
+
32
+ // Check for overflow: size - 1 should be positive and reasonable
33
+ // INT_MAX - 1 check prevents overflow in subtraction
34
+ // MAX_STRING_CONVERSION_SIZE check prevents excessive allocations
35
+ if (size > INT_MAX - 1 || size > MAX_STRING_CONVERSION_SIZE) {
36
+ DEBUG_LOG("[WideToUtf8] Size too large: %d (max: %d)", size,
37
+ MAX_STRING_CONVERSION_SIZE);
38
+ throw std::runtime_error(
39
+ "String conversion size exceeds reasonable limits");
40
+ }
41
+
42
+ std::string result(static_cast<size_t>(size - 1), 0);
43
+
44
+ // Perform conversion and check result
45
+ int written = WideCharToMultiByte(CP_UTF8, 0, wide, -1, &result[0], size,
46
+ nullptr, nullptr);
47
+ if (written <= 0) {
48
+ DEBUG_LOG("[WideToUtf8] WideCharToMultiByte conversion failed: %lu",
49
+ GetLastError());
50
+ throw std::runtime_error("String conversion failed");
51
+ }
17
52
 
18
- std::string result(size - 1, 0);
19
- WideCharToMultiByte(CP_UTF8, 0, wide, -1, &result[0], size, nullptr, nullptr);
20
53
  return result;
21
54
  }
22
55
 
@@ -27,18 +60,42 @@ public:
27
60
  return L"";
28
61
  }
29
62
 
30
- int wlen = MultiByteToWideChar(CP_UTF8, 0, path.c_str(),
63
+ // Validate input length fits in int (required by MultiByteToWideChar)
64
+ if (path.length() > static_cast<size_t>(INT_MAX)) {
65
+ DEBUG_LOG("[ToWString] Input path length exceeds INT_MAX: %zu",
66
+ path.length());
67
+ throw std::runtime_error("Input string too large for conversion");
68
+ }
69
+
70
+ int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path.c_str(),
31
71
  static_cast<int>(path.length()), nullptr, 0);
32
72
 
33
- if (wlen == 0) {
73
+ // Validate wlen
74
+ if (wlen <= 0) {
75
+ DEBUG_LOG(
76
+ "[ToWString] MultiByteToWideChar returned invalid size: %d (error: "
77
+ "%lu)",
78
+ wlen, GetLastError());
34
79
  return L"";
35
80
  }
36
81
 
37
- std::wstring wpath(wlen, 0);
38
- if (!MultiByteToWideChar(CP_UTF8, 0, path.c_str(),
39
- static_cast<int>(path.length()), &wpath[0],
40
- wlen)) {
41
- return L"";
82
+ // Check for reasonable size (PATHCCH_MAX_CCH for paths)
83
+ if (wlen > PATHCCH_MAX_CCH) {
84
+ DEBUG_LOG("[ToWString] Size exceeds maximum path length: %d (max: %d)",
85
+ wlen, PATHCCH_MAX_CCH);
86
+ throw std::runtime_error("Path too long for conversion");
87
+ }
88
+
89
+ std::wstring wpath(static_cast<size_t>(wlen), 0);
90
+
91
+ // Perform conversion with error checking
92
+ int written =
93
+ MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path.c_str(),
94
+ static_cast<int>(path.length()), &wpath[0], wlen);
95
+ if (written <= 0) {
96
+ DEBUG_LOG("[ToWString] MultiByteToWideChar conversion failed: %lu",
97
+ GetLastError());
98
+ throw std::runtime_error("String conversion failed");
42
99
  }
43
100
 
44
101
  return wpath;
@@ -2,10 +2,10 @@
2
2
  #pragma once
3
3
  #include "../common/debug_log.h"
4
4
  #include "string.h"
5
+ #include "windows_arch.h"
5
6
  #include <PathCch.h>
6
7
  #include <shlobj.h> // For SHGetFolderPathW and CSIDL constants
7
8
  #include <string>
8
- #include <windows.h>
9
9
 
10
10
  // If FILE_SUPPORTS_SYSTEM_PATHS is not defined (older SDK)
11
11
  #ifndef FILE_SUPPORTS_SYSTEM_PATHS