@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +34 -1
  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 +17 -20
  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
@@ -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/darwin/path_security.h
2
- // Secure path validation for macOS using realpath()
3
- // Implements recommendations from Apple's Secure Coding Guide
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 "../common/debug_log.h"
9
- #include "../common/error_utils.h"
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
@@ -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 "path_security.h"
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
- struct stat statbuf;
46
- if (stat(validated_path.c_str(), &statbuf) != 0) {
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 stat path %s: %s (%d)",
59
+ DEBUG_LOG("[GetHiddenWorker] failed to open path %s: %s (%d)",
53
60
  validated_path.c_str(), strerror(error), error);
54
- SetError(CreatePathErrorMessage("stat", validated_path, error));
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
- struct stat statbuf;
125
- if (stat(validated_path.c_str(), &statbuf) != 0) {
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 stat path %s: %s (%d)",
154
+ DEBUG_LOG("[SetHiddenWorker] failed to open path %s: %s (%d)",
132
155
  validated_path.c_str(), strerror(error), error);
133
- SetError(CreatePathErrorMessage("stat", validated_path, error));
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 (chflags(validated_path.c_str(), new_flags) != 0) {
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 (statfs(validated_path.c_str(), &fs) == 0) {
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("chflags", validated_path, error));
196
+ CreatePathErrorMessage("fchflags", validated_path, error));
164
197
  } else {
165
- SetError(CreatePathErrorMessage("chflags", validated_path, error));
198
+ SetError(CreatePathErrorMessage("fchflags", validated_path, error));
166
199
  }
167
200
  return;
168
201
  }
@@ -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 <fcntl.h> // For open(), O_RDONLY, O_DIRECTORY
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> // For close()
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
- // Find actual string length by looking for null terminator
59
- // This is safer than using strlen which could read past buffer
60
- size_t actualLength = 0;
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
- int fd = open(mountPoint.c_str(), O_RDONLY | O_DIRECTORY);
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
- struct FdGuard {
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 > 0) {
176
- if (totalBlocks > std::numeric_limits<uint64_t>::max() / blockSize) {
177
- SetError("Total volume size calculation would overflow");
178
- return false;
179
- }
180
- if (availBlocks > std::numeric_limits<uint64_t>::max() / blockSize) {
181
- SetError("Available space calculation would overflow");
182
- return false;
183
- }
184
- if (freeBlocks > std::numeric_limits<uint64_t>::max() / blockSize) {
185
- SetError("Free space calculation would overflow");
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.error = "Invalid URL";
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.error = std::string("Invalid URL string: ") +
337
- CFStringToString(urlString.get());
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 TimeoutMsDefault}
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
- try {
36
- blkid_put_cache(cache_);
37
- cache_ = nullptr; // Avoid double-release
38
- DEBUG_LOG("[BlkidCache] cache released successfully");
39
- } catch (const std::exception &e) {
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
  }
@@ -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
@@ -101,29 +101,13 @@ void MountIterator::forEachMount(const MountCallback &callback) {
101
101
  DEBUG_LOG("[gio::MountIterator::forEachMount] completed");
102
102
  }
103
103
 
104
- // OPTIONAL: Try to get GVolumeMonitor (may fail, that's OK)
105
- // This is best-effort enrichment and should NOT be required for basic operation
106
- GVolumeMonitor *MountIterator::tryGetMonitor() noexcept {
107
- try {
108
- // NOTE: This violates GVolumeMonitor thread-safety requirements when
109
- // called from worker threads. We use it only for optional metadata
110
- // enrichment. The primary path uses thread-safe g_unix_mounts_get().
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
@@ -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
- // Add this before any existing smart pointer definitions
31
- struct GFileInfoDeleter {
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
- // OPTIONAL: Try to get GVolumeMonitor for metadata enrichment
59
- // Returns nullptr if unavailable (that's OK, not required)
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
- // Helper class for scoped GIO resource management
66
- template <typename T> class GioResource {
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