@photostructure/fs-metadata 1.2.0 → 1.4.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 (41) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/CONTRIBUTING.md +3 -3
  3. package/binding.gyp +0 -22
  4. package/dist/index.cjs +11 -40
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +15 -3
  7. package/dist/index.d.mts +15 -3
  8. package/dist/index.d.ts +15 -3
  9. package/dist/index.mjs +11 -40
  10. package/dist/index.mjs.map +1 -1
  11. package/doc/C++_REVIEW_TODO.md +3 -36
  12. package/doc/LINUX_API_REFERENCE.md +4 -147
  13. package/doc/SECURITY_AUDIT_2026.md +5 -0
  14. package/doc/gotchas.md +27 -0
  15. package/package.json +9 -10
  16. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  17. package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
  18. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  19. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  20. package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  21. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  22. package/scripts/clang-tidy.ts +1 -3
  23. package/scripts/prebuild-linux-glibc.sh +1 -5
  24. package/scripts/sanitizers-test.sh +0 -1
  25. package/src/binding.cpp +0 -15
  26. package/src/index.ts +6 -2
  27. package/src/linux/mount_points.ts +8 -42
  28. package/src/linux/volume_metadata.cpp +0 -16
  29. package/src/mount_point_for_path.ts +1 -1
  30. package/src/types/mount_point.ts +1 -1
  31. package/src/types/native_bindings.ts +0 -5
  32. package/src/types/options.ts +14 -0
  33. package/src/volume_metadata.ts +22 -11
  34. package/src/volume_mount_points.ts +1 -1
  35. package/scripts/setup-native.mjs +0 -39
  36. package/src/linux/gio_mount_points.cpp +0 -80
  37. package/src/linux/gio_mount_points.h +0 -37
  38. package/src/linux/gio_utils.cpp +0 -115
  39. package/src/linux/gio_utils.h +0 -69
  40. package/src/linux/gio_volume_metadata.cpp +0 -81
  41. package/src/linux/gio_volume_metadata.h +0 -20
@@ -95,7 +95,8 @@ async function _getVolumeMetadata(
95
95
  }
96
96
  } catch (err) {
97
97
  debug("[getVolumeMetadata] failed to get mtab info: " + err);
98
- // this may be a GIO mount. Ignore the error and continue.
98
+ // Mtab lookup can fail for transient mounts or race conditions.
99
+ // Ignore and continue with whatever the native call returns.
99
100
  }
100
101
  }
101
102
 
@@ -140,7 +141,7 @@ async function _getVolumeMetadata(
140
141
  remote,
141
142
  }) as VolumeMetadata;
142
143
 
