@photostructure/fs-metadata 0.6.1 → 0.7.1
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 +7 -1
- package/CLAUDE.md +141 -315
- package/CODE_OF_CONDUCT.md +11 -11
- package/CONTRIBUTING.md +1 -1
- package/README.md +34 -103
- package/binding.gyp +97 -22
- package/claude.sh +23 -0
- package/dist/index.cjs +51 -21
- 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 +51 -21
- package/dist/index.mjs.map +1 -1
- package/doc/C++_REVIEW_TODO.md +97 -25
- package/doc/GPG_RELEASE_HOWTO.md +44 -13
- package/doc/MACOS_API_REFERENCE.md +469 -0
- package/doc/SECURITY_AUDIT_2025.md +809 -0
- package/doc/SSH_RELEASE_HOWTO.md +28 -24
- package/doc/WINDOWS_API_REFERENCE.md +422 -0
- package/doc/WINDOWS_ARM64_SECURITY.md +161 -0
- package/doc/WINDOWS_DEBUG_GUIDE.md +9 -2
- 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 +23 -0
- package/package.json +61 -36
- 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 +690 -99
- 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 +12 -1
- package/scripts/prebuildify-wrapper.ts +77 -0
- package/scripts/precommit.ts +45 -20
- package/scripts/sanitizers-test.sh +1 -1
- 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/remote_info.ts +5 -3
- package/src/stack_path.ts +8 -6
- package/src/string_enum.ts +1 -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 +154 -27
- 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
|
@@ -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();
|
|
@@ -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
|
package/src/windows/string.h
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|