@photostructure/fs-metadata 0.6.0 → 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 (98) hide show
  1. package/CHANGELOG.md +11 -6
  2. package/CLAUDE.md +160 -136
  3. package/CODE_OF_CONDUCT.md +11 -11
  4. package/CONTRIBUTING.md +2 -2
  5. package/README.md +34 -84
  6. package/binding.gyp +98 -23
  7. package/claude.sh +23 -0
  8. package/dist/index.cjs +53 -22
  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 +52 -21
  14. package/dist/index.mjs.map +1 -1
  15. package/{C++_REVIEW_TODO.md → doc/C++_REVIEW_TODO.md} +97 -25
  16. package/doc/GPG_RELEASE_HOWTO.md +505 -0
  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 +207 -0
  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 +96 -0
  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 +24 -0
  35. package/package.json +68 -44
  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 +832 -0
  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 +119 -0
  51. package/scripts/prebuildify-wrapper.ts +77 -0
  52. package/scripts/precommit.ts +70 -0
  53. package/scripts/sanitizers-test.sh +7 -1
  54. package/scripts/{configure.mjs → setup-native.mjs} +4 -1
  55. package/src/binding.cpp +1 -1
  56. package/src/common/error_utils.h +0 -6
  57. package/src/common/volume_metadata.h +6 -0
  58. package/src/darwin/hidden.cpp +73 -25
  59. package/src/darwin/path_security.h +149 -0
  60. package/src/darwin/raii_utils.h +104 -4
  61. package/src/darwin/volume_metadata.cpp +132 -58
  62. package/src/darwin/volume_mount_points.cpp +80 -47
  63. package/src/hidden.ts +36 -13
  64. package/src/linux/gio_mount_points.cpp +17 -18
  65. package/src/linux/gio_utils.cpp +92 -37
  66. package/src/linux/gio_utils.h +11 -5
  67. package/src/linux/gio_volume_metadata.cpp +111 -48
  68. package/src/linux/volume_metadata.cpp +67 -4
  69. package/src/object.ts +1 -0
  70. package/src/options.ts +6 -0
  71. package/src/path.ts +11 -0
  72. package/src/platform.ts +25 -0
  73. package/src/remote_info.ts +5 -3
  74. package/src/stack_path.ts +8 -6
  75. package/src/string_enum.ts +1 -0
  76. package/src/test-utils/benchmark-harness.ts +192 -0
  77. package/src/test-utils/debuglog-child.ts +30 -2
  78. package/src/test-utils/debuglog-enabled-child.ts +38 -8
  79. package/src/test-utils/jest-setup.ts +14 -0
  80. package/src/test-utils/memory-test-core.ts +336 -0
  81. package/src/test-utils/memory-test-runner.ts +108 -0
  82. package/src/test-utils/platform.ts +46 -1
  83. package/src/test-utils/worker-thread-helper.cjs +157 -26
  84. package/src/types/native_bindings.ts +1 -1
  85. package/src/types/options.ts +6 -0
  86. package/src/windows/drive_status.h +133 -163
  87. package/src/windows/error_utils.h +54 -3
  88. package/src/windows/fs_meta.h +1 -1
  89. package/src/windows/hidden.cpp +60 -43
  90. package/src/windows/security_utils.h +250 -0
  91. package/src/windows/string.h +68 -11
  92. package/src/windows/system_volume.h +1 -1
  93. package/src/windows/thread_pool.h +206 -0
  94. package/src/windows/volume_metadata.cpp +11 -6
  95. package/src/windows/volume_mount_points.cpp +8 -7
  96. package/src/windows/windows_arch.h +39 -0
  97. package/scripts/check-memory.mjs +0 -123
  98. package/scripts/clang-tidy.mjs +0 -73
@@ -2,6 +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"
6
+ #include <string.h> // for strcmp
7
+ #include <sys/mount.h>
5
8
  #include <sys/stat.h>
6
9
  #include <unistd.h>
7
10
 
