@photostructure/fs-metadata 1.0.1 → 1.2.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 (47) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/CLAUDE.md +13 -0
  3. package/binding.gyp +1 -0
  4. package/claude.sh +29 -5
  5. package/dist/index.cjs +237 -129
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +55 -3
  8. package/dist/index.d.mts +55 -3
  9. package/dist/index.d.ts +55 -3
  10. package/dist/index.mjs +236 -130
  11. package/dist/index.mjs.map +1 -1
  12. package/doc/SECURITY_AUDIT_2025.md +1 -1
  13. package/doc/SECURITY_AUDIT_2026.md +361 -0
  14. package/doc/TPP-GUIDE.md +144 -0
  15. package/doc/system-volume-detection.md +268 -0
  16. package/package.json +12 -12
  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/src/binding.cpp +11 -0
  26. package/src/common/volume_metadata.h +10 -3
  27. package/src/common/volume_mount_points.h +7 -1
  28. package/src/darwin/da_mutex.h +23 -0
  29. package/src/darwin/get_mount_point.cpp +96 -0
  30. package/src/darwin/get_mount_point.h +13 -0
  31. package/src/darwin/raii_utils.h +39 -0
  32. package/src/darwin/system_volume.h +156 -0
  33. package/src/darwin/volume_metadata.cpp +18 -2
  34. package/src/darwin/volume_mount_points.cpp +46 -14
  35. package/src/index.ts +49 -0
  36. package/src/linux/mtab.ts +6 -0
  37. package/src/mount_point_for_path.ts +54 -0
  38. package/src/options.ts +7 -17
  39. package/src/path.ts +16 -1
  40. package/src/system_volume.ts +5 -9
  41. package/src/test-utils/assert.ts +4 -0
  42. package/src/types/mount_point.ts +28 -1
  43. package/src/types/native_bindings.ts +7 -0
  44. package/src/volume_metadata.ts +117 -2
  45. package/src/windows/system_volume.h +21 -16
  46. package/src/windows/volume_metadata.cpp +13 -7
  47. package/src/windows/volume_mount_points.cpp +11 -7
package/dist/index.mjs CHANGED
@@ -388,7 +388,7 @@ function toError(cause) {
388
388
  }
389
389
 
390
390
  // src/path.ts
