@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
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ /**
4
+ * Standalone memory test runner for use in CI environments
5
+ *
6
+ * This script runs memory tests without Jest to avoid worker process issues
7
+ * particularly on Windows CI. It uses the same test logic as the Jest tests
8
+ * but with a simpler execution model.
9
+ */
10
+
11
+ import { MemoryTestResult, runAllMemoryTests } from "./memory-test-core";
12
+
13
+ // ANSI color codes for output (disable on Windows for better compatibility)
14
+ const isWindows = process.platform === "win32";
15
+ const colors = {
16
+ RED: isWindows ? "" : "\x1b[31m",
17
+ GREEN: isWindows ? "" : "\x1b[32m",
18
+ YELLOW: isWindows ? "" : "\x1b[33m",
19
+ BLUE: isWindows ? "" : "\x1b[34m",
20
+ RESET: isWindows ? "" : "\x1b[0m",
21
+ };
22
+
23
+ function formatMemory(bytes: number): string {
24
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
25
+ }
26
+
27
+ function printResult(result: MemoryTestResult): void {
28
+ const statusSymbol = result.passed ? "✓" : "✗";
29
+ const statusColor = result.passed ? colors.GREEN : colors.RED;
30
+
31
+ console.log(
32
+ `\n${statusColor}${statusSymbol} ${result.testName}${colors.RESET}`,
33
+ );
34
+ console.log(` Initial memory: ${formatMemory(result.initialMemory)}`);
35
+ console.log(` Final memory: ${formatMemory(result.finalMemory)}`);
36
+ console.log(` Memory increase: ${formatMemory(result.memoryIncrease)}`);
37
+ console.log(` Memory slope: ${result.slope.toFixed(6)}`);
38
+
39
+ if (result.errorMessage) {
40
+ console.log(` ${colors.RED}Error: ${result.errorMessage}${colors.RESET}`);
41
+ }
42
+ }
43
+
44
+ async function main(): Promise<void> {
45
+ console.log(
46
+ `${colors.BLUE}=== Standalone Memory Test Runner ===${colors.RESET}`,
47
+ );
48
+ console.log(`Platform: ${process.platform}`);
49
+ console.log(`Node version: ${process.version}`);
50
+ console.log(`Process architecture: ${process.arch}`);
51
+
52
+ // Check if garbage collection is exposed
53
+ if (!global.gc) {
54
+ console.error(
55
+ `${colors.RED}Error: Garbage collection is not exposed.${colors.RESET}`,
56
+ );
57
+ console.error(
58
+ "Please run with: NODE_OPTIONS='--expose-gc' tsx scripts/memory-test-runner.ts",
59
+ );
60
+ process.exit(1);
61
+ }
62
+
63
+ console.log("\nRunning memory tests...");
64
+
65
+ try {
66
+ const startTime = Date.now();
67
+ const results = await runAllMemoryTests();
68
+ const duration = Date.now() - startTime;
69
+
70
+ // Print individual results
71
+ for (const result of results) {
72
+ printResult(result);
73
+ }
74
+
75
+ // Summary
76
+ const passed = results.filter((r) => r.passed).length;
77
+ const failed = results.filter((r) => !r.passed).length;
78
+ const total = results.length;
79
+
80
+ console.log(`\n${colors.BLUE}=== Test Summary ===${colors.RESET}`);
81
+ console.log(`Total tests: ${total}`);
82
+ console.log(`${colors.GREEN}Passed: ${passed}${colors.RESET}`);
83
+ if (failed > 0) {
84
+ console.log(`${colors.RED}Failed: ${failed}${colors.RESET}`);
85
+ }
86
+ console.log(`Duration: ${(duration / 1000).toFixed(2)}s`);
87
+
88
+ if (failed > 0) {
89
+ console.log(`\n${colors.RED}Memory tests failed!${colors.RESET}`);
90
+ process.exit(1);
91
+ } else {
92
+ console.log(`\n${colors.GREEN}All memory tests passed!${colors.RESET}`);
93
+ process.exit(0);
94
+ }
95
+ } catch (error) {
96
+ console.error(
97
+ `\n${colors.RED}Fatal error running memory tests:${colors.RESET}`,
98
+ );
99
+ console.error(error);
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ // Run the tests
105
+ main().catch((error) => {
106
+ console.error(`${colors.RED}Unhandled error:${colors.RESET}`, error);
107
+ process.exit(1);
108
+ });
@@ -5,7 +5,7 @@ import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { env, platform } from "node:process";
7
7
  import { normalizePath } from "../path";
8
- import { isMacOS, isWindows } from "../platform";
8
+ import { isAlpine, isARM64, isMacOS, isWindows } from "../platform";
9
9
  import { toNotBlank } from "../string";
10
10
 
11
11
  /**
@@ -45,3 +45,48 @@ export function tmpDirNotHidden() {
45
45
  mkdirSync(dir, { recursive: true });
46
46
  return dir;
47
47
  }
48
+
49
+ /**
50
+ * Skip timing-sensitive tests on Alpine ARM64 due to emulation timing issues
51
+ */
52
+ export const itSkipAlpineARM64 = isAlpine() && isARM64 ? it.skip : it;
53
+
54
+ /**
55
+ * Skip timing-sensitive describe blocks on Alpine ARM64 due to emulation timing issues
56
+ */
57
+ export const describeSkipAlpineARM64 =
58
+ isAlpine() && isARM64 ? describe.skip : describe;
59
+
60
+ /**
61
+ * Skip tests on ARM64 CI environments due to various issues:
62
+ * - Alpine ARM64: emulation timing issues
63
+ * - Windows ARM64: Jest worker process failures
64
+ */
65
+ export const describeSkipARM64CI =
66
+ isARM64 && env["CI"] ? describe.skip : describe;
67
+
68
+ export const itSkipARM64CI = isARM64 && env["CI"] ? it.skip : it;
69
+
70
+ /**
71
+ * Skip tests on Windows CI environments due to Jest worker process failures
72
+ * These tests pass locally but fail in GitHub Actions with:
73
+ * "Jest worker encountered 4 child process exceptions, exceeding retry limit"
74
+ */
75
+ export const describeSkipWindowsCI =
76
+ isWindows && env["CI"] ? describe.skip : describe;
77
+
78
+ export const itSkipWindowsCI = isWindows && env["CI"] ? it.skip : it;
79
+
80
+ /**
81
+ * Platform-specific tests that are stable in CI environments
82
+ * (skips on Windows CI due to Jest worker process issues)
83
+ * @param supported The platforms to run tests on
84
+ * @returns jest.Describe function that runs on specified platforms but skips on Windows CI
85
+ */
86
+ export function describePlatformStable(...supported: NodeJS.Platform[]) {
87
+ // Skip on Windows CI due to Jest worker process failures
88
+ if (isWindows && env["CI"]) {
89
+ return describe.skip;
90
+ }
91
+ return describePlatform(...supported);
92
+ }
@@ -3,18 +3,101 @@
3
3
  /* eslint-disable @typescript-eslint/no-require-imports */
4
4
  /* eslint-disable no-undef */
5
5
 
6
- const { parentPort, workerData } = require('node:worker_threads');
7
- const path = require('node:path');
6
+ const { parentPort, workerData } = require("node:worker_threads");
7
+ const path = require("node:path");
8
+
9
+ // Windows ARM64 Jest worker workaround
10
+ if (
11
+ process.platform === "win32" &&
12
+ process.arch === "arm64" &&
13
+ process.env.CI
14
+ ) {
15
+ console.error("[Worker] Windows ARM64 detected in CI, applying workarounds");
16
+ // Ensure we're using the correct module paths
17
+ if (!global.__dirname && typeof __dirname === "undefined") {
18
+ console.error("[Worker] __dirname is undefined, using workaround");
19
+ }
20
+ }
8
21
 
9
22
  // Use eval with the worker data to create the module functions
10
23
  const createModule = () => {
11
- const nodeGypBuild = require('node-gyp-build');
12
- const binding = nodeGypBuild(path.join(__dirname, '../..'));
13
-
24
+ const nodeGypBuild = require("node-gyp-build");
25
+
26
+ // Debug logging for CI
27
+ if (process.env.CI || process.env.DEBUG_WORKER) {
28
+ console.error("[Worker] Environment:");
29
+ console.error(" - Platform:", process.platform);
30
+ console.error(" - Architecture:", process.arch);
31
+ console.error(" - Node version:", process.version);
32
+ console.error(" - Current directory:", process.cwd());
33
+ console.error(" - __dirname:", __dirname);
34
+ console.error(" - Worker ID:", require("node:worker_threads").threadId);
35
+ console.error(
36
+ " - Is main thread:",
37
+ require("node:worker_threads").isMainThread,
38
+ );
39
+ }
40
+
41
+ // Try multiple paths to find the native module
42
+ let binding;
43
+ const possiblePaths = [
44
+ path.join(__dirname, "../.."), // Original path
45
+ process.cwd(), // Current working directory
46
+ path.resolve(__dirname, "../.."), // Absolute resolved path
47
+ path.join(process.cwd(), "prebuilds"), // Direct prebuilds path
48
+ ];
49
+
50
+ // Add Windows-specific paths if on Windows
51
+ if (process.platform === "win32") {
52
+ // Try normalized Windows paths
53
+ possiblePaths.push(
54
+ path.win32.resolve(__dirname, "../.."),
55
+ path.win32.join(process.cwd()),
56
+ );
57
+ }
58
+
59
+ let lastError;
60
+ for (const tryPath of possiblePaths) {
61
+ try {
62
+ if (process.env.CI || process.env.DEBUG_WORKER) {
63
+ console.error("[Worker] Trying path:", tryPath);
64
+ // Also check if prebuilds directory exists
65
+ const fs = require("fs");
66
+ const prebuildsPath = path.join(tryPath, "prebuilds");
67
+ if (fs.existsSync(prebuildsPath)) {
68
+ console.error("[Worker] Prebuilds found at:", prebuildsPath);
69
+ const files = fs.readdirSync(prebuildsPath);
70
+ console.error("[Worker] Prebuild directories:", files);
71
+ }
72
+ }
73
+ binding = nodeGypBuild(tryPath);
74
+ if (process.env.CI || process.env.DEBUG_WORKER) {
75
+ console.error("[Worker] Success! Loaded from:", tryPath);
76
+ console.error("[Worker] Binding functions:", Object.keys(binding));
77
+ }
78
+ break;
79
+ } catch (err) {
80
+ lastError = err;
81
+ if (process.env.CI || process.env.DEBUG_WORKER) {
82
+ console.error("[Worker] Failed to load from", tryPath);
83
+ console.error("[Worker] Error:", err.message);
84
+ if (err.stack && process.env.DEBUG_WORKER) {
85
+ console.error("[Worker] Stack:", err.stack);
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ if (!binding) {
92
+ const errorMsg = `Failed to load native module from any path. Tried: ${possiblePaths.join(", ")}. Last error: ${lastError?.message || "unknown"}`;
93
+ console.error("[Worker] FATAL:", errorMsg);
94
+ throw new Error(errorMsg);
95
+ }
96
+
14
97
  // Platform detection
15
98
  const platform = process.platform;
16
- const isLinux = platform === 'linux';
17
-
99
+ const isLinux = platform === "linux";
100
+
18
101
  // For Linux, we need a simplified implementation since the main one is complex
19
102
  if (isLinux) {
20
103
  return {
@@ -22,10 +105,10 @@ const createModule = () => {
22
105
  // Return the same mount points as the main thread would
23
106
  // This is a simplified version for testing
24
107
  return [
25
- { mountPoint: '/', fstype: 'ext4', isSystemVolume: true },
26
- { mountPoint: '/boot', fstype: 'ext4', isSystemVolume: true },
27
- { mountPoint: '/boot/efi', fstype: 'vfat', isSystemVolume: true },
28
- { mountPoint: '/home', fstype: 'ext4', isSystemVolume: false }
108
+ { mountPoint: "/", fstype: "ext4", isSystemVolume: true },
109
+ { mountPoint: "/boot", fstype: "ext4", isSystemVolume: true },
110
+ { mountPoint: "/boot/efi", fstype: "vfat", isSystemVolume: true },
111
+ { mountPoint: "/home", fstype: "ext4", isSystemVolume: false },
29
112
  ];
30
113
  },
31
114
  getVolumeMetadata: async (mountPoint, options) => {
@@ -33,15 +116,15 @@ const createModule = () => {
33
116
  },
34
117
  isHidden: async (filePath) => {
35
118
  const basename = path.basename(filePath);
36
- return basename.startsWith('.');
119
+ return basename.startsWith(".");
37
120
  },
38
121
  setHidden: async (/* filePath, hidden */) => {
39
122
  // Linux doesn't support hidden attribute
40
123
  return;
41
- }
124
+ },
42
125
  };
43
126
  }
44
-
127
+
45
128
  // For Windows and macOS, use the native binding directly
46
129
  return {
47
130
  getVolumeMountPoints: async () => {
@@ -51,38 +134,86 @@ const createModule = () => {
51
134
  return binding.getVolumeMetadata({ mountPoint, ...options });
52
135
  },
53
136
  isHidden: binding.isHidden,
54
- setHidden: binding.setHidden
137
+ setHidden: binding.setHidden,
55
138
  };
56
139
  };
57
140
 
58
- const fsMetadata = createModule();
141
+ let fsMetadata;
142
+ try {
143
+ fsMetadata = createModule();
144
+ } catch (error) {
145
+ console.error("[Worker] Failed to create module:", error.message);
146
+ console.error("[Worker] Stack:", error.stack);
147
+ // Send error back to parent
148
+ parentPort.postMessage({
149
+ success: false,
150
+ error: `Module initialization failed: ${error.message}`,
151
+ stack: error.stack,
152
+ platform: process.platform,
153
+ arch: process.arch,
154
+ task: "module_init",
155
+ });
156
+ parentPort.close();
157
+ process.exit(1);
158
+ }
59
159
 
60
160
  async function runWorkerTask() {
61
161
  try {
62
162
  const { task, ...params } = workerData;
163
+
164
+ if (process.env.CI || process.env.DEBUG_WORKER) {
165
+ console.error("[Worker] Running task:", task, "with params:", params);
166
+ }
167
+
63
168
  let result;
64
-
169
+
65
170
  switch (task) {
66
- case 'getVolumeMountPoints':
171
+ case "getVolumeMountPoints":
67
172
  result = await fsMetadata.getVolumeMountPoints();
68
173
  break;
69
- case 'getVolumeMetadata':
70
- result = await fsMetadata.getVolumeMetadata(params.mountPoint, params.options);
174
+ case "getVolumeMetadata":
175
+ result = await fsMetadata.getVolumeMetadata(
176
+ params.mountPoint,
177
+ params.options,
178
+ );
71
179
  break;
72
- case 'isHidden':
180
+ case "isHidden":
73
181
  result = await fsMetadata.isHidden(params.path);
74
182
  break;
75
- case 'setHidden':
183
+ case "setHidden":
76
184
  result = await fsMetadata.setHidden(params.path, params.hidden);
77
185
  break;
78
186
  default:
79
- throw new Error('Unknown task: ' + task);
187
+ throw new Error("Unknown task: " + task);
80
188
  }
81
-
189
+
190
+ if (process.env.CI || process.env.DEBUG_WORKER) {
191
+ console.error("[Worker] Task completed successfully");
192
+ }
193
+
82
194
  parentPort.postMessage({ success: true, result });
83
195
  } catch (error) {
84
- parentPort.postMessage({ success: false, error: error.message });
196
+ if (process.env.CI || process.env.DEBUG_WORKER) {
197
+ console.error("[Worker] Task failed:", error.message);
198
+ console.error("[Worker] Error stack:", error.stack);
199
+ }
200
+
201
+ // Include more error details
202
+ const errorInfo = {
203
+ success: false,
204
+ error: error.message,
205
+ stack: process.env.CI ? error.stack : undefined,
206
+ platform: process.platform,
207
+ arch: process.arch,
208
+ task: workerData.task,
209
+ };
210
+
211
+ parentPort.postMessage(errorInfo);
85
212
  }
213
+
214
+ // Close the parent port to signal we're done
215
+ // This allows the worker to exit naturally
216
+ parentPort.close();
86
217
  }
87
218
 
88
- runWorkerTask();
219
+ runWorkerTask();
@@ -59,6 +59,6 @@ export interface NativeBindings {
59
59
  export type GetVolumeMetadataOptions = {
60
60
  mountPoint: string;
61
61
  device?: string;
62
- } & Partial<Pick<Options, "timeoutMs">>;
62
+ } & Partial<Pick<Options, "timeoutMs" | "skipNetworkVolumes">>;
63
63
 
64
64
  export type NativeBindingsFn = () => NativeBindings | Promise<NativeBindings>;
@@ -51,4 +51,10 @@ export interface Options {
51
51
  * Windows and false elsewhere.
52
52
  */
53
53
  includeSystemVolumes: boolean;
54
+
55
+ /**
56
+ * Skip detailed info for network volumes to avoid blocking.
57
+ * Defaults to false.
58
+ */
59
+ skipNetworkVolumes: boolean;
54
60
  }