@@ -9,35 +12,51 @@ namespace FSMeta {
9
12
 
10
13
  GetHiddenWorker::GetHiddenWorker(std::string path,
11
14
  Napi::Promise::Deferred deferred)
12
- : Napi::AsyncWorker(deferred.Env()), path_(path), deferred_(deferred),
13
- is_hidden_(false) {
15
+ : Napi::AsyncWorker(deferred.Env()), path_(std::move(path)),
16
+ deferred_(deferred), is_hidden_(false) {
14
17
  DEBUG_LOG("[GetHiddenWorker] created for path: %s", path_.c_str());
15
18
  }
16
19
 
17
20
  void GetHiddenWorker::Execute() {
18
21
  DEBUG_LOG("[GetHiddenWorker] checking hidden status for: %s", path_.c_str());
19
22
 
20
- // Add path validation to prevent directory traversal
21
- if (path_.find("..") != std::string::npos) {
22
- SetError("Invalid path containing '..'");
23
+ // Validate and canonicalize path using realpath() to prevent directory
24
+ // traversal This follows Apple's Secure Coding Guide recommendations For
25
+ // isHidden(), we allow non-existent paths (they will fail stat() below)
26
+ std::string error;
27
+ std::string validated_path = ValidateAndCanonicalizePath(path_, error, true);
28
+ if (validated_path.empty()) {
29
+ // If validation failed, check if it's because the path doesn't exist
30
+ // In that case, return the expected "Path not found" error for TypeScript
31
+ // layer
32
+ if (error.find("realpath") != std::string::npos &&
33
+ error.find("No such file or directory") != std::string::npos) {
34
+ SetError("Path not found: '" + path_ + "'");
35
+ } else {
36
+ SetError(error);
37
+ }
23
38
  return;
24
39
  }
25
40
 
41
+ // Use the validated path for all subsequent operations
42
+ DEBUG_LOG("[GetHiddenWorker] Using validated path: %s",
43
+ validated_path.c_str());
44
+
26
45
  struct stat statbuf;
27
- if (stat(path_.c_str(), &statbuf) != 0) {
46
+ if (stat(validated_path.c_str(), &statbuf) != 0) {
28
47
  int error = errno;
29
48
  if (error == ENOENT) {
30
- DEBUG_LOG("[GetHiddenWorker] path not found: %s", path_.c_str());
31
- SetError("Path not found: '" + path_ + "'");
49
+ DEBUG_LOG("[GetHiddenWorker] path not found: %s", validated_path.c_str());
50
+ SetError("Path not found: '" + validated_path + "'");
32
51
  } else {
33
52
  DEBUG_LOG("[GetHiddenWorker] failed to stat path %s: %s (%d)",
34
- path_.c_str(), strerror(error), error);
35
- SetError(CreatePathErrorMessage("stat", path_, error));
53
+ validated_path.c_str(), strerror(error), error);
54
+ SetError(CreatePathErrorMessage("stat", validated_path, error));
36
55
  }
37
56
  return;
38
57
  }
39
58
  is_hidden_ = (statbuf.st_flags & UF_HIDDEN) != 0;
40
- DEBUG_LOG("[GetHiddenWorker] path %s is %s", path_.c_str(),
59
+ DEBUG_LOG("[GetHiddenWorker] path %s is %s", validated_path.c_str(),
41
60
  is_hidden_ ? "hidden" : "not hidden");
42
61
  }
43
62
 
@@ -74,8 +93,8 @@ Napi::Promise GetHiddenAttribute(const Napi::CallbackInfo &info) {
74
93
 
75
94
  SetHiddenWorker::SetHiddenWorker(std::string path, bool hidden,
76
95
  Napi::Promise::Deferred deferred)
77
- : Napi::AsyncWorker(deferred.Env()), path_(path), hidden_(hidden),
78
- deferred_(deferred) {
96
+ : Napi::AsyncWorker(deferred.Env()), path_(std::move(path)),
97
+ hidden_(hidden), deferred_(deferred) {
79
98
  DEBUG_LOG("[SetHiddenWorker] created for path: %s, hidden: %d", path_.c_str(),
80
99
  hidden_);
81
100
  }
@@ -84,22 +103,34 @@ void SetHiddenWorker::Execute() {
84
103
  DEBUG_LOG("[SetHiddenWorker] setting hidden=%d for: %s", hidden_,
85
104
  path_.c_str());
86
105
 
87
- // Add path validation to prevent directory traversal
88
- if (path_.find("..") != std::string::npos) {
89
- SetError("Invalid path containing '..'");
106
+ // macOS uses BSD file flags (UF_HIDDEN) to control file visibility.
107
+ // This is different from the dot-prefix convention used on Unix systems.
108
+ // The chflags() system call modifies these BSD-specific file flags.
109
+
110
+ // Validate and canonicalize path using realpath() to prevent directory
111
+ // traversal This follows Apple's Secure Coding Guide recommendations For
112
+ // setHidden, the file must exist, so we use ValidatePathForRead
113
+ std::string error;
114
+ std::string validated_path = ValidatePathForRead(path_, error);
115
+ if (validated_path.empty()) {
116
+ SetError(error);
90
117
  return;
91
118
  }
92
119
 
120
+ // Use the validated path for all subsequent operations
121
+ DEBUG_LOG("[SetHiddenWorker] Using validated path: %s",
122
+ validated_path.c_str());
123
+
93
124
  struct stat statbuf;
94
- if (stat(path_.c_str(), &statbuf) != 0) {
125
+ if (stat(validated_path.c_str(), &statbuf) != 0) {
95
126
  int error = errno;
96
127
  if (error == ENOENT) {
97
- DEBUG_LOG("[SetHiddenWorker] path not found: %s", path_.c_str());
98
- SetError("Path not found: '" + path_ + "'");
128
+ DEBUG_LOG("[SetHiddenWorker] path not found: %s", validated_path.c_str());
129
+ SetError("Path not found: '" + validated_path + "'");
99
130
  } else {
100
131
  DEBUG_LOG("[SetHiddenWorker] failed to stat path %s: %s (%d)",
101
- path_.c_str(), strerror(error), error);
102
- SetError(CreatePathErrorMessage("stat", path_, error));
132
+ validated_path.c_str(), strerror(error), error);
133
+ SetError(CreatePathErrorMessage("stat", validated_path, error));
103
134
  }
104
135
  return;
105
136
  }
@@ -111,15 +142,32 @@ void SetHiddenWorker::Execute() {
111
142
  new_flags = statbuf.st_flags & ~UF_HIDDEN;
112
143
  }
113
144
 
114
- if (chflags(path_.c_str(), new_flags) != 0) {
145
+ if (chflags(validated_path.c_str(), new_flags) != 0) {
115
146
  int error = errno;
116
147
  DEBUG_LOG("[SetHiddenWorker] failed to set flags for %s: %s (%d)",
117
- path_.c_str(), strerror(error), error);
118
- SetError(CreatePathErrorMessage("chflags", path_, error));
148
+ validated_path.c_str(), strerror(error), error);
149
+
150
+ // Check if this is an APFS filesystem issue
151
+ struct statfs fs;
152
+ bool is_apfs = false;
153
+ if (statfs(validated_path.c_str(), &fs) == 0) {
154
+ is_apfs = (strcmp(fs.f_fstypename, "apfs") == 0);
155
+ DEBUG_LOG("[SetHiddenWorker] filesystem type: %s", fs.f_fstypename);
156
+ }
157
+
158
+ // Provide more detailed error message for APFS
159
+ if (is_apfs && (error == EPERM || error == ENOTSUP)) {
160
+ SetError("Setting hidden attribute failed on APFS filesystem. "
161
+ "This is a known issue with some APFS volumes. "
162
+ "Error: " +
163
+ CreatePathErrorMessage("chflags", validated_path, error));
164
+ } else {
165
+ SetError(CreatePathErrorMessage("chflags", validated_path, error));
166
+ }
119
167
  return;
120
168
  }
121
169
  DEBUG_LOG("[SetHiddenWorker] successfully set hidden=%d for: %s", hidden_,
122
- path_.c_str());
170
+ validated_path.c_str());
123
171
  }
124
172
 
125
173
  void SetHiddenWorker::OnOK() {
@@ -0,0 +1,149 @@
1
+ // src/darwin/path_security.h
2
+ // Secure path validation for macOS using realpath()
3
+ // Implements recommendations from Apple's Secure Coding Guide
4
+ // https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/RaceConditions.html
5
+
6
+ #pragma once
7
+
8
+ #include "../common/debug_log.h"
9
+ #include "../common/error_utils.h"
10
+ #include <cerrno>
11
+ #include <cstring>
12
+ #include <string>
13
+ #include <sys/param.h> // For PATH_MAX
14
+ #include <unistd.h> // For realpath()
15
+
16
+ namespace FSMeta {
17
+
18
+ /**
19
+ * Validates a path for security issues and canonicalizes it using realpath().
20
+ *
21
+ * This function prevents directory traversal attacks by:
22
+ * 1. Checking for null bytes (path injection)
23
+ * 2. Using realpath() to resolve symbolic links and path references (../, ./)
24
+ * 3. Handling non-existent paths by validating the parent directory
25
+ *
26
+ * @param path The path to validate
27
+ * @param error Output parameter for error message if validation fails
28
+ * @param allow_nonexistent If true, allows paths that don't exist by validating
29
+ * parent
30
+ * @return The canonicalized path, or empty string if validation fails
31
+ */
32
+ inline std::string ValidateAndCanonicalizePath(const std::string &path,
33
+ std::string &error,
34
+ bool allow_nonexistent = false) {
35
+ DEBUG_LOG("[ValidateAndCanonicalizePath] Validating path: %s "
36
+ "(allow_nonexistent: %d)",
37
+ path.c_str(), allow_nonexistent);
38
+
39
+ // Check for empty path
40
+ if (path.empty()) {
41
+ error = "Empty path provided";
42
+ DEBUG_LOG("[ValidateAndCanonicalizePath] %s", error.c_str());
43
+ return "";
44
+ }
45
+
46
+ // Security check #1: Reject paths with null bytes (path injection attack)
47
+ if (path.find('\0') != std::string::npos) {
48
+ error = "Invalid path containing null byte";
49
+ DEBUG_LOG("[ValidateAndCanonicalizePath] %s", error.c_str());
50
+ return "";
51
+ }
52
+
53
+ // Security check #2: Use realpath() to canonicalize and validate
54
+ // realpath() resolves symbolic links and eliminates ../, ./, redundant
55
+ // slashes
56
+ char resolved_path[PATH_MAX];
57
+ if (realpath(path.c_str(), resolved_path) != nullptr) {
58
+ // Path exists and was successfully canonicalized
59
+ std::string canonical_path(resolved_path);
60
+ DEBUG_LOG("[ValidateAndCanonicalizePath] Canonicalized: %s -> %s",
61
+ path.c_str(), canonical_path.c_str());
62
+ return canonical_path;
63
+ }
64
+
65
+ // realpath() failed - check if it's because the path doesn't exist
66
+ int realpath_error = errno;
67
+
68
+ if (realpath_error == ENOENT && allow_nonexistent) {
69
+ // For operations that create files (like setHidden), validate parent
70
+ // directory
71
+ DEBUG_LOG(
72
+ "[ValidateAndCanonicalizePath] Path doesn't exist, validating parent");
73
+
74
+ // Find the parent directory
75
+ size_t last_slash = path.find_last_of('/');
76
+ std::string parent_dir;
77
+
78
+ if (last_slash == std::string::npos) {
79
+ // No slash found - relative path, use current directory
80
+ parent_dir = ".";
81
+ } else if (last_slash == 0) {
82
+ // Root directory
83
+ parent_dir = "/";
84
+ } else {
85
+ parent_dir = path.substr(0, last_slash);
86
+ }
87
+
88
+ // Validate parent directory exists and is accessible
89
+ if (realpath(parent_dir.c_str(), resolved_path) == nullptr) {
90
+ int parent_error = errno;
91
+ error =
92
+ CreatePathErrorMessage("realpath (parent)", parent_dir, parent_error);
93
+ DEBUG_LOG("[ValidateAndCanonicalizePath] Parent validation failed: %s",
94
+ error.c_str());
95
+ return "";
96
+ }
97
+
98
+ // Parent is valid - construct the full path
99
+ std::string parent_canonical(resolved_path);
100
+ std::string filename =
101
+ (last_slash == std::string::npos) ? path : path.substr(last_slash + 1);
102
+
103
+ std::string result;
104
+ if (parent_canonical == "/") {
105
+ result = "/" + filename;
106
+ } else {
107
+ result = parent_canonical + "/" + filename;
108
+ }
109
+
110
+ DEBUG_LOG(
111
+ "[ValidateAndCanonicalizePath] Validated non-existent path: %s -> %s",
112
+ path.c_str(), result.c_str());
113
+ return result;
114
+ }
115
+
116
+ // realpath() failed for a different reason, or path doesn't exist and we
117
+ // don't allow it
118
+ error = CreatePathErrorMessage("realpath", path, realpath_error);
119
+ DEBUG_LOG("[ValidateAndCanonicalizePath] Failed: %s", error.c_str());
120
+ return "";
121
+ }
122
+
123
+ /**
124
+ * Validates that a path is secure for read operations.
125
+ * The path must exist and be accessible.
126
+ *
127
+ * @param path The path to validate
128
+ * @param error Output parameter for error message if validation fails
129
+ * @return The canonicalized path, or empty string if validation fails
130
+ */
131
+ inline std::string ValidatePathForRead(const std::string &path,
132
+ std::string &error) {
133
+ return ValidateAndCanonicalizePath(path, error, false);
134
+ }
135
+
136
+ /**
137
+ * Validates that a path is secure for write operations.
138
+ * The path may not exist, but its parent directory must be valid.
139
+ *
140
+ * @param path The path to validate
141
+ * @param error Output parameter for error message if validation fails
142
+ * @return The canonicalized path, or empty string if validation fails
143
+ */
144
+ inline std::string ValidatePathForWrite(const std::string &path,
145
+ std::string &error) {
146
+ return ValidateAndCanonicalizePath(path, error, true);
147
+ }
148
+
149
+ } // namespace FSMeta
@@ -1,11 +1,18 @@
1
1
  #pragma once
2
2
 
3
3
  #include <CoreFoundation/CoreFoundation.h>
4
+ #include <DiskArbitration/DiskArbitration.h>
4
5
  #include <sys/mount.h>
5
6
 
7
+ // RAII (Resource Acquisition Is Initialization) utilities for macOS APIs.
8
+ // These wrappers ensure proper cleanup of system resources even in the
9
+ // presence of exceptions, preventing memory leaks and resource exhaustion.
10
+
6
11
  namespace FSMeta {
7
12
 
8
- // Generic RAII wrapper for resources that need free()
13
+ // Generic RAII wrapper for resources that need free().
14
+ // This is used for C-style allocations that must be freed with free().
15
+ // Common usage: buffers returned by system APIs like getmntinfo_r_np().
9
16
  template <typename T> class ResourceRAII {
10
17
  private:
11
18
  T *resource_;
@@ -41,10 +48,48 @@ public:
41
48
  ResourceRAII &operator=(const ResourceRAII &) = delete;
42
49
  };
43
50
 
44
- // Specialized for mount info
45
- using MountBufferRAII = ResourceRAII<struct statfs>;
51
+ // Specialized RAII wrapper for mount buffer from getmntinfo_r_np().
52
+ // getmntinfo_r_np() allocates a buffer that the caller must free.
53
+ // This wrapper ensures the buffer is freed even if exceptions occur.
54
+ class MountBufferRAII {
55
+ private:
56
+ struct statfs *buffer_;
57
+
58
+ public:
59
+ MountBufferRAII() : buffer_(nullptr) {}
60
+ ~MountBufferRAII() {
61
+ if (buffer_) {
62
+ free(buffer_);
63
+ }
64
+ }
46
65
 
47
- // CoreFoundation RAII wrapper
66
+ struct statfs **ptr() { return &buffer_; }
67
+ struct statfs *get() { return buffer_; }
68
+
69
+ // Add move operations for better resource management
70
+ MountBufferRAII(MountBufferRAII &&other) noexcept : buffer_(other.buffer_) {
71
+ other.buffer_ = nullptr;
72
+ }
73
+
74
+ MountBufferRAII &operator=(MountBufferRAII &&other) noexcept {
75
+ if (this != &other) {
76
+ if (buffer_)
77
+ free(buffer_);
78
+ buffer_ = other.buffer_;
79
+ other.buffer_ = nullptr;
80
+ }
81
+ return *this;
82
+ }
83
+
84
+ // Prevent copying
85
+ MountBufferRAII(const MountBufferRAII &) = delete;
86
+ MountBufferRAII &operator=(const MountBufferRAII &) = delete;
87
+ };
88
+
89
+ // CoreFoundation RAII wrapper following the Create/Copy/Get rule.
90
+ // Any CF object obtained via Create or Copy functions must be released.
91
+ // This wrapper automatically calls CFRelease() in the destructor,
92
+ // preventing memory leaks from Core Foundation objects.
48
93
  template <typename T> class CFReleaser {
49
94
  private:
50
95
  T ref_;
@@ -82,4 +127,59 @@ public:
82
127
  }
83
128
  };
84
129
 
130
+ // Specialized RAII wrapper for DASession that handles dispatch queue lifecycle.
131
+ // DASessionSetDispatchQueue must be called with NULL before the session is
132
+ // released. This wrapper ensures proper cleanup order: unschedule then release.
133
+ class DASessionRAII {
134
+ private:
135
+ CFReleaser<DASessionRef> session_;
136
+ bool is_scheduled_;
137
+
138
+ public:
139
+ explicit DASessionRAII(DASessionRef session = nullptr) noexcept
140
+ : session_(session), is_scheduled_(false) {}
141
+
142
+ ~DASessionRAII() { unschedule(); }
143
+
144
+ // Schedule the session on a dispatch queue
145
+ void scheduleOnQueue(dispatch_queue_t queue) {
146
+ if (session_.isValid() && queue != nullptr) {
147
+ DASessionSetDispatchQueue(session_.get(), queue);
148
+ is_scheduled_ = true;
149
+ }
150
+ }
151
+
152
+ // Unschedule the session (must be called before session is released)
153
+ void unschedule() {
154
+ if (is_scheduled_ && session_.isValid()) {
155
+ DASessionSetDispatchQueue(session_.get(), nullptr);
156
+ is_scheduled_ = false;
157
+ }
158
+ }
159
+
160
+ DASessionRef get() const noexcept { return session_.get(); }
161
+ bool isValid() const noexcept { return session_.isValid(); }
162
+
163
+ // Prevent copying
164
+ DASessionRAII(const DASessionRAII &) = delete;
165
+ DASessionRAII &operator=(const DASessionRAII &) = delete;
166
+
167
+ // Allow moving
168
+ DASessionRAII(DASessionRAII &&other) noexcept
169
+ : session_(std::move(other.session_)),
170
+ is_scheduled_(other.is_scheduled_) {
171
+ other.is_scheduled_ = false;
172
+ }
173
+
174
+ DASessionRAII &operator=(DASessionRAII &&other) noexcept {
175
+ if (this != &other) {
176
+ unschedule();
177
+ session_ = std::move(other.session_);
178
+ is_scheduled_ = other.is_scheduled_;
179
+ other.is_scheduled_ = false;
180
+ }
181
+ return *this;
182
+ }
183
+ };
184
+
85
185
  } // namespace FSMeta