391
- import { dirname as dirname2, resolve as resolve2 } from "path";
391
+ import { dirname as dirname2, resolve as resolve2, sep } from "path";
392
392
  function normalizePath(mountPoint) {
393
393
  if (isBlank(mountPoint)) return void 0;
394
394
  if (mountPoint.includes("..")) {
@@ -417,6 +417,11 @@ function isRootDirectory(path2) {
417
417
  const n = normalizePath(path2);
418
418
  return n == null ? false : isWindows ? dirname2(n) === n : n === "/";
419
419
  }
420
+ function isAncestorOrSelf(ancestor, descendant) {
421
+ if (ancestor === descendant) return true;
422
+ const prefix = isRootDirectory(ancestor) ? ancestor : ancestor + sep;
423
+ return descendant.startsWith(prefix);
424
+ }
420
425
 
421
426
  // src/hidden.ts
422
427
  var HiddenSupportByPlatform = {
@@ -581,6 +586,74 @@ async function setHiddenImpl(pathname, hide, method, nativeFn2) {
581
586
  return { pathname: norm, actions };
582
587
  }
583
588
 
589
+ // src/mount_point_for_path.ts
590
+ import { realpath as realpath2 } from "fs/promises";
591
+ import { dirname as dirname5 } from "path";
592
+
593
+ // src/volume_metadata.ts
594
+ import { realpath } from "fs/promises";
595
+ import { dirname as dirname4 } from "path";
596
+
597
+ // src/linux/dev_disk.ts
598
+ import { readdir, readlink } from "fs/promises";
599
+ import { join as join3, resolve as resolve3 } from "path";
600
+ async function getUuidFromDevDisk(devicePath) {
601
+ try {
602
+ const result = await getBasenameLinkedTo(
603
+ "/dev/disk/by-uuid",
604
+ resolve3(devicePath)
605
+ );
606
+ debug("[getUuidFromDevDisk] result: %o", result);
607
+ return result;
608
+ } catch (error) {
609
+ debug("[getUuidFromDevDisk] failed: " + error);
610
+ return;
611
+ }
612
+ }
613
+ async function getLabelFromDevDisk(devicePath) {
614
+ try {
615
+ const result = await getBasenameLinkedTo(
616
+ "/dev/disk/by-label",
617
+ resolve3(devicePath)
618
+ );
619
+ debug("[getLabelFromDevDisk] result: %o", result);
620
+ return result;
621
+ } catch (error) {
622
+ debug("[getLabelFromDevDisk] failed: " + error);
623
+ return;
624
+ }
625
+ }
626
+ async function getBasenameLinkedTo(linkDir, linkPath) {
627
+ for await (const ea of readLinks(linkDir)) {
628
+ if (ea.linkTarget === linkPath) {
629
+ return decodeEscapeSequences(ea.dirent.name);
630
+ }
631
+ }
632
+ return;
633
+ }
634
+ async function* readLinks(directory) {
635
+ for (const dirent of await readdir(directory, { withFileTypes: true })) {
636
+ if (dirent.isSymbolicLink()) {
637
+ try {
638
+ const linkTarget = resolve3(
639
+ directory,
640
+ await readlink(join3(directory, dirent.name))
641
+ );
642
+ yield { dirent, linkTarget };
643
+ } catch {
644
+ }
645
+ }
646
+ }
647
+ }
648
+
649
+ // src/linux/mount_points.ts
650
+ import { readFile } from "fs/promises";
651
+
652
+ // src/mount_point.ts
653
+ function isMountPoint(obj) {
654
+ return isObject(obj) && "mountPoint" in obj && isNotBlank(obj.mountPoint);
655
+ }
656
+
584
657
  // src/options.ts
585
658
  import { availableParallelism as availableParallelism2 } from "os";
586
659
  import { env as env2 } from "process";
@@ -643,23 +716,13 @@ var SystemPathPatternsDefault = [
643
716
  "/mnt/wslg/doc",
644
717
  "/mnt/wslg/versions.txt",
645
718
  "/usr/lib/wsl/drivers",
646
- // macOS system paths:
647
- "/private/var/vm",
648
- // macOS swap
649
- "/System/Volumes/Hardware",
650
- "/System/Volumes/iSCPreboot",
651
- "/System/Volumes/Preboot",
652
- "/System/Volumes/Recovery",
653
- "/System/Volumes/Reserved",
654
- "/System/Volumes/Update",
655
- "/System/Volumes/VM",
656
- "/System/Volumes/xarts",
657
- // macOS per-volume metadata (Spotlight, FSEvents, versioning, Trash):
658
- // https://eclecticlight.co/2021/01/28/spotlight-on-search-how-spotlight-works/
659
- "**/.DocumentRevisions-V100",
660
- "**/.fseventsd",
661
- "**/.Spotlight-V100",
662
- "**/.Trashes"
719
+ // macOS system volumes are detected natively via APFS volume roles
720
+ // (IOKit IOMedia "Role" property) with MNT_SNAPSHOT as a fallback.
721
+ // No path patterns needed. See src/darwin/system_volume.h.
722
+ //
723
+ // /private/var/vm is the macOS swap directory (not a mount point on most
724
+ // systems, but included for completeness if it appears as one).
725
+ "/private/var/vm"
663
726
  ];
664
727
  var SystemFsTypesDefault = [
665
728
  "autofs",
@@ -791,109 +854,6 @@ function optionsWithDefaults(overrides = {}) {
791
854
  };
792
855
  }
793
856
 
794
- // src/string_enum.ts
795
- function stringEnum(...o) {
796
- const set = new Set(o);
797
- const dict = {};
798
- for (const key of o) {
799
- dict[key] = key;
800
- }
801
- return {
802
- ...dict,
803
- values: Object.freeze([...set]),
804
- size: set.size,
805
- get: (s) => s != null && set.has(s) ? s : void 0
806
- };
807
- }
808
-
809
- // src/volume_health_status.ts
810
- var VolumeHealthStatuses = stringEnum(
811
- "healthy",
812
- "timeout",
813
- "inaccessible",
814
- "disconnected",
815
- "unknown"
816
- );
817
- async function directoryStatus(dir, timeoutMs, canReaddirImpl = canReaddir) {
818
- try {
819
- if (await canReaddirImpl(dir, timeoutMs)) {
820
- return { status: VolumeHealthStatuses.healthy };
821
- }
822
- } catch (error) {
823
- debug("[directoryStatus] %s: %s", dir, error);
824
- let status = VolumeHealthStatuses.unknown;
825
- if (error instanceof TimeoutError) {
826
- status = VolumeHealthStatuses.timeout;
827
- } else if (isObject(error) && error instanceof Error && "code" in error) {
828
- if (error.code === "EPERM" || error.code === "EACCES") {
829
- status = VolumeHealthStatuses.inaccessible;
830
- }
831
- }
832
- return { status, error: toError(error) };
833
- }
834
- return { status: VolumeHealthStatuses.unknown };
835
- }
836
-
837
- // src/linux/dev_disk.ts
838
- import { readdir, readlink } from "fs/promises";
839
- import { join as join3, resolve as resolve3 } from "path";
840
- async function getUuidFromDevDisk(devicePath) {
841
- try {
842
- const result = await getBasenameLinkedTo(
843
- "/dev/disk/by-uuid",
844
- resolve3(devicePath)
845
- );
846
- debug("[getUuidFromDevDisk] result: %o", result);
847
- return result;
848
- } catch (error) {
849
- debug("[getUuidFromDevDisk] failed: " + error);
850
- return;
851
- }
852
- }
853
- async function getLabelFromDevDisk(devicePath) {
854
- try {
855
- const result = await getBasenameLinkedTo(
856
- "/dev/disk/by-label",
857
- resolve3(devicePath)
858
- );
859
- debug("[getLabelFromDevDisk] result: %o", result);
860
- return result;
861
- } catch (error) {
862
- debug("[getLabelFromDevDisk] failed: " + error);
863
- return;
864
- }
865
- }
866
- async function getBasenameLinkedTo(linkDir, linkPath) {
867
- for await (const ea of readLinks(linkDir)) {
868
- if (ea.linkTarget === linkPath) {
869
- return decodeEscapeSequences(ea.dirent.name);
870
- }
871
- }
872
- return;
873
- }
874
- async function* readLinks(directory) {
875
- for (const dirent of await readdir(directory, { withFileTypes: true })) {
876
- if (dirent.isSymbolicLink()) {
877
- try {
878
- const linkTarget = resolve3(
879
- directory,
880
- await readlink(join3(directory, dirent.name))
881
- );
882
- yield { dirent, linkTarget };
883
- } catch {
884
- }
885
- }
886
- }
887
- }
888
-
889
- // src/linux/mount_points.ts
890
- import { readFile } from "fs/promises";
891
-
892
- // src/mount_point.ts
893
- function isMountPoint(obj) {
894
- return isObject(obj) && "mountPoint" in obj && isNotBlank(obj.mountPoint);
895
- }
896
-
897
857
  // src/remote_info.ts
898
858
  function isRemoteInfo(obj) {
899
859
  if (!isObject(obj)) return false;
@@ -1118,20 +1078,20 @@ function isSystemVolume(mountPoint, fstype, config = {}) {
1118
1078
  }
1119
1079
  function assignSystemVolume(mp, config) {
1120
1080
  const result = isSystemVolume(mp.mountPoint, mp.fstype, config);
1121
- if (isWindows) {
1122
- mp.isSystemVolume ??= result;
1123
- } else {
1124
- mp.isSystemVolume = result;
1125
- }
1081
+ mp.isSystemVolume = mp.isSystemVolume || result;
1126
1082
  }
1127
1083
 
1128
1084
  // src/linux/mtab.ts
1085
+ function isReadOnlyMount(fs_mntops) {
1086
+ return fs_mntops?.split(",").includes("ro") ?? false;
1087
+ }
1129
1088
  function mountEntryToMountPoint(entry) {
1130
1089
  const mountPoint = normalizePosixPath(entry.fs_file);
1131
1090
  const fstype = toNotBlank(entry.fs_vfstype) ?? toNotBlank(entry.fs_spec);
1132
1091
  return mountPoint == null || fstype == null ? void 0 : {
1133
1092
  mountPoint,
1134
- fstype
1093
+ fstype,
1094
+ isReadOnly: isReadOnlyMount(entry.fs_mntops)
1135
1095
  };
1136
1096
  }
1137
1097
  function mountEntryToPartialVolumeMetadata(entry, options = {}) {
@@ -1141,6 +1101,7 @@ function mountEntryToPartialVolumeMetadata(entry, options = {}) {
1141
1101
  fstype: entry.fs_vfstype,
1142
1102
  mountFrom: entry.fs_spec,
1143
1103
  isSystemVolume: isSystemVolume(entry.fs_file, entry.fs_vfstype, options),
1104
+ isReadOnly: isReadOnlyMount(entry.fs_mntops),
1144
1105
  remote: false,
1145
1106
  // < default to false, but it may be overridden by extractRemoteInfo
1146
1107
  ...extractRemoteInfo(entry.fs_spec, networkFsTypes)
@@ -1274,6 +1235,49 @@ function extractUUID(uuid) {
1274
1235
  return toS(uuid).match(uuidRegex)?.[0];
1275
1236
  }
1276
1237
 
1238
+ // src/string_enum.ts
1239
+ function stringEnum(...o) {
1240
+ const set = new Set(o);
1241
+ const dict = {};
1242
+ for (const key of o) {
1243
+ dict[key] = key;
1244
+ }
1245
+ return {
1246
+ ...dict,
1247
+ values: Object.freeze([...set]),
1248
+ size: set.size,
1249
+ get: (s) => s != null && set.has(s) ? s : void 0
1250
+ };
1251
+ }
1252
+
1253
+ // src/volume_health_status.ts
1254
+ var VolumeHealthStatuses = stringEnum(
1255
+ "healthy",
1256
+ "timeout",
1257
+ "inaccessible",
1258
+ "disconnected",
1259
+ "unknown"
1260
+ );
1261
+ async function directoryStatus(dir, timeoutMs, canReaddirImpl = canReaddir) {
1262
+ try {
1263
+ if (await canReaddirImpl(dir, timeoutMs)) {
1264
+ return { status: VolumeHealthStatuses.healthy };
1265
+ }
1266
+ } catch (error) {
1267
+ debug("[directoryStatus] %s: %s", dir, error);
1268
+ let status = VolumeHealthStatuses.unknown;
1269
+ if (error instanceof TimeoutError) {
1270
+ status = VolumeHealthStatuses.timeout;
1271
+ } else if (isObject(error) && error instanceof Error && "code" in error) {
1272
+ if (error.code === "EPERM" || error.code === "EACCES") {
1273
+ status = VolumeHealthStatuses.inaccessible;
1274
+ }
1275
+ }
1276
+ return { status, error: toError(error) };
1277
+ }
1278
+ return { status: VolumeHealthStatuses.unknown };
1279
+ }
1280
+
1277
1281
  // src/array.ts
1278
1282
  function uniqBy(arr, keyFn) {
1279
1283
  const seen = /* @__PURE__ */ new Set();
@@ -1424,6 +1428,63 @@ async function _getVolumeMetadata(o, nativeFn2) {
1424
1428
  debug("[getVolumeMetadata] final result for %s: %o", o.mountPoint, result);
1425
1429
  return compactValues(result);
1426
1430
  }
1431
+ async function getVolumeMetadataForPathImpl(pathname, opts, nativeFn2) {
1432
+ if (isBlank(pathname)) {
1433
+ throw new TypeError("Invalid pathname: got " + JSON.stringify(pathname));
1434
+ }
1435
+ const resolved = await realpath(pathname);
1436
+ const resolvedStat = await statAsync(resolved);
1437
+ const dir = resolvedStat.isDirectory() ? resolved : dirname4(resolved);
1438
+ if (isMacOS) {
1439
+ const probe = await getVolumeMetadataImpl(
1440
+ { ...opts, mountPoint: dir },
1441
+ nativeFn2
1442
+ );
1443
+ const canonicalMountPoint = isNotBlank(probe.mountName) ? probe.mountName : dir;
1444
+ if (canonicalMountPoint === dir) return probe;
1445
+ return getVolumeMetadataImpl(
1446
+ { ...opts, mountPoint: canonicalMountPoint },
1447
+ nativeFn2
1448
+ );
1449
+ }
1450
+ const mountPoint = await findMountPointByDeviceId(
1451
+ resolved,
1452
+ resolvedStat,
1453
+ opts,
1454
+ nativeFn2
1455
+ );
1456
+ return getVolumeMetadataImpl({ ...opts, mountPoint }, nativeFn2);
1457
+ }
1458
+ async function findMountPointByDeviceId(resolved, resolvedStat, opts, nativeFn2) {
1459
+ const targetDev = resolvedStat.dev;
1460
+ const mountPoints = await getVolumeMountPointsImpl(
1461
+ { ...opts, includeSystemVolumes: true },
1462
+ nativeFn2
1463
+ );
1464
+ const prefixMatches = [];
1465
+ const deviceMatches = [];
1466
+ await Promise.all(
1467
+ mountPoints.map(async ({ mountPoint }) => {
1468
+ try {
1469
+ const mpDev = (await statAsync(mountPoint)).dev;
1470
+ if (mpDev !== targetDev) return;
1471
+ if (isAncestorOrSelf(mountPoint, resolved)) {
1472
+ prefixMatches.push(mountPoint);
1473
+ } else {
1474
+ deviceMatches.push(mountPoint);
1475
+ }
1476
+ } catch {
1477
+ }
1478
+ })
1479
+ );
1480
+ const candidates = prefixMatches.length > 0 ? prefixMatches : deviceMatches;
1481
+ if (candidates.length === 0) {
1482
+ throw new Error(
1483
+ "No mount point found for path: " + JSON.stringify(resolved)
1484
+ );
1485
+ }
1486
+ return candidates.reduce((a, b) => a.length >= b.length ? a : b);
1487
+ }
1427
1488
  async function getAllVolumeMetadataImpl(opts, nativeFn2) {
1428
1489
  const o = optionsWithDefaults(opts);
1429
1490
  debug("[getAllVolumeMetadata] starting with options: %o", o);
@@ -1475,15 +1536,44 @@ async function getAllVolumeMetadataImpl(opts, nativeFn2) {
1475
1536
  );
1476
1537
  }
1477
1538
 
1539
+ // src/mount_point_for_path.ts
1540
+ async function getMountPointForPathImpl(pathname, opts, nativeFn2) {
1541
+ if (isBlank(pathname)) {
1542
+ throw new TypeError("Invalid pathname: got " + JSON.stringify(pathname));
1543
+ }
1544
+ const resolved = await realpath2(pathname);
1545
+ const resolvedStat = await statAsync(resolved);
1546
+ const dir = resolvedStat.isDirectory() ? resolved : dirname5(resolved);
1547
+ if (isMacOS) {
1548
+ const native = await nativeFn2();
1549
+ if (native.getMountPoint) {
1550
+ debug("[getMountPointForPath] using native getMountPoint for %s", dir);
1551
+ const p = native.getMountPoint(dir);
1552
+ const mountPoint = await withTimeout({
1553
+ desc: "getMountPoint()",
1554
+ timeoutMs: opts.timeoutMs,
1555
+ promise: p
1556
+ });
1557
+ if (isNotBlank(mountPoint)) {
1558
+ debug("[getMountPointForPath] resolved to %s", mountPoint);
1559
+ return mountPoint;
1560
+ }
1561
+ }
1562
+ throw new Error("getMountPoint native function unavailable");
1563
+ }
1564
+ debug("[getMountPointForPath] using device matching for %s", resolved);
1565
+ return findMountPointByDeviceId(resolved, resolvedStat, opts, nativeFn2);
1566
+ }
1567
+
1478
1568
  // src/index.ts
1479
1569
  var nativeFn = defer2(async () => {
1480
1570
  const start = Date.now();
1481
1571
  try {
1482
- const dirname4 = _dirname();
1483
- const dir = await findAncestorDir(dirname4, "binding.gyp");
1572
+ const dirname6 = _dirname();
1573
+ const dir = await findAncestorDir(dirname6, "binding.gyp");
1484
1574
  if (dir == null) {
1485
1575
  throw new Error(
1486
- "Could not find bindings.gyp in any ancestor directory of " + dirname4
1576
+ "Could not find bindings.gyp in any ancestor directory of " + dirname6
1487
1577
  );
1488
1578
  }
1489
1579
  const bindings = NodeGypBuild(dir);
@@ -1506,6 +1596,20 @@ function getVolumeMetadata(mountPoint, opts) {
1506
1596
  nativeFn
1507
1597
  );
1508
1598
  }
1599
+ function getVolumeMetadataForPath(pathname, opts) {
1600
+ return getVolumeMetadataForPathImpl(
1601
+ pathname,
1602
+ optionsWithDefaults(opts),
1603
+ nativeFn
1604
+ );
1605
+ }
1606
+ function getMountPointForPath(pathname, opts) {
1607
+ return getMountPointForPathImpl(
1608
+ pathname,
1609
+ optionsWithDefaults(opts),
1610
+ nativeFn
1611
+ );
1612
+ }
1509
1613
  function getAllVolumeMetadata(opts) {
1510
1614
  return getAllVolumeMetadataImpl(optionsWithDefaults(opts), nativeFn);
1511
1615
  }
@@ -1532,8 +1636,10 @@ export {
1532
1636
  VolumeHealthStatuses,
1533
1637
  getAllVolumeMetadata,
1534
1638
  getHiddenMetadata,
1639
+ getMountPointForPath,
1535
1640
  getTimeoutMsDefault,
1536
1641
  getVolumeMetadata,
1642
+ getVolumeMetadataForPath,
1537
1643
  getVolumeMountPoints,
1538
1644
  isHidden,
1539
1645
  isHiddenRecursive,