@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
|
@@ -31,6 +31,11 @@ public:
|
|
|
31
31
|
// separately and our error handling already covers mount state changes
|
|
32
32
|
// See https://github.com/swiftlang/swift-corelibs-foundation/issues/4649
|
|
33
33
|
|
|
34
|
+
// getmntinfo_r_np is the thread-safe version of getmntinfo().
|
|
35
|
+
// The "_r" suffix indicates "reentrant" (thread-safe).
|
|
36
|
+
// The "_np" suffix indicates "non-portable" (Apple-specific).
|
|
37
|
+
// This function allocates a new buffer that we must free (handled by
|
|
38
|
+
// RAII).
|
|
34
39
|
int count = getmntinfo_r_np(mntbuf.ptr(), MNT_NOWAIT);
|
|
35
40
|
|
|
36
41
|
if (count <= 0) {
|
|
@@ -43,64 +48,92 @@ public:
|
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
mp.mountPoint = mntbuf.get()[i].f_mntonname;
|
|
49
|
-
mp.fstype = mntbuf.get()[i].f_fstypename;
|
|
50
|
-
mp.error = ""; // Initialize error field
|
|
51
|
+
// Process mount points in batches to limit concurrent threads
|
|
52
|
+
const size_t maxConcurrentChecks = 4; // Limit concurrent access checks
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
for (size_t i = 0; i < static_cast<size_t>(count);
|
|
55
|
+
i += maxConcurrentChecks) {
|
|
56
|
+
std::vector<std::future<std::pair<std::string, bool>>> futures;
|
|
57
|
+
std::vector<MountPoint> batchMountPoints;
|
|
58
|
+
|
|
59
|
+
// Create batch of mount points and launch their checks
|
|
60
|
+
for (size_t j = i;
|
|
61
|
+
j < static_cast<size_t>(count) && j < i + maxConcurrentChecks;
|
|
62
|
+
j++) {
|
|
63
|
+
MountPoint mp;
|
|
64
|
+
mp.mountPoint = mntbuf.get()[j].f_mntonname;
|
|
65
|
+
mp.fstype = mntbuf.get()[j].f_fstypename;
|
|
66
|
+
mp.error = ""; // Initialize error field
|
|
67
|
+
|
|
68
|
+
DEBUG_LOG("[GetVolumeMountPointsWorker] Checking mount point: %s",
|
|
69
|
+
mp.mountPoint.c_str());
|
|
54
70
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
batchMountPoints.push_back(mp);
|
|
72
|
+
|
|
73
|
+
// Launch async check
|
|
74
|
+
futures.push_back(
|
|
58
75
|
std::async(std::launch::async, [path = mp.mountPoint]() {
|
|
59
|
-
//
|
|
60
|
-
|
|
76
|
+
// faccessat is preferred over access() for security:
|
|
77
|
+
// - AT_FDCWD: Use current working directory as base
|
|
78
|
+
// - AT_EACCESS: Check using effective user/group IDs (not real
|
|
79
|
+
// IDs) This prevents TOCTOU attacks and privilege escalation
|
|
80
|
+
// issues
|
|
81
|
+
bool accessible =
|
|
82
|
+
faccessat(AT_FDCWD, path.c_str(), R_OK, AT_EACCESS) == 0;
|
|
83
|
+
return std::make_pair(path, accessible);
|
|
61
84
|
}));
|
|
85
|
+
}
|
|
62
86
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
// Process results for this batch
|
|
88
|
+
for (size_t k = 0; k < futures.size(); k++) {
|
|
89
|
+
auto &mp = batchMountPoints[k];
|
|
90
|
+
try {
|
|
91
|
+
auto status =
|
|
92
|
+
futures[k].wait_for(std::chrono::milliseconds(timeoutMs_));
|
|
93
|
+
|
|
94
|
+
switch (status) {
|
|
95
|
+
case std::future_status::timeout:
|
|
96
|
+
mp.status = "disconnected";
|
|
97
|
+
mp.error = "Access check timed out";
|
|
98
|
+
DEBUG_LOG(
|
|
99
|
+
"[GetVolumeMountPointsWorker] Access check timed out: %s",
|
|
100
|
+
mp.mountPoint.c_str());
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case std::future_status::ready:
|
|
104
|
+
try {
|
|
105
|
+
auto result = futures[k].get();
|
|
106
|
+
bool isAccessible = result.second;
|
|
107
|
+
mp.status = isAccessible ? "healthy" : "inaccessible";
|
|
108
|
+
if (!isAccessible) {
|
|
109
|
+
mp.error = "Path is not accessible";
|
|
110
|
+
}
|
|
111
|
+
DEBUG_LOG("[GetVolumeMountPointsWorker] Access check %s: %s",
|
|
112
|
+
isAccessible ? "succeeded" : "failed",
|
|
113
|
+
mp.mountPoint.c_str());
|
|
114
|
+
} catch (const std::exception &e) {
|
|
115
|
+
mp.status = "error";
|
|
116
|
+
mp.error = std::string("Access check failed: ") + e.what();
|
|
117
|
+
DEBUG_LOG("[GetVolumeMountPointsWorker] Exception: %s",
|
|
118
|
+
e.what());
|
|
79
119
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} catch (const std::exception &e) {
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
default:
|
|
84
123
|
mp.status = "error";
|
|
85
|
-
mp.error =
|
|
86
|
-
DEBUG_LOG("[GetVolumeMountPointsWorker]
|
|
124
|
+
mp.error = "Unexpected future status";
|
|
125
|
+
DEBUG_LOG("[GetVolumeMountPointsWorker] Unexpected status: %s",
|
|
126
|
+
mp.mountPoint.c_str());
|
|
127
|
+
break;
|
|
87
128
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
default:
|
|
129
|
+
} catch (const std::exception &e) {
|
|
91
130
|
mp.status = "error";
|
|
92
|
-
mp.error = "
|
|
93
|
-
DEBUG_LOG("[GetVolumeMountPointsWorker]
|
|
94
|
-
mp.mountPoint.c_str());
|
|
95
|
-
break;
|
|
131
|
+
mp.error = std::string("Mount point check failed: ") + e.what();
|
|
132
|
+
DEBUG_LOG("[GetVolumeMountPointsWorker] Exception: %s", e.what());
|
|
96
133
|
}
|
|
97
|
-
} catch (const std::exception &e) {
|
|
98
|
-
mp.status = "error";
|
|
99
|
-
mp.error = std::string("Mount point check failed: ") + e.what();
|
|
100
|
-
DEBUG_LOG("[GetVolumeMountPointsWorker] Exception: %s", e.what());
|
|
101
|
-
}
|
|
102
134
|
|
|
103
|
-
|
|
135
|
+
mountPoints_.push_back(std::move(mp));
|
|
136
|
+
}
|
|
104
137
|
}
|
|
105
138
|
} catch (const std::exception &e) {
|
|
106
139
|
SetError(std::string("Failed to process mount points: ") + e.what());
|
package/src/hidden.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { rename } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { debug } from "./debuglog";
|
|
5
6
|
import { WrappedError } from "./error";
|
|
6
|
-
import {
|
|
7
|
+
import { statAsync } from "./fs";
|
|
7
8
|
import { isRootDirectory, normalizePath } from "./path";
|
|
8
9
|
import { isWindows } from "./platform";
|
|
9
10
|
import type { HiddenMetadata } from "./types/hidden_metadata";
|
|
@@ -47,14 +48,24 @@ export async function isHiddenImpl(
|
|
|
47
48
|
pathname: string,
|
|
48
49
|
nativeFn: NativeBindingsFn,
|
|
49
50
|
): Promise<boolean> {
|
|
51
|
+
debug("isHiddenImpl called with pathname: %s", pathname);
|
|
50
52
|
const norm = normalizePath(pathname);
|
|
51
53
|
if (norm == null) {
|
|
52
54
|
throw new Error("Invalid pathname: " + JSON.stringify(pathname));
|
|
53
55
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
debug("Normalized path: %s", norm);
|
|
57
|
+
debug(
|
|
58
|
+
"LocalSupport: dotPrefix=%s, systemFlag=%s",
|
|
59
|
+
LocalSupport.dotPrefix,
|
|
60
|
+
LocalSupport.systemFlag,
|
|
57
61
|
);
|
|
62
|
+
|
|
63
|
+
const result =
|
|
64
|
+
(LocalSupport.dotPrefix && isPosixHidden(norm)) ||
|
|
65
|
+
(LocalSupport.systemFlag && (await isSystemHidden(norm, nativeFn)));
|
|
66
|
+
|
|
67
|
+
debug("isHiddenImpl returning: %s", result);
|
|
68
|
+
return result;
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
export async function isHiddenRecursiveImpl(
|
|
@@ -108,20 +119,32 @@ async function isSystemHidden(
|
|
|
108
119
|
pathname: string,
|
|
109
120
|
nativeFn: NativeBindingsFn,
|
|
110
121
|
): Promise<boolean> {
|
|
122
|
+
debug("isSystemHidden called with pathname: %s", pathname);
|
|
111
123
|
if (!LocalSupport.systemFlag) {
|
|
124
|
+
debug("systemFlag not supported on this platform");
|
|
112
125
|
// not supported on this platform
|
|
113
126
|
return false;
|
|
114
127
|
}
|
|
115
|
-
if (isWindows && isRootDirectory(pathname)) {
|
|
116
|
-
// windows `attr` thinks all drive letters don't exist.
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
128
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
// Let the native function handle all validation, including root directories
|
|
130
|
+
// This ensures security checks are performed before any other checks
|
|
131
|
+
const native = await nativeFn();
|
|
132
|
+
debug("Calling native isHidden for: %s", pathname);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const isHidden = await native.isHidden(pathname);
|
|
136
|
+
debug("Native isHidden returned: %s", isHidden);
|
|
137
|
+
return isHidden;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
debug("Native isHidden threw error: %s", error);
|
|
140
|
+
// Handle non-existent paths by returning false (consistent with Windows behavior)
|
|
141
|
+
const errorStr = String(error);
|
|
142
|
+
if (errorStr.includes("Path not found")) {
|
|
143
|
+
debug("Path not found, returning false");
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
125
148
|
}
|
|
126
149
|
|
|
127
150
|
/**
|
|
@@ -21,25 +21,24 @@ void GioMountPointsWorker::Execute() {
|
|
|
21
21
|
try {
|
|
22
22
|
DEBUG_LOG("[GioMountPoints] processing mounts");
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
}
|
|
24
|
+
// Use thread-safe g_unix_mounts_get() API
|
|
25
|
+
MountIterator::forEachMount([this](GUnixMountEntry *entry) {
|
|
26
|
+
// Get mount path and filesystem type from thread-safe Unix mount API
|
|
27
|
+
const char *mount_path = g_unix_mount_get_mount_path(entry);
|
|
28
|
+
const char *fs_type = g_unix_mount_get_fs_type(entry);
|
|
29
|
+
|
|
30
|
+
if (mount_path && fs_type) {
|
|
31
|
+
DEBUG_LOG("[GioMountPoints] found {mountPoint: %s, fsType: %s}",
|
|
32
|
+
mount_path, fs_type);
|
|
33
|
+
|
|
34
|
+
MountPoint point{};
|
|
35
|
+
point.mountPoint = mount_path;
|
|
36
|
+
point.fstype = fs_type;
|
|
37
|
+
mountPoints.push_back(point);
|
|
38
|
+
} else {
|
|
39
|
+
DEBUG_LOG("[GioMountPoints] skipping mount with null path or fstype");
|
|
42
40
|
}
|
|
41
|
+
|
|
43
42
|
return true; // Continue iteration
|
|
44
43
|
});
|
|
45
44
|
|
package/src/linux/gio_utils.cpp
CHANGED
|
@@ -1,73 +1,128 @@
|
|
|
1
1
|
// src/linux/gio_utils.cpp
|
|
2
|
+
//
|
|
3
|
+
// Thread-Safe Mount Enumeration for Linux
|
|
4
|
+
//
|
|
5
|
+
// This implementation uses g_unix_mounts_get() as the primary, thread-safe path
|
|
6
|
+
// for enumerating mounts. GVolumeMonitor is optionally used for enrichment but
|
|
7
|
+
// is NOT required for correct operation.
|
|
8
|
+
//
|
|
9
|
+
// IMPORTANT THREAD SAFETY NOTES:
|
|
10
|
+
//
|
|
11
|
+
// According to GIO documentation
|
|
12
|
+
// (https://docs.gtk.org/gio/class.VolumeMonitor.html): "GVolumeMonitor is not
|
|
13
|
+
// thread-default-context aware and so should not be used other than from the
|
|
14
|
+
// main thread, with no thread-default-context active."
|
|
15
|
+
//
|
|
16
|
+
// However, g_unix_mounts_get() is explicitly thread-safe:
|
|
17
|
+
// - Uses getmntent_r() when available (reentrant)
|
|
18
|
+
// - Falls back to getmntent() with G_LOCK protection
|
|
19
|
+
// See: https://gitlab.gnome.org/GNOME/glib/-/blob/main/gio/gunixmounts.c
|
|
20
|
+
//
|
|
21
|
+
// This design:
|
|
22
|
+
// ✅ Primary path uses thread-safe g_unix_mounts_get()
|
|
23
|
+
// ✅ Optional GVolumeMonitor enhancement (best-effort, may be skipped)
|
|
24
|
+
// ✅ Fixes Finding #6 (thread safety violation)
|
|
25
|
+
// ✅ Fixes Finding #7 (double-free risk with g_list_free_full)
|
|
2
26
|
|
|
3
27
|
#ifdef ENABLE_GIO
|
|
4
28
|
|
|
5
29
|
#include "gio_utils.h"
|
|
6
30
|
#include "../common/debug_log.h"
|
|
7
31
|
#include <gio/gio.h>
|
|
32
|
+
#include <gio/gunixmounts.h>
|
|
8
33
|
#include <memory>
|
|
9
34
|
#include <stdexcept>
|
|
10
35
|
|
|
11
36
|
namespace FSMeta {
|
|
12
37
|
namespace gio {
|
|
13
38
|
|
|
14
|
-
// HEY FUTURE ME: DON'T `g_object_unref` THIS POINTER!
|
|
15
|
-
GVolumeMonitor *MountIterator::getMonitor() {
|
|
16
|
-
GVolumeMonitor *monitor = g_volume_monitor_get();
|
|
17
|
-
if (!monitor) {
|
|
18
|
-
DEBUG_LOG("[gio::getMonitor] g_volume_monitor_get() failed");
|
|
19
|
-
throw std::runtime_error("Failed to get GVolumeMonitor");
|
|
20
|
-
}
|
|
21
|
-
return monitor;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
39
|
void MountIterator::forEachMount(const MountCallback &callback) {
|
|
25
|
-
|
|
40
|
+
// PRIMARY PATH: Thread-safe Unix mount enumeration
|
|
41
|
+
// g_unix_mounts_get() is documented as thread-safe and can be called
|
|
42
|
+
// from worker threads without violating GIO threading requirements.
|
|
43
|
+
GList *unix_mounts = g_unix_mounts_get(nullptr);
|
|
26
44
|
|
|
27
|
-
if (!
|
|
45
|
+
if (!unix_mounts) {
|
|
28
46
|
DEBUG_LOG("[gio::MountIterator::forEachMount] no mounts found");
|
|
29
47
|
return;
|
|
30
48
|
}
|
|
31
49
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
DEBUG_LOG("[gio::MountIterator::forEachMount] processing Unix mounts");
|
|
51
|
+
|
|
52
|
+
// Iterate over all Unix mounts
|
|
53
|
+
GList *current = unix_mounts;
|
|
54
|
+
bool should_continue = true;
|
|
55
|
+
|
|
56
|
+
while (current && should_continue) {
|
|
57
|
+
GUnixMountEntry *entry = static_cast<GUnixMountEntry *>(current->data);
|
|
35
58
|
|
|
36
|
-
if (!
|
|
37
|
-
DEBUG_LOG("[gio::MountIterator::forEachMount] Skipping
|
|
59
|
+
if (!entry) {
|
|
60
|
+
DEBUG_LOG("[gio::MountIterator::forEachMount] Skipping null entry");
|
|
61
|
+
current = current->next;
|
|
38
62
|
continue;
|
|
39
63
|
}
|
|
40
64
|
|
|
41
|
-
// Take an extra reference on the mount while we work with it
|
|
42
|
-
g_object_ref(mount);
|
|
43
|
-
|
|
44
65
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (root.get() && G_IS_FILE(root.get())) {
|
|
49
|
-
const bool continue_iteration = callback(mount, root.get());
|
|
50
|
-
g_object_unref(mount);
|
|
51
|
-
|
|
52
|
-
if (!continue_iteration) {
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
66
|
+
// Get mount path from thread-safe Unix mount API
|
|
67
|
+
const char *mount_path = g_unix_mount_get_mount_path(entry);
|
|
68
|
+
if (!mount_path) {
|
|
56
69
|
DEBUG_LOG(
|
|
57
|
-
"[gio::MountIterator::forEachMount]
|
|
58
|
-
|
|
70
|
+
"[gio::MountIterator::forEachMount] Skipping mount with null path");
|
|
71
|
+
current = current->next;
|
|
72
|
+
continue;
|
|
59
73
|
}
|
|
74
|
+
|
|
75
|
+
DEBUG_LOG("[gio::MountIterator::forEachMount] processing mount: %s",
|
|
76
|
+
mount_path);
|
|
77
|
+
|
|
78
|
+
// Invoke callback with Unix mount entry
|
|
79
|
+
// The callback receives the entry and can extract data using
|
|
80
|
+
// g_unix_mount_get_* functions
|
|
81
|
+
should_continue = callback(entry);
|
|
82
|
+
|
|
60
83
|
} catch (const std::exception &e) {
|
|
61
84
|
DEBUG_LOG("[gio::MountIterator::forEachMount] Exception during mount "
|
|
62
85
|
"processing: %s",
|
|
63
86
|
e.what());
|
|
64
|
-
|
|
65
|
-
|
|
87
|
+
// Clean up and re-throw
|
|
88
|
+
g_list_free_full(unix_mounts,
|
|
89
|
+
reinterpret_cast<GDestroyNotify>(g_unix_mount_free));
|
|
90
|
+
throw;
|
|
66
91
|
}
|
|
92
|
+
|
|
93
|
+
current = current->next;
|
|
67
94
|
}
|
|
68
95
|
|
|
69
|
-
// Free
|
|
70
|
-
|
|
96
|
+
// Free list and all mount entries
|
|
97
|
+
// Each entry is freed with g_unix_mount_free() - no double-free risk
|
|
98
|
+
g_list_free_full(unix_mounts,
|
|
99
|
+
reinterpret_cast<GDestroyNotify>(g_unix_mount_free));
|
|
100
|
+
|
|
101
|
+
DEBUG_LOG("[gio::MountIterator::forEachMount] completed");
|
|
102
|
+
}
|
|
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
|
+
}
|
|
71
126
|
}
|
|
72
127
|
|
|
73
128
|
} // namespace gio
|
package/src/linux/gio_utils.h
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#ifdef ENABLE_GIO
|
|
5
5
|
|
|
6
6
|
#include <gio/gio.h>
|
|
7
|
+
#include <gio/gunixmounts.h>
|
|
7
8
|
#include <napi.h>
|
|
8
9
|
#include <string>
|
|
9
10
|
#include <vector>
|
|
@@ -46,14 +47,19 @@ namespace gio {
|
|
|
46
47
|
class MountIterator {
|
|
47
48
|
public:
|
|
48
49
|
// Callback type for mount processing
|
|
49
|
-
|
|
50
|
+
// Receives GUnixMountEntry which provides thread-safe access to mount data
|
|
51
|
+
// Return true to continue iteration, false to stop
|
|
52
|
+
using MountCallback = std::function<bool(GUnixMountEntry *)>;
|
|
50
53
|
|
|
51
|
-
// Static method to iterate over mounts
|
|
54
|
+
// Static method to iterate over mounts using thread-safe g_unix_mounts_get()
|
|
55
|
+
// This is safe to call from worker threads
|
|
52
56
|
static void forEachMount(const MountCallback &callback);
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
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;
|
|
57
63
|
};
|
|
58
64
|
|
|
59
65
|
// Helper class for scoped GIO resource management
|
|
@@ -16,72 +16,135 @@ void addMountMetadata(const std::string &mountPoint, VolumeMetadata &metadata) {
|
|
|
16
16
|
DEBUG_LOG("[gio::addMountMetadata] getting mount metadata for %s",
|
|
17
17
|
mountPoint.c_str());
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
bool found = false;
|
|
20
|
+
|
|
21
|
+
// PRIMARY PATH: Thread-safe Unix mount API
|
|
22
|
+
MountIterator::forEachMount([&](GUnixMountEntry *entry) {
|
|
23
|
+
const char *mount_path = g_unix_mount_get_mount_path(entry);
|
|
24
|
+
if (!mount_path || mountPoint != mount_path) {
|
|
22
25
|
return true; // Continue iteration
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
// Found matching mount point
|
|
26
29
|
DEBUG_LOG("[gio::addMountMetadata] found matching mount point: %s",
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Get volume information
|
|
30
|
-
const GObjectPtr<GVolume> volume(g_mount_get_volume(mount));
|
|
31
|
-
if (volume && volume.get()) {
|
|
32
|
-
const GCharPtr label(g_volume_get_name(volume.get()));
|
|
33
|
-
if (label && label.get()) {
|
|
34
|
-
DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s, label: %s}",
|
|
35
|
-
path.get(), label.get());
|
|
36
|
-
metadata.label = label.get();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const GCharPtr mount_name(g_mount_get_name(mount));
|
|
41
|
-
if (mount_name) {
|
|
42
|
-
metadata.mountName = mount_name.get();
|
|
43
|
-
}
|
|
30
|
+
mount_path);
|
|
31
|
+
found = true;
|
|
44
32
|
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s,
|
|
50
|
-
|
|
51
|
-
metadata.
|
|
33
|
+
// Get basic metadata from thread-safe Unix mount API
|
|
34
|
+
if (metadata.fstype.empty()) {
|
|
35
|
+
const char *fs_type = g_unix_mount_get_fs_type(entry);
|
|
36
|
+
if (fs_type) {
|
|
37
|
+
DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s, fsType: %s}",
|
|
38
|
+
mount_path, fs_type);
|
|
39
|
+
metadata.fstype = fs_type;
|
|
52
40
|
}
|
|
53
41
|
}
|
|
54
42
|
|
|
55
|
-
if (metadata.
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (fs_type_str) {
|
|
62
|
-
const GCharPtr fs_type(g_strdup(fs_type_str));
|
|
63
|
-
DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s, fsType: %s}",
|
|
64
|
-
path.get(), fs_type.get());
|
|
65
|
-
metadata.fstype = fs_type.get();
|
|
66
|
-
}
|
|
43
|
+
if (metadata.mountFrom.empty()) {
|
|
44
|
+
const char *device_path = g_unix_mount_get_device_path(entry);
|
|
45
|
+
if (device_path) {
|
|
46
|
+
DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s, mountFrom: %s}",
|
|
47
|
+
mount_path, device_path);
|
|
48
|
+
metadata.mountFrom = device_path;
|
|
67
49
|
}
|
|
68
50
|
}
|
|
69
51
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
52
|
+
// OPTIONAL ENHANCEMENT: Try to get rich metadata from GVolumeMonitor
|
|
53
|
+
// This may fail (thread safety violation), but that's OK - we have basic
|
|
54
|
+
// data
|
|
55
|
+
try {
|
|
56
|
+
GVolumeMonitor *monitor = MountIterator::tryGetMonitor();
|
|
57
|
+
if (monitor) {
|
|
58
|
+
DEBUG_LOG(
|
|
59
|
+
"[gio::addMountMetadata] attempting GVolumeMonitor enrichment");
|
|
60
|
+
|
|
61
|
+
// Try to find matching GMount for this path
|
|
62
|
+
GList *mounts = g_volume_monitor_get_mounts(monitor);
|
|
63
|
+
if (mounts) {
|
|
64
|
+
for (GList *l = mounts; l != nullptr; l = l->next) {
|
|
65
|
+
GMount *mount = G_MOUNT(l->data);
|
|
66
|
+
if (!mount || !G_IS_MOUNT(mount)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
GFile *root = g_mount_get_root(mount);
|
|
71
|
+
if (root) {
|
|
72
|
+
char *path = g_file_get_path(root);
|
|
73
|
+
if (path && mountPoint == path) {
|
|
74
|
+
// Found matching mount - try to get rich metadata
|
|
75
|
+
|
|
76
|
+
// Try to get volume label
|
|
77
|
+
if (metadata.label.empty()) {
|
|
78
|
+
GVolume *volume = g_mount_get_volume(mount);
|
|
79
|
+
if (volume) {
|
|
80
|
+
char *label = g_volume_get_name(volume);
|
|
81
|
+
if (label) {
|
|
82
|
+
DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s, "
|
|
83
|
+
"label: %s} (from GVolume)",
|
|
84
|
+
path, label);
|
|
85
|
+
metadata.label = label;
|
|
86
|
+
g_free(label);
|
|
87
|
+
}
|
|
88
|
+
g_object_unref(volume);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Try to get mount name
|
|
93
|
+
if (metadata.mountName.empty()) {
|
|
94
|
+
char *mount_name = g_mount_get_name(mount);
|
|
95
|
+
if (mount_name) {
|
|
96
|
+
metadata.mountName = mount_name;
|
|
97
|
+
g_free(mount_name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Try to get URI
|
|
102
|
+
if (metadata.uri.empty()) {
|
|
103
|
+
GFile *location = g_mount_get_default_location(mount);
|
|
104
|
+
if (location) {
|
|
105
|
+
char *uri = g_file_get_uri(location);
|
|
106
|
+
if (uri) {
|
|
107
|
+
DEBUG_LOG("[gio::addMountMetadata] {mountPoint: %s, uri: "
|
|
108
|
+
"%s} (from GMount)",
|
|
109
|
+
path, uri);
|
|
110
|
+
metadata.uri = uri;
|
|
111
|
+
g_free(uri);
|
|
112
|
+
}
|
|
113
|
+
g_object_unref(location);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
g_free(path);
|
|
118
|
+
g_object_unref(root);
|
|
119
|
+
break; // Found our mount
|
|
120
|
+
}
|
|
121
|
+
if (path)
|
|
122
|
+
g_free(path);
|
|
123
|
+
g_object_unref(root);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Clean up mounts list - this time correctly without double-free
|
|
128
|
+
g_list_free_full(mounts,
|
|
129
|
+
reinterpret_cast<GDestroyNotify>(g_object_unref));
|
|
79
130
|
}
|
|
131
|
+
|
|
132
|
+
// Note: Don't unref monitor - it's a singleton
|
|
80
133
|
}
|
|
134
|
+
} catch (const std::exception &e) {
|
|
135
|
+
DEBUG_LOG("[gio::addMountMetadata] GVolumeMonitor enrichment failed "
|
|
136
|
+
"(expected, not critical): %s",
|
|
137
|
+
e.what());
|
|
138
|
+
// Ignore - we have basic metadata from Unix mount API
|
|
81
139
|
}
|
|
82
140
|
|
|
83
141
|
return false; // Stop iteration, we found our mount
|
|
84
142
|
});
|
|
143
|
+
|
|
144
|
+
if (!found) {
|
|
145
|
+
DEBUG_LOG("[gio::addMountMetadata] mount point %s not found",
|
|
146
|
+
mountPoint.c_str());
|
|
147
|
+
}
|
|
85
148
|
}
|
|
86
149
|
|
|
87
150
|
} // namespace gio
|