143
- // Backfill if blkid or gio failed us:
144
+ // Backfill if blkid failed us:
144
145
  if (isLinux && isNotBlank(device)) {
145
146
  // Sometimes blkid doesn't have the UUID in cache. Try to get it from
146
147
  // /dev/disk/by-uuid:
@@ -209,7 +210,7 @@ export async function getVolumeMetadataForPathImpl(
209
210
 
210
211
  // Linux/Windows: stat().dev is reliable (no firmlinks). Find the mount point
211
212
  // by comparing device IDs, using path prefix as a tiebreaker for bind mounts
212
- // or GIO mounts that share the same device id.
213
+ // or GVfs/FUSE mounts that share the same device id.
213
214
  const mountPoint = await findMountPointByDeviceId(
214
215
  resolved,
215
216
  resolvedStat,
@@ -221,12 +222,18 @@ export async function getVolumeMetadataForPathImpl(
221
222
  }
222
223
 
223
224
  /**
224
- * Find the mount point for a resolved path using device ID matching.
225
+ * Find the mount point for a resolved path using device ID + path ancestry.
225
226
  * Used on Linux and Windows where stat().dev is reliable (no firmlinks).
226
227
  *
227
- * Compares device IDs of mount points against the target path's device ID,
228
- * using path prefix as a tiebreaker for bind mounts or GIO mounts that share
229
- * the same device id. The longest prefix match wins.
228
+ * Device ID filters out unrelated filesystems. Among same-device mount points,
229
+ * ancestor-path matches (mount point is a parent of `resolved`) are strongly
230
+ * preferred over device-only matches GVfs/FUSE mounts on Linux can share
231
+ * the same device ID across unrelated volumes (e.g. multiple SMB shares
232
+ * under /run/user/.../gvfs/), so device ID alone is ambiguous. The longest
233
+ * ancestor wins.
234
+ *
235
+ * The device-only fallback (`deviceMatches`) exists for bind mounts where the
236
+ * canonical mount point may not be a path ancestor of the target.
230
237
  */
231
238
  export async function findMountPointByDeviceId(
232
239
  resolved: string,
@@ -235,10 +242,12 @@ export async function findMountPointByDeviceId(
235
242
  nativeFn: NativeBindingsFn,
236
243
  ): Promise<string> {
237
244
  const targetDev = resolvedStat.dev;
238
- const mountPoints = await getVolumeMountPointsImpl(
239
- { ...opts, includeSystemVolumes: true },
240
- nativeFn,
241
- );
245
+ const mountPoints =
246
+ opts.mountPoints ??
247
+ (await getVolumeMountPointsImpl(
248
+ { ...opts, includeSystemVolumes: true },
249
+ nativeFn,
250
+ ));
242
251
 
243
252
  const prefixMatches: string[] = [];
244
253
  const deviceMatches: string[] = [];
@@ -259,6 +268,8 @@ export async function findMountPointByDeviceId(
259
268
  }),
260
269
  );
261
270
 
271
+ // Prefer ancestor matches — they're unambiguous. Fall back to device-only
272
+ // matches only when the mount point isn't an ancestor (e.g. bind mounts).
262
273
  const candidates = prefixMatches.length > 0 ? prefixMatches : deviceMatches;
263
274
  if (candidates.length === 0) {
264
275
  throw new Error(
@@ -51,7 +51,7 @@ async function _getVolumeMountPoints(
51
51
  );
52
52
  return points;
53
53
  })()
54
- : getLinuxMountPoints(nativeFn, o));
54
+ : getLinuxMountPoints(o));
55
55
 
56
56
  debug("[getVolumeMountPoints] raw mount points: %o", raw);
57
57
 
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // scripts/setup-native.mjs
4
-
5
- // This script sets up the config.gypi file to include GIO support when available.
6
- // It should be run before building native modules with node-gyp.
7
-
8
- import { execSync } from "node:child_process";
9
- import { writeFileSync } from "node:fs";
10
- import { platform } from "node:os";
11
- import { argv } from "node:process";
12
- import { pathToFileURL } from "node:url";
13
-
14
- function hasGio() {
15
- if (platform() !== "linux") return false;
16
- try {
17
- execSync("pkg-config --exists gio-2.0", { stdio: "ignore" });
18
- return true;
19
- } catch {
20
- return false;
21
- }
22
- }
23
-
24
- export function configure() {
25
- // Create a gyp config file that node-gyp will read
26
- const config = {
27
- variables: {
28
- "enable_gio%": hasGio() ? "true" : "false",
29
- },
30
- };
31
-
32
- const payload = JSON.stringify(config, null, 2);
33
- writeFileSync("config.gypi", payload);
34
- }
35
-
36
- // If the script is run directly, call the configure function
37
- if (import.meta.url === pathToFileURL(argv[1]).href) {
38
- configure();
39
- }
@@ -1,80 +0,0 @@
1
- // src/linux/gio_mount_points.cpp
2
- #ifdef ENABLE_GIO
3
-
4
- #include "gio_mount_points.h"
5
- #include "../common/debug_log.h"
6
- #include "gio_utils.h"
7
- #include <gio/gio.h>
8
- #include <memory>
9
- #include <stdexcept>
10
-
11
- namespace FSMeta {
12
- namespace gio {
13
-
14
- GioMountPointsWorker::GioMountPointsWorker(
15
- const Napi::Promise::Deferred &deferred)
16
- : Napi::AsyncWorker(deferred.Env()), deferred_(deferred) {}
17
-
18
- GioMountPointsWorker::~GioMountPointsWorker() { mountPoints.clear(); }
19
-
20
- void GioMountPointsWorker::Execute() {
21
- try {
22
- DEBUG_LOG("[GioMountPoints] processing mounts");
23
-
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");
40
- }
41
-
42
- return true; // Continue iteration
43
- });
44
-
45
- DEBUG_LOG("[GioMountPoints] found %zu mount points", mountPoints.size());
46
- } catch (const std::exception &e) {
47
- DEBUG_LOG("[GioMountPoints] error: %s", e.what());
48
- SetError(e.what());
49
- }
50
- }
51
-
52
- void GioMountPointsWorker::OnOK() {
53
- const Napi::HandleScope scope(Env());
54
- const Napi::Array result = Napi::Array::New(Env());
55
-
56
- for (size_t i = 0; i < mountPoints.size(); i++) {
57
- const Napi::Object point = Napi::Object::New(Env());
58
- point.Set("mountPoint", mountPoints[i].mountPoint);
59
- point.Set("fstype", mountPoints[i].fstype);
60
- result.Set(i, point);
61
- }
62
-
63
- deferred_.Resolve(result);
64
- }
65
-
66
- void GioMountPointsWorker::OnError(const Napi::Error &error) {
67
- deferred_.Reject(error.Value());
68
- }
69
-
70
- Napi::Value GetMountPoints(Napi::Env env) {
71
- auto deferred = Napi::Promise::Deferred::New(env);
72
- auto *worker = new GioMountPointsWorker(deferred);
73
- worker->Queue();
74
- return deferred.Promise();
75
- }
76
-
77
- } // namespace gio
78
- } // namespace FSMeta
79
-
80
- #endif // ENABLE_GIO
@@ -1,37 +0,0 @@
1
- // src/linux/gio_mount_points.h
2
-
3
- #pragma once
4
-
5
- #ifdef ENABLE_GIO
6
-
7
- #include "../common/volume_mount_points.h"
8
- #include <napi.h>
9
- #include <string>
10
- #include <vector>
11
-
12
- namespace FSMeta {
13
- namespace gio {
14
-
15
- /**
16
- * Get mount points asynchronously using GIO
17
- */
18
- Napi::Value GetMountPoints(Napi::Env env);
19
-
20
- class GioMountPointsWorker : public Napi::AsyncWorker {
21
- public:
22
- explicit GioMountPointsWorker(const Napi::Promise::Deferred &deferred);
23
- ~GioMountPointsWorker() override; // Add override specifier
24
-
25
- void Execute() override;
26
- void OnOK() override;
27
- void OnError(const Napi::Error &error) override;
28
-
29
- private:
30
- std::vector<MountPoint> mountPoints;
31
- Napi::Promise::Deferred deferred_;
32
- };
33
-
34
- } // namespace gio
35
- } // namespace FSMeta
36
-
37
- #endif // ENABLE_GIO
@@ -1,115 +0,0 @@
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)
26
-
27
- #ifdef ENABLE_GIO
28
-
29
- #include "gio_utils.h"
30
- #include "../common/debug_log.h"
31
- #include <gio/gio.h>
32
- #include <gio/gunixmounts.h>
33
- #include <memory>
34
- #include <stdexcept>
35
-
36
- namespace FSMeta {
37
- namespace gio {
38
-
39
- void MountIterator::forEachMount(const MountCallback &callback) {
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);
44
-
45
- if (!unix_mounts) {
46
- DEBUG_LOG("[gio::MountIterator::forEachMount] no mounts found");
47
- return;
48
- }
49
-
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);
58
-
59
- if (!entry) {
60
- DEBUG_LOG("[gio::MountIterator::forEachMount] Skipping null entry");
61
- current = current->next;
62
- continue;
63
- }
64
-
65
- try {
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) {
69
- DEBUG_LOG(
70
- "[gio::MountIterator::forEachMount] Skipping mount with null path");
71
- current = current->next;
72
- continue;
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
-
83
- } catch (const std::exception &e) {
84
- DEBUG_LOG("[gio::MountIterator::forEachMount] Exception during mount "
85
- "processing: %s",
86
- e.what());
87
- // Clean up and re-throw
88
- g_list_free_full(unix_mounts,
89
- reinterpret_cast<GDestroyNotify>(g_unix_mount_free));
90
- throw;
91
- }
92
-
93
- current = current->next;
94
- }
95
-
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
- // 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.
111
-
112
- } // namespace gio
113
- } // namespace FSMeta
114
-
115
- #endif // ENABLE_GIO
@@ -1,69 +0,0 @@
1
- // src/linux/gio_utils.h
2
- #pragma once
3
-
4
- #ifdef ENABLE_GIO
5
-
6
- #include <gio/gio.h>
7
- #include <gio/gunixmounts.h>
8
- #include <napi.h>
9
- #include <string>
10
- #include <vector>
11
-
12
- // Custom deleter for GObject types using g_object_unref
13
- template <typename T> struct GObjectDeleter {
14
- void operator()(T *ptr) const {
15
- if (ptr) {
16
- g_object_unref(ptr);
17
- }
18
- }
19
- };
20
-
21
- // Custom deleter for g_free (used for strings from GIO APIs like
22
- // g_file_get_path)
23
- struct GFreeDeleter {
24
- void operator()(void *ptr) const {
25
- if (ptr) {
26
- g_free(ptr);
27
- }
28
- }
29
- };
30
-
31
- // Smart pointer aliases for RAII management of GIO resources
32
- // These ensure proper cleanup even when exceptions occur
33
- template <typename T> using GObjectPtr = std::unique_ptr<T, GObjectDeleter<T>>;
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.)
43
- using GCharPtr = std::unique_ptr<char, GFreeDeleter>;
44
-
45
- namespace FSMeta {
46
- namespace gio {
47
-
48
- class MountIterator {
49
- public:
50
- // Callback type for mount processing
51
- // Receives GUnixMountEntry which provides thread-safe access to mount data
52
- // Return true to continue iteration, false to stop
53
- using MountCallback = std::function<bool(GUnixMountEntry *)>;
54
-
55
- // Static method to iterate over mounts using thread-safe g_unix_mounts_get()
56
- // This is safe to call from worker threads
57
- static void forEachMount(const MountCallback &callback);
58
-
59
- // NOTE: tryGetMonitor() has been removed because GVolumeMonitor is NOT
60
- // thread-safe. See: https://docs.gtk.org/gio/class.VolumeMonitor.html
61
- };
62
-
63
- // Note: GioResource<T> has been removed in favor of GObjectPtr<T> above,
64
- // which provides equivalent RAII semantics with std::unique_ptr.
65
-
66
- } // namespace gio
67
- } // namespace FSMeta
68
-
69
- #endif // ENABLE_GIO
@@ -1,81 +0,0 @@
1
- // src/linux/gio_volume_metadata.cpp
2
-
3
- #ifdef ENABLE_GIO
4
-
5
- #include "gio_volume_metadata.h"
6
- #include "../common/debug_log.h"
7
- #include "gio_utils.h"
8
- #include <gio/gio.h>
9
- #include <memory>
10
- #include <stdexcept>
11
-
12
- namespace FSMeta {
13
- namespace gio {
14
-
15
- void addMountMetadata(const std::string &mountPoint, VolumeMetadata &metadata) {
16
- DEBUG_LOG("[gio::addMountMetadata] getting mount metadata for %s",
17
- mountPoint.c_str());
18
-
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) {
25
- return true; // Continue iteration
26
- }
27
-
28
- // Found matching mount point
29
- DEBUG_LOG("[gio::addMountMetadata] found matching mount point: %s",
30
- mount_path);
31
- found = true;
32
-
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;
40
- }
41
- }
42
-
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;
49
- }
50
- }
51
-
52
- // NOTE: GVolumeMonitor enrichment has been removed.
53
- //
54
- // According to GIO documentation:
55
- // https://docs.gtk.org/gio/class.VolumeMonitor.html
56
- // "GVolumeMonitor is not thread-default-context aware and so should not
57
- // be used other than from the main thread, with no thread-default-context
58
- // active."
59
- //
60
- // This function is called from Napi::AsyncWorker::Execute() which runs
61
- // on a worker thread. Using GVolumeMonitor here causes race conditions
62
- // leading to GLib-GObject-CRITICAL errors like:
63
- // "g_object_ref: assertion '!object_already_finalized' failed"
64
- //
65
- // The basic metadata (fstype, mountFrom) from g_unix_mounts_get() is
66
- // sufficient and thread-safe. Rich metadata (label, mountName, uri) can
67
- // be obtained from blkid or other thread-safe sources.
68
-
69
- return false; // Stop iteration, we found our mount
70
- });
71
-
72
- if (!found) {
73
- DEBUG_LOG("[gio::addMountMetadata] mount point %s not found",
74
- mountPoint.c_str());
75
- }
76
- }
77
-
78
- } // namespace gio
79
- } // namespace FSMeta
80
-
81
- #endif // ENABLE_GIO
@@ -1,20 +0,0 @@
1
- // src/linux/gio_volume_metadata.h
2
-
3
- #pragma once
4
-
5
- #ifdef ENABLE_GIO
6
-
7
- #include "../common/volume_metadata.h"
8
- #include <string>
9
-
10
- namespace FSMeta {
11
- namespace gio {
12
- /**
13
- * Add metadata from GIO to the volume metadata
14
- */
15
- void addMountMetadata(const std::string &mountPoint, VolumeMetadata &metadata);
16
-
17
- } // namespace gio
18
- } // namespace FSMeta
19
-
20
- #endif // ENABLE_GIO