@photostructure/fs-metadata 0.7.0 → 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.
- package/CHANGELOG.md +34 -1
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +15 -0
- package/README.md +2 -1
- package/dist/index.cjs +11 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.mts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.mjs +10 -3
- package/dist/index.mjs.map +1 -1
- package/doc/LINUX_API_REFERENCE.md +310 -0
- package/doc/MACOS_API_REFERENCE.md +367 -31
- package/doc/WINDOWS_API_REFERENCE.md +35 -2
- package/doc/gotchas.md +28 -0
- package/package.json +17 -20
- 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/precommit.ts +4 -1
- package/src/common/fd_guard.h +71 -0
- package/src/{darwin → common}/path_security.h +8 -5
- package/src/common/volume_utils.h +51 -0
- package/src/darwin/hidden.cpp +47 -14
- package/src/darwin/raii_utils.h +8 -8
- package/src/darwin/volume_metadata.cpp +33 -39
- package/src/index.ts +3 -3
- package/src/linux/blkid_cache.cpp +5 -11
- package/src/linux/blkid_cache.h +21 -0
- package/src/linux/gio_utils.cpp +7 -23
- package/src/linux/gio_utils.h +16 -40
- package/src/linux/gio_volume_metadata.cpp +16 -88
- package/src/linux/volume_metadata.cpp +35 -27
- package/src/options.ts +16 -3
- package/src/types/options.ts +1 -1
- package/src/windows/drive_status.h +74 -49
- package/src/windows/error_utils.h +2 -2
- package/src/windows/security_utils.h +47 -2
- package/src/windows/thread_pool.h +29 -4
- package/src/windows/volume_metadata.cpp +17 -12
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/common/fd_guard.h
|
|
2
|
+
// RAII wrapper for POSIX file descriptors
|
|
3
|
+
// Ensures file descriptors are properly closed, even when exceptions occur
|
|
4
|
+
|
|
5
|
+
#pragma once
|
|
6
|
+
|
|
7
|
+
#include <unistd.h>
|
|
8
|
+
|
|
9
|
+
namespace FSMeta {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* RAII guard for POSIX file descriptors.
|
|
13
|
+
*
|
|
14
|
+
* Automatically closes the file descriptor when destroyed, preventing
|
|
15
|
+
* resource leaks. Particularly important for:
|
|
16
|
+
* - Exception safety (fd closed even if exception thrown)
|
|
17
|
+
* - Early returns (fd closed regardless of return path)
|
|
18
|
+
* - Fork safety (when combined with O_CLOEXEC)
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
22
|
+
* if (fd < 0) { handle error }
|
|
23
|
+
* FdGuard guard(fd);
|
|
24
|
+
* // fd is automatically closed when guard goes out of scope
|
|
25
|
+
*/
|
|
26
|
+
class FdGuard {
|
|
27
|
+
public:
|
|
28
|
+
explicit FdGuard(int fd) noexcept : fd_(fd) {}
|
|
29
|
+
|
|
30
|
+
~FdGuard() noexcept {
|
|
31
|
+
if (fd_ >= 0) {
|
|
32
|
+
close(fd_);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get the underlying file descriptor
|
|
37
|
+
int get() const noexcept { return fd_; }
|
|
38
|
+
|
|
39
|
+
// Release ownership of the file descriptor (caller must close it)
|
|
40
|
+
int release() noexcept {
|
|
41
|
+
int fd = fd_;
|
|
42
|
+
fd_ = -1;
|
|
43
|
+
return fd;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if the guard holds a valid file descriptor
|
|
47
|
+
bool isValid() const noexcept { return fd_ >= 0; }
|
|
48
|
+
|
|
49
|
+
// Non-copyable
|
|
50
|
+
FdGuard(const FdGuard &) = delete;
|
|
51
|
+
FdGuard &operator=(const FdGuard &) = delete;
|
|
52
|
+
|
|
53
|
+
// Movable
|
|
54
|
+
FdGuard(FdGuard &&other) noexcept : fd_(other.fd_) { other.fd_ = -1; }
|
|
55
|
+
|
|
56
|
+
FdGuard &operator=(FdGuard &&other) noexcept {
|
|
57
|
+
if (this != &other) {
|
|
58
|
+
if (fd_ >= 0) {
|
|
59
|
+
close(fd_);
|
|
60
|
+
}
|
|
61
|
+
fd_ = other.fd_;
|
|
62
|
+
other.fd_ = -1;
|
|
63
|
+
}
|
|
64
|
+
return *this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private:
|
|
68
|
+
int fd_;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
} // namespace FSMeta
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
// Secure path validation
|
|
3
|
-
//
|
|
1
|
+
// src/common/path_security.h
|
|
2
|
+
// Secure path validation using POSIX realpath()
|
|
3
|
+
// Based on recommendations from Apple's Secure Coding Guide and CERT C
|
|
4
|
+
// guidelines References:
|
|
5
|
+
// -
|
|
4
6
|
// https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/RaceConditions.html
|
|
7
|
+
// - https://wiki.sei.cmu.edu/confluence/x/DtcxBQ (FIO02-C)
|
|
5
8
|
|
|
6
9
|
#pragma once
|
|
7
10
|
|
|
8
|
-
#include "
|
|
9
|
-
#include "
|
|
11
|
+
#include "debug_log.h"
|
|
12
|
+
#include "error_utils.h"
|
|
10
13
|
#include <cerrno>
|
|
11
14
|
#include <cstring>
|
|
12
15
|
#include <string>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/common/volume_utils.h
|
|
2
|
+
// Shared utilities for volume metadata calculations
|
|
3
|
+
|
|
4
|
+
#pragma once
|
|
5
|
+
|
|
6
|
+
#include <cstdint>
|
|
7
|
+
#include <limits>
|
|
8
|
+
|
|
9
|
+
namespace FSMeta {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks if multiplying two uint64_t values would overflow.
|
|
13
|
+
*
|
|
14
|
+
* Used to safely calculate volume sizes: size = blockSize * blockCount
|
|
15
|
+
* Must be called BEFORE performing the multiplication.
|
|
16
|
+
*
|
|
17
|
+
* @param a First operand (e.g., block size)
|
|
18
|
+
* @param b Second operand (e.g., block count)
|
|
19
|
+
* @return true if multiplication would overflow, false if safe
|
|
20
|
+
*
|
|
21
|
+
* Example usage:
|
|
22
|
+
* if (WouldOverflow(blockSize, totalBlocks)) {
|
|
23
|
+
* SetError("Total volume size calculation would overflow");
|
|
24
|
+
* return false;
|
|
25
|
+
* }
|
|
26
|
+
* uint64_t totalSize = blockSize * totalBlocks; // Safe
|
|
27
|
+
*/
|
|
28
|
+
inline bool WouldOverflow(uint64_t a, uint64_t b) noexcept {
|
|
29
|
+
// Overflow occurs if a * b > MAX, which is equivalent to a > MAX / b
|
|
30
|
+
// We need b > 0 check to avoid division by zero
|
|
31
|
+
return b > 0 && a > std::numeric_limits<uint64_t>::max() / b;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Safely multiplies two uint64_t values, returning 0 on overflow.
|
|
36
|
+
*
|
|
37
|
+
* @param a First operand
|
|
38
|
+
* @param b Second operand
|
|
39
|
+
* @param overflow_out Optional pointer to receive overflow status
|
|
40
|
+
* @return Product of a * b, or 0 if overflow would occur
|
|
41
|
+
*/
|
|
42
|
+
inline uint64_t SafeMultiply(uint64_t a, uint64_t b,
|
|
43
|
+
bool *overflow_out = nullptr) noexcept {
|
|
44
|
+
bool overflow = WouldOverflow(a, b);
|
|
45
|
+
if (overflow_out) {
|
|
46
|
+
*overflow_out = overflow;
|
|
47
|
+
}
|
|
48
|
+
return overflow ? 0 : a * b;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
} // namespace FSMeta
|
package/src/darwin/hidden.cpp
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
#include "hidden.h"
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
4
|
#include "../common/error_utils.h"
|
|
5
|
-
#include "
|
|
5
|
+
#include "../common/fd_guard.h"
|
|
6
|
+
#include "../common/path_security.h"
|
|
7
|
+
#include <fcntl.h> // for open(), O_RDONLY, O_CLOEXEC
|
|
6
8
|
#include <string.h> // for strcmp
|
|
7
9
|
#include <sys/mount.h>
|
|
8
10
|
#include <sys/stat.h>
|
|
@@ -42,19 +44,35 @@ void GetHiddenWorker::Execute() {
|
|
|
42
44
|
DEBUG_LOG("[GetHiddenWorker] Using validated path: %s",
|
|
43
45
|
validated_path.c_str());
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
// SECURITY: Use file descriptor-based approach to prevent TOCTOU race
|
|
48
|
+
// condition. Opening the file and using fstat() on the fd ensures we're
|
|
49
|
+
// checking the same file that realpath() validated.
|
|
50
|
+
// O_CLOEXEC: Prevent fd leak to child processes on fork/exec
|
|
51
|
+
// O_RDONLY: We only need to read the flags
|
|
52
|
+
int fd = open(validated_path.c_str(), O_RDONLY | O_CLOEXEC);
|
|
53
|
+
if (fd < 0) {
|
|
47
54
|
int error = errno;
|
|
48
55
|
if (error == ENOENT) {
|
|
49
56
|
DEBUG_LOG("[GetHiddenWorker] path not found: %s", validated_path.c_str());
|
|
50
57
|
SetError("Path not found: '" + validated_path + "'");
|
|
51
58
|
} else {
|
|
52
|
-
DEBUG_LOG("[GetHiddenWorker] failed to
|
|
59
|
+
DEBUG_LOG("[GetHiddenWorker] failed to open path %s: %s (%d)",
|
|
53
60
|
validated_path.c_str(), strerror(error), error);
|
|
54
|
-
SetError(CreatePathErrorMessage("
|
|
61
|
+
SetError(CreatePathErrorMessage("open", validated_path, error));
|
|
55
62
|
}
|
|
56
63
|
return;
|
|
57
64
|
}
|
|
65
|
+
FdGuard fd_guard(fd);
|
|
66
|
+
|
|
67
|
+
struct stat statbuf;
|
|
68
|
+
if (fstat(fd, &statbuf) != 0) {
|
|
69
|
+
int error = errno;
|
|
70
|
+
DEBUG_LOG("[GetHiddenWorker] failed to fstat path %s: %s (%d)",
|
|
71
|
+
validated_path.c_str(), strerror(error), error);
|
|
72
|
+
SetError(CreatePathErrorMessage("fstat", validated_path, error));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
is_hidden_ = (statbuf.st_flags & UF_HIDDEN) != 0;
|
|
59
77
|
DEBUG_LOG("[GetHiddenWorker] path %s is %s", validated_path.c_str(),
|
|
60
78
|
is_hidden_ ? "hidden" : "not hidden");
|
|
@@ -121,19 +139,34 @@ void SetHiddenWorker::Execute() {
|
|
|
121
139
|
DEBUG_LOG("[SetHiddenWorker] Using validated path: %s",
|
|
122
140
|
validated_path.c_str());
|
|
123
141
|
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
// SECURITY: Use file descriptor-based approach to prevent TOCTOU race
|
|
143
|
+
// condition. Opening the file, reading flags with fstat(), and setting
|
|
144
|
+
// flags with fchflags() all operate on the same inode via the fd.
|
|
145
|
+
// O_CLOEXEC: Prevent fd leak to child processes on fork/exec
|
|
146
|
+
// O_RDONLY: fchflags() doesn't require write access to the file contents
|
|
147
|
+
int fd = open(validated_path.c_str(), O_RDONLY | O_CLOEXEC);
|
|
148
|
+
if (fd < 0) {
|
|
126
149
|
int error = errno;
|
|
127
150
|
if (error == ENOENT) {
|
|
128
151
|
DEBUG_LOG("[SetHiddenWorker] path not found: %s", validated_path.c_str());
|
|
129
152
|
SetError("Path not found: '" + validated_path + "'");
|
|
130
153
|
} else {
|
|
131
|
-
DEBUG_LOG("[SetHiddenWorker] failed to
|
|
154
|
+
DEBUG_LOG("[SetHiddenWorker] failed to open path %s: %s (%d)",
|
|
132
155
|
validated_path.c_str(), strerror(error), error);
|
|
133
|
-
SetError(CreatePathErrorMessage("
|
|
156
|
+
SetError(CreatePathErrorMessage("open", validated_path, error));
|
|
134
157
|
}
|
|
135
158
|
return;
|
|
136
159
|
}
|
|
160
|
+
FdGuard fd_guard(fd);
|
|
161
|
+
|
|
162
|
+
struct stat statbuf;
|
|
163
|
+
if (fstat(fd, &statbuf) != 0) {
|
|
164
|
+
int error = errno;
|
|
165
|
+
DEBUG_LOG("[SetHiddenWorker] failed to fstat path %s: %s (%d)",
|
|
166
|
+
validated_path.c_str(), strerror(error), error);
|
|
167
|
+
SetError(CreatePathErrorMessage("fstat", validated_path, error));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
137
170
|
|
|
138
171
|
u_int32_t new_flags;
|
|
139
172
|
if (hidden_) {
|
|
@@ -142,15 +175,15 @@ void SetHiddenWorker::Execute() {
|
|
|
142
175
|
new_flags = statbuf.st_flags & ~UF_HIDDEN;
|
|
143
176
|
}
|
|
144
177
|
|
|
145
|
-
if (
|
|
178
|
+
if (fchflags(fd, new_flags) != 0) {
|
|
146
179
|
int error = errno;
|
|
147
180
|
DEBUG_LOG("[SetHiddenWorker] failed to set flags for %s: %s (%d)",
|
|
148
181
|
validated_path.c_str(), strerror(error), error);
|
|
149
182
|
|
|
150
|
-
// Check if this is an APFS filesystem issue
|
|
183
|
+
// Check if this is an APFS filesystem issue using fstatfs on the fd
|
|
151
184
|
struct statfs fs;
|
|
152
185
|
bool is_apfs = false;
|
|
153
|
-
if (
|
|
186
|
+
if (fstatfs(fd, &fs) == 0) {
|
|
154
187
|
is_apfs = (strcmp(fs.f_fstypename, "apfs") == 0);
|
|
155
188
|
DEBUG_LOG("[SetHiddenWorker] filesystem type: %s", fs.f_fstypename);
|
|
156
189
|
}
|
|
@@ -160,9 +193,9 @@ void SetHiddenWorker::Execute() {
|
|
|
160
193
|
SetError("Setting hidden attribute failed on APFS filesystem. "
|
|
161
194
|
"This is a known issue with some APFS volumes. "
|
|
162
195
|
"Error: " +
|
|
163
|
-
CreatePathErrorMessage("
|
|
196
|
+
CreatePathErrorMessage("fchflags", validated_path, error));
|
|
164
197
|
} else {
|
|
165
|
-
SetError(CreatePathErrorMessage("
|
|
198
|
+
SetError(CreatePathErrorMessage("fchflags", validated_path, error));
|
|
166
199
|
}
|
|
167
200
|
return;
|
|
168
201
|
}
|
package/src/darwin/raii_utils.h
CHANGED
|
@@ -18,8 +18,8 @@ private:
|
|
|
18
18
|
T *resource_;
|
|
19
19
|
|
|
20
20
|
public:
|
|
21
|
-
ResourceRAII() : resource_(nullptr) {}
|
|
22
|
-
~ResourceRAII() {
|
|
21
|
+
ResourceRAII() noexcept : resource_(nullptr) {}
|
|
22
|
+
~ResourceRAII() noexcept {
|
|
23
23
|
if (resource_) {
|
|
24
24
|
free(resource_);
|
|
25
25
|
}
|
|
@@ -56,8 +56,8 @@ private:
|
|
|
56
56
|
struct statfs *buffer_;
|
|
57
57
|
|
|
58
58
|
public:
|
|
59
|
-
MountBufferRAII() : buffer_(nullptr) {}
|
|
60
|
-
~MountBufferRAII() {
|
|
59
|
+
MountBufferRAII() noexcept : buffer_(nullptr) {}
|
|
60
|
+
~MountBufferRAII() noexcept {
|
|
61
61
|
if (buffer_) {
|
|
62
62
|
free(buffer_);
|
|
63
63
|
}
|
|
@@ -96,9 +96,9 @@ private:
|
|
|
96
96
|
|
|
97
97
|
public:
|
|
98
98
|
explicit CFReleaser(T ref = nullptr) noexcept : ref_(ref) {}
|
|
99
|
-
~CFReleaser() { reset(); }
|
|
99
|
+
~CFReleaser() noexcept { reset(); }
|
|
100
100
|
|
|
101
|
-
void reset(T ref = nullptr) {
|
|
101
|
+
void reset(T ref = nullptr) noexcept {
|
|
102
102
|
if (ref_) {
|
|
103
103
|
CFRelease(ref_);
|
|
104
104
|
}
|
|
@@ -139,7 +139,7 @@ public:
|
|
|
139
139
|
explicit DASessionRAII(DASessionRef session = nullptr) noexcept
|
|
140
140
|
: session_(session), is_scheduled_(false) {}
|
|
141
141
|
|
|
142
|
-
~DASessionRAII() { unschedule(); }
|
|
142
|
+
~DASessionRAII() noexcept { unschedule(); }
|
|
143
143
|
|
|
144
144
|
// Schedule the session on a dispatch queue
|
|
145
145
|
void scheduleOnQueue(dispatch_queue_t queue) {
|
|
@@ -150,7 +150,7 @@ public:
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
// Unschedule the session (must be called before session is released)
|
|
153
|
-
void unschedule() {
|
|
153
|
+
void unschedule() noexcept {
|
|
154
154
|
if (is_scheduled_ && session_.isValid()) {
|
|
155
155
|
DASessionSetDispatchQueue(session_.get(), nullptr);
|
|
156
156
|
is_scheduled_ = false;
|
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
// Thread-safe implementation with DiskArbitration mutex synchronization
|
|
3
3
|
|
|
4
4
|
#include "../common/debug_log.h"
|
|
5
|
+
#include "../common/fd_guard.h"
|
|
6
|
+
#include "../common/path_security.h"
|
|
7
|
+
#include "../common/volume_utils.h"
|
|
5
8
|
#include "./fs_meta.h"
|
|
6
|
-
#include "./path_security.h"
|
|
7
9
|
#include "./raii_utils.h"
|
|
8
10
|
|
|
9
11
|
#include <CoreFoundation/CoreFoundation.h>
|
|
10
12
|
#include <DiskArbitration/DiskArbitration.h>
|
|
11
|
-
#include <
|
|
13
|
+
#include <cstring> // For strlen()
|
|
14
|
+
#include <fcntl.h> // For open(), O_RDONLY, O_DIRECTORY, O_CLOEXEC
|
|
12
15
|
#include <memory>
|
|
13
16
|
#include <mutex>
|
|
14
17
|
#include <string>
|
|
15
18
|
#include <sys/mount.h>
|
|
16
19
|
#include <sys/param.h>
|
|
17
20
|
#include <sys/statvfs.h>
|
|
18
|
-
#include <unistd.h>
|
|
21
|
+
#include <unistd.h>
|
|
19
22
|
|
|
20
23
|
namespace FSMeta {
|
|
21
24
|
|
|
@@ -55,17 +58,9 @@ static std::string CFStringToString(CFStringRef cfString) {
|
|
|
55
58
|
return "";
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
for (size_t i = 0; i < static_cast<size_t>(maxSize); ++i) {
|
|
62
|
-
if (result[i] == '\0') {
|
|
63
|
-
actualLength = i;
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
result.resize(actualLength);
|
|
61
|
+
// CFStringGetCString guarantees null termination on success, so strlen is
|
|
62
|
+
// safe here. The buffer was sized with GetMaximumSizeForEncoding + 1.
|
|
63
|
+
result.resize(strlen(result.c_str()));
|
|
69
64
|
return result;
|
|
70
65
|
}
|
|
71
66
|
|
|
@@ -122,7 +117,8 @@ private:
|
|
|
122
117
|
// Use file descriptors to prevent TOCTOU race conditions
|
|
123
118
|
// Open the mount point directory with O_DIRECTORY to ensure it's a
|
|
124
119
|
// directory
|
|
125
|
-
|
|
120
|
+
// O_CLOEXEC: Prevent fd leak to child processes on fork/exec
|
|
121
|
+
int fd = open(mountPoint.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
126
122
|
if (fd < 0) {
|
|
127
123
|
int error = errno;
|
|
128
124
|
DEBUG_LOG("[GetVolumeMetadataWorker] open failed: %s (%d)",
|
|
@@ -132,14 +128,7 @@ private:
|
|
|
132
128
|
}
|
|
133
129
|
|
|
134
130
|
// RAII wrapper for file descriptor to ensure it's always closed
|
|
135
|
-
|
|
136
|
-
int fd;
|
|
137
|
-
~FdGuard() {
|
|
138
|
-
if (fd >= 0) {
|
|
139
|
-
close(fd);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} fd_guard{fd};
|
|
131
|
+
FdGuard fd_guard(fd);
|
|
143
132
|
|
|
144
133
|
struct statvfs vfs;
|
|
145
134
|
struct statfs fs;
|
|
@@ -172,19 +161,17 @@ private:
|
|
|
172
161
|
const uint64_t freeBlocks = static_cast<uint64_t>(vfs.f_bfree);
|
|
173
162
|
|
|
174
163
|
// Check for overflow before multiplication
|
|
175
|
-
if (blockSize
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
164
|
+
if (WouldOverflow(blockSize, totalBlocks)) {
|
|
165
|
+
SetError("Total volume size calculation would overflow");
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (WouldOverflow(blockSize, availBlocks)) {
|
|
169
|
+
SetError("Available space calculation would overflow");
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (WouldOverflow(blockSize, freeBlocks)) {
|
|
173
|
+
SetError("Free space calculation would overflow");
|
|
174
|
+
return false;
|
|
188
175
|
}
|
|
189
176
|
|
|
190
177
|
const uint64_t totalSize = blockSize * totalBlocks;
|
|
@@ -243,6 +230,12 @@ private:
|
|
|
243
230
|
// it. We use a background queue (not main queue) to avoid deadlock in
|
|
244
231
|
// Node.js. The RAII wrapper will automatically unschedule in its
|
|
245
232
|
// destructor.
|
|
233
|
+
//
|
|
234
|
+
// NOTE: This static dispatch queue is intentionally never released.
|
|
235
|
+
// It's a singleton that lives for the process lifetime. The queue is
|
|
236
|
+
// lightweight (just a reference to GCD's internal structures), and
|
|
237
|
+
// releasing it on module unload could race with in-flight operations.
|
|
238
|
+
// This pattern is standard for long-lived queues in macOS applications.
|
|
246
239
|
static dispatch_queue_t da_queue =
|
|
247
240
|
dispatch_queue_create("com.photostructure.fs-metadata.diskarbitration",
|
|
248
241
|
DISPATCH_QUEUE_SERIAL);
|
|
@@ -327,14 +320,15 @@ private:
|
|
|
327
320
|
CFURLRef url = (CFURLRef)CFDictionaryGetValue(
|
|
328
321
|
description, kDADiskDescriptionVolumePathKey);
|
|
329
322
|
if (!url) {
|
|
330
|
-
metadata.
|
|
323
|
+
metadata.status = "partial";
|
|
324
|
+
metadata.error = "Volume path not available in disk description";
|
|
331
325
|
return;
|
|
332
326
|
}
|
|
333
327
|
CFReleaser<CFStringRef> urlString(
|
|
334
328
|
CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle));
|
|
335
329
|
if (!urlString.isValid()) {
|
|
336
|
-
metadata.
|
|
337
|
-
|
|
330
|
+
metadata.status = "partial";
|
|
331
|
+
metadata.error = "Failed to get filesystem path from volume URL";
|
|
338
332
|
return;
|
|
339
333
|
}
|
|
340
334
|
|
package/src/index.ts
CHANGED
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
setHiddenImpl,
|
|
14
14
|
} from "./hidden";
|
|
15
15
|
import {
|
|
16
|
+
getTimeoutMsDefault,
|
|
16
17
|
IncludeSystemVolumesDefault,
|
|
17
18
|
LinuxMountTablePathsDefault,
|
|
18
19
|
OptionsDefault,
|
|
19
20
|
optionsWithDefaults,
|
|
20
21
|
SystemFsTypesDefault,
|
|
21
22
|
SystemPathPatternsDefault,
|
|
22
|
-
TimeoutMsDefault,
|
|
23
23
|
} from "./options";
|
|
24
24
|
import type { StringEnum, StringEnumKeys, StringEnumType } from "./string_enum";
|
|
25
25
|
import type { SystemVolumeConfig } from "./system_volume";
|
|
@@ -118,7 +118,7 @@ export function getVolumeMetadata(
|
|
|
118
118
|
* {@link https://nodejs.org/api/os.html#osavailableparallelism | os.availableParallelism()}
|
|
119
119
|
* @param opts.timeoutMs - Maximum time to wait for
|
|
120
120
|
* {@link getVolumeMountPointsImpl}, as well as **each** {@link getVolumeMetadataImpl}
|
|
121
|
-
* to complete. Defaults to {@link
|
|
121
|
+
* to complete. Defaults to {@link getTimeoutMsDefault}
|
|
122
122
|
* @returns Promise that resolves to an array of either VolumeMetadata objects
|
|
123
123
|
* or error objects containing the mount point and error
|
|
124
124
|
* @throws Never - errors are caught and returned as part of the result array
|
|
@@ -186,12 +186,12 @@ export function setHidden(
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
export {
|
|
189
|
+
getTimeoutMsDefault,
|
|
189
190
|
IncludeSystemVolumesDefault,
|
|
190
191
|
LinuxMountTablePathsDefault,
|
|
191
192
|
OptionsDefault,
|
|
192
193
|
optionsWithDefaults,
|
|
193
194
|
SystemFsTypesDefault,
|
|
194
195
|
SystemPathPatternsDefault,
|
|
195
|
-
TimeoutMsDefault,
|
|
196
196
|
VolumeHealthStatuses,
|
|
197
197
|
};
|
|
@@ -32,17 +32,11 @@ BlkidCache::~BlkidCache() {
|
|
|
32
32
|
const std::lock_guard<std::mutex> lock(mutex_);
|
|
33
33
|
if (cache_) { // Double-check after acquiring lock
|
|
34
34
|
DEBUG_LOG("[BlkidCache] releasing cache");
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
DEBUG_LOG("[BlkidCache] error releasing cache: %s", e.what());
|
|
41
|
-
cache_ = nullptr; // Ensure it's nulled even on error
|
|
42
|
-
// Optional: Log error during cache cleanup
|
|
43
|
-
// std::cerr << "Error while releasing blkid cache: " << e.what()
|
|
44
|
-
// << std::endl;
|
|
45
|
-
}
|
|
35
|
+
// Note: blkid_put_cache() is a C function that cannot throw C++
|
|
36
|
+
// exceptions, so no try-catch is needed here.
|
|
37
|
+
blkid_put_cache(cache_);
|
|
38
|
+
cache_ = nullptr;
|
|
39
|
+
DEBUG_LOG("[BlkidCache] cache released successfully");
|
|
46
40
|
}
|
|
47
41
|
}
|
|
48
42
|
}
|
package/src/linux/blkid_cache.h
CHANGED
|
@@ -21,8 +21,29 @@ public:
|
|
|
21
21
|
|
|
22
22
|
operator bool() const { return cache_ != nullptr; }
|
|
23
23
|
|
|
24
|
+
// Prevent copying - each instance owns its cache
|
|
24
25
|
BlkidCache(const BlkidCache &) = delete;
|
|
25
26
|
BlkidCache &operator=(const BlkidCache &) = delete;
|
|
27
|
+
|
|
28
|
+
// Allow moving - transfers ownership of the cache
|
|
29
|
+
BlkidCache(BlkidCache &&other) noexcept : cache_(other.cache_) {
|
|
30
|
+
other.cache_ = nullptr;
|
|
31
|
+
}
|
|
32
|
+
BlkidCache &operator=(BlkidCache &&other) noexcept {
|
|
33
|
+
if (this != &other) {
|
|
34
|
+
// Release current cache if any (under lock)
|
|
35
|
+
if (cache_) {
|
|
36
|
+
const std::lock_guard<std::mutex> lock(mutex_);
|
|
37
|
+
if (cache_) {
|
|
38
|
+
blkid_put_cache(cache_);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Take ownership from other
|
|
42
|
+
cache_ = other.cache_;
|
|
43
|
+
other.cache_ = nullptr;
|
|
44
|
+
}
|
|
45
|
+
return *this;
|
|
46
|
+
}
|
|
26
47
|
};
|
|
27
48
|
|
|
28
49
|
} // namespace FSMeta
|
package/src/linux/gio_utils.cpp
CHANGED
|
@@ -101,29 +101,13 @@ void MountIterator::forEachMount(const MountCallback &callback) {
|
|
|
101
101
|
DEBUG_LOG("[gio::MountIterator::forEachMount] completed");
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
GVolumeMonitor
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
// Future work: Consider removing this entirely or moving enrichment
|
|
113
|
-
// to main thread if needed.
|
|
114
|
-
GVolumeMonitor *monitor = g_volume_monitor_get();
|
|
115
|
-
if (!monitor) {
|
|
116
|
-
DEBUG_LOG("[gio::tryGetMonitor] g_volume_monitor_get() returned null");
|
|
117
|
-
}
|
|
118
|
-
return monitor; // May be null, caller must check
|
|
119
|
-
} catch (const std::exception &e) {
|
|
120
|
-
DEBUG_LOG("[gio::tryGetMonitor] Exception: %s", e.what());
|
|
121
|
-
return nullptr;
|
|
122
|
-
} catch (...) {
|
|
123
|
-
DEBUG_LOG("[gio::tryGetMonitor] Unknown exception");
|
|
124
|
-
return nullptr;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
104
|
+
// NOTE: tryGetMonitor() has been removed.
|
|
105
|
+
//
|
|
106
|
+
// GVolumeMonitor is NOT thread-safe and must only be used from the main thread.
|
|
107
|
+
// See: https://docs.gtk.org/gio/class.VolumeMonitor.html
|
|
108
|
+
//
|
|
109
|
+
// Since all our code runs on Napi::AsyncWorker threads, using GVolumeMonitor
|
|
110
|
+
// causes race conditions leading to GLib-GObject-CRITICAL errors.
|
|
127
111
|
|
|
128
112
|
} // namespace gio
|
|
129
113
|
} // namespace FSMeta
|
package/src/linux/gio_utils.h
CHANGED
|
@@ -18,7 +18,8 @@ template <typename T> struct GObjectDeleter {
|
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
// Custom deleter for g_free
|
|
21
|
+
// Custom deleter for g_free (used for strings from GIO APIs like
|
|
22
|
+
// g_file_get_path)
|
|
22
23
|
struct GFreeDeleter {
|
|
23
24
|
void operator()(void *ptr) const {
|
|
24
25
|
if (ptr) {
|
|
@@ -27,18 +28,18 @@ struct GFreeDeleter {
|
|
|
27
28
|
}
|
|
28
29
|
};
|
|
29
30
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
void operator()(GFileInfo *ptr) {
|
|
33
|
-
if (ptr)
|
|
34
|
-
g_object_unref(ptr);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
using GFileInfoPtr = std::unique_ptr<GFileInfo, GFileInfoDeleter>;
|
|
38
|
-
|
|
39
|
-
// Smart pointer aliases
|
|
31
|
+
// Smart pointer aliases for RAII management of GIO resources
|
|
32
|
+
// These ensure proper cleanup even when exceptions occur
|
|
40
33
|
template <typename T> using GObjectPtr = std::unique_ptr<T, GObjectDeleter<T>>;
|
|
41
34
|
|
|
35
|
+
// Common GIO object types
|
|
36
|
+
using GFilePtr = GObjectPtr<GFile>;
|
|
37
|
+
using GMountPtr = GObjectPtr<GMount>;
|
|
38
|
+
using GVolumePtr = GObjectPtr<GVolume>;
|
|
39
|
+
using GVolumeMonitorPtr = GObjectPtr<GVolumeMonitor>;
|
|
40
|
+
using GFileInfoPtr = GObjectPtr<GFileInfo>;
|
|
41
|
+
|
|
42
|
+
// For strings allocated by GIO (g_file_get_path, g_file_get_uri, etc.)
|
|
42
43
|
using GCharPtr = std::unique_ptr<char, GFreeDeleter>;
|
|
43
44
|
|
|
44
45
|
namespace FSMeta {
|
|
@@ -55,37 +56,12 @@ public:
|
|
|
55
56
|
// This is safe to call from worker threads
|
|
56
57
|
static void forEachMount(const MountCallback &callback);
|
|
57
58
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// WARNING: Violates thread-safety when called from worker threads
|
|
61
|
-
// Only use for best-effort enrichment
|
|
62
|
-
static GVolumeMonitor *tryGetMonitor() noexcept;
|
|
59
|
+
// NOTE: tryGetMonitor() has been removed because GVolumeMonitor is NOT
|
|
60
|
+
// thread-safe. See: https://docs.gtk.org/gio/class.VolumeMonitor.html
|
|
63
61
|
};
|
|
64
62
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
public:
|
|
68
|
-
explicit GioResource(T *resource) : resource_(resource) {}
|
|
69
|
-
~GioResource() {
|
|
70
|
-
if (resource_) {
|
|
71
|
-
g_object_unref(resource_);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
T *get() const { return resource_; }
|
|
76
|
-
T *release() {
|
|
77
|
-
T *temp = resource_;
|
|
78
|
-
resource_ = nullptr;
|
|
79
|
-
return temp;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Prevent copying
|
|
83
|
-
GioResource(const GioResource &) = delete;
|
|
84
|
-
GioResource &operator=(const GioResource &) = delete;
|
|
85
|
-
|
|
86
|
-
private:
|
|
87
|
-
T *resource_;
|
|
88
|
-
};
|
|
63
|
+
// Note: GioResource<T> has been removed in favor of GObjectPtr<T> above,
|
|
64
|
+
// which provides equivalent RAII semantics with std::unique_ptr.
|
|
89
65
|
|
|
90
66
|
} // namespace gio
|
|
91
67
|
} // namespace FSMeta
|