@photostructure/fs-metadata 0.4.0 → 0.5.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 (179) hide show
  1. package/C++_REVIEW_TODO.md +291 -0
  2. package/CHANGELOG.md +29 -1
  3. package/CLAUDE.md +169 -0
  4. package/CONTRIBUTING.md +25 -0
  5. package/coverage/base.css +224 -0
  6. package/coverage/block-navigation.js +87 -0
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/index.html +131 -0
  9. package/coverage/lcov-report/base.css +224 -0
  10. package/coverage/lcov-report/block-navigation.js +87 -0
  11. package/coverage/lcov-report/favicon.png +0 -0
  12. package/coverage/lcov-report/index.html +131 -0
  13. package/coverage/lcov-report/prettify.css +1 -0
  14. package/coverage/lcov-report/prettify.js +2 -0
  15. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  16. package/coverage/lcov-report/sorter.js +196 -0
  17. package/coverage/lcov-report/src/array.ts.html +217 -0
  18. package/coverage/lcov-report/src/async.ts.html +547 -0
  19. package/coverage/lcov-report/src/debuglog.ts.html +187 -0
  20. package/coverage/lcov-report/src/defer.ts.html +175 -0
  21. package/coverage/lcov-report/src/dirname.ts.html +124 -0
  22. package/coverage/lcov-report/src/error.ts.html +322 -0
  23. package/coverage/lcov-report/src/fs.ts.html +316 -0
  24. package/coverage/lcov-report/src/glob.ts.html +472 -0
  25. package/coverage/lcov-report/src/hidden.ts.html +724 -0
  26. package/coverage/lcov-report/src/index.html +521 -0
  27. package/coverage/lcov-report/src/index.ts.html +676 -0
  28. package/coverage/lcov-report/src/linux/dev_disk.ts.html +316 -0
  29. package/coverage/lcov-report/src/linux/index.html +146 -0
  30. package/coverage/lcov-report/src/linux/mount_points.ts.html +364 -0
  31. package/coverage/lcov-report/src/linux/mtab.ts.html +493 -0
  32. package/coverage/lcov-report/src/mount_point.ts.html +106 -0
  33. package/coverage/lcov-report/src/number.ts.html +148 -0
  34. package/coverage/lcov-report/src/object.ts.html +265 -0
  35. package/coverage/lcov-report/src/options.ts.html +475 -0
  36. package/coverage/lcov-report/src/path.ts.html +268 -0
  37. package/coverage/lcov-report/src/platform.ts.html +112 -0
  38. package/coverage/lcov-report/src/random.ts.html +205 -0
  39. package/coverage/lcov-report/src/remote_info.ts.html +553 -0
  40. package/coverage/lcov-report/src/stack_path.ts.html +298 -0
  41. package/coverage/lcov-report/src/string.ts.html +382 -0
  42. package/coverage/lcov-report/src/string_enum.ts.html +208 -0
  43. package/coverage/lcov-report/src/system_volume.ts.html +301 -0
  44. package/coverage/lcov-report/src/unc.ts.html +274 -0
  45. package/coverage/lcov-report/src/units.ts.html +274 -0
  46. package/coverage/lcov-report/src/uuid.ts.html +157 -0
  47. package/coverage/lcov-report/src/volume_health_status.ts.html +259 -0
  48. package/coverage/lcov-report/src/volume_metadata.ts.html +787 -0
  49. package/coverage/lcov-report/src/volume_mount_points.ts.html +388 -0
  50. package/coverage/lcov.info +3581 -0
  51. package/coverage/prettify.css +1 -0
  52. package/coverage/prettify.js +2 -0
  53. package/coverage/sort-arrow-sprite.png +0 -0
  54. package/coverage/sorter.js +196 -0
  55. package/coverage/src/array.ts.html +217 -0
  56. package/coverage/src/async.ts.html +547 -0
  57. package/coverage/src/debuglog.ts.html +187 -0
  58. package/coverage/src/defer.ts.html +175 -0
  59. package/coverage/src/dirname.ts.html +124 -0
  60. package/coverage/src/error.ts.html +322 -0
  61. package/coverage/src/fs.ts.html +316 -0
  62. package/coverage/src/glob.ts.html +472 -0
  63. package/coverage/src/hidden.ts.html +724 -0
  64. package/coverage/src/index.html +521 -0
  65. package/coverage/src/index.ts.html +676 -0
  66. package/coverage/src/linux/dev_disk.ts.html +316 -0
  67. package/coverage/src/linux/index.html +146 -0
  68. package/coverage/src/linux/mount_points.ts.html +364 -0
  69. package/coverage/src/linux/mtab.ts.html +493 -0
  70. package/coverage/src/mount_point.ts.html +106 -0
  71. package/coverage/src/number.ts.html +148 -0
  72. package/coverage/src/object.ts.html +265 -0
  73. package/coverage/src/options.ts.html +475 -0
  74. package/coverage/src/path.ts.html +268 -0
  75. package/coverage/src/platform.ts.html +112 -0
  76. package/coverage/src/random.ts.html +205 -0
  77. package/coverage/src/remote_info.ts.html +553 -0
  78. package/coverage/src/stack_path.ts.html +298 -0
  79. package/coverage/src/string.ts.html +382 -0
  80. package/coverage/src/string_enum.ts.html +208 -0
  81. package/coverage/src/system_volume.ts.html +301 -0
  82. package/coverage/src/unc.ts.html +274 -0
  83. package/coverage/src/units.ts.html +274 -0
  84. package/coverage/src/uuid.ts.html +157 -0
  85. package/coverage/src/volume_health_status.ts.html +259 -0
  86. package/coverage/src/volume_metadata.ts.html +787 -0
  87. package/coverage/src/volume_mount_points.ts.html +388 -0
  88. package/jest.config.cjs +67 -6
  89. package/package.json +51 -41
  90. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  91. package/scripts/check-memory.mjs +243 -0
  92. package/scripts/clang-tidy.mjs +73 -0
  93. package/scripts/is-platform.mjs +12 -0
  94. package/scripts/post-build.mjs +21 -0
  95. package/scripts/run-asan.sh +92 -0
  96. package/scripts/valgrind-test.mjs +83 -0
  97. package/scripts/valgrind.sh +70 -0
  98. package/src/async.ts +3 -3
  99. package/src/binding.cpp +3 -3
  100. package/src/error.ts +3 -3
  101. package/src/fs.ts +1 -1
  102. package/src/glob.ts +2 -2
  103. package/src/hidden.ts +6 -6
  104. package/src/index.ts +19 -23
  105. package/src/linux/blkid_cache.cpp +15 -12
  106. package/src/linux/dev_disk.ts +2 -2
  107. package/src/linux/gio_mount_points.cpp +7 -7
  108. package/src/linux/gio_utils.cpp +19 -8
  109. package/src/linux/gio_volume_metadata.cpp +15 -15
  110. package/src/linux/mount_points.ts +9 -9
  111. package/src/linux/mtab.ts +7 -7
  112. package/src/linux/volume_metadata.cpp +6 -1
  113. package/src/object.ts +1 -1
  114. package/src/options.ts +3 -3
  115. package/src/path.ts +2 -2
  116. package/src/remote_info.ts +5 -5
  117. package/src/system_volume.ts +8 -8
  118. package/src/test-utils/assert.ts +2 -2
  119. package/src/test-utils/debuglog-child.ts +1 -3
  120. package/src/test-utils/debuglog-enabled-child.ts +10 -0
  121. package/src/test-utils/hidden-tests.ts +1 -1
  122. package/src/test-utils/platform.ts +3 -3
  123. package/src/types/native_bindings.ts +3 -3
  124. package/src/types/volume_metadata.ts +2 -2
  125. package/src/unc.ts +2 -2
  126. package/src/uuid.ts +1 -1
  127. package/src/volume_health_status.ts +6 -6
  128. package/src/volume_metadata.ts +20 -23
  129. package/src/volume_mount_points.ts +12 -17
  130. package/src/windows/drive_status.h +30 -13
  131. package/src/windows/hidden.cpp +12 -0
  132. package/src/windows/volume_metadata.cpp +17 -7
  133. package/tsup.config.ts +8 -2
  134. package/dist/index.cjs +0 -1439
  135. package/dist/index.cjs.map +0 -1
  136. package/dist/index.mjs +0 -1396
  137. package/dist/index.mjs.map +0 -1
  138. package/dist/types/array.d.ts +0 -25
  139. package/dist/types/async.d.ts +0 -42
  140. package/dist/types/debuglog.d.ts +0 -3
  141. package/dist/types/defer.d.ts +0 -10
  142. package/dist/types/dirname.d.ts +0 -1
  143. package/dist/types/error.d.ts +0 -17
  144. package/dist/types/fs.d.ts +0 -22
  145. package/dist/types/glob.d.ts +0 -17
  146. package/dist/types/hidden.d.ts +0 -29
  147. package/dist/types/index.d.ts +0 -91
  148. package/dist/types/linux/dev_disk.d.ts +0 -13
  149. package/dist/types/linux/mount_points.d.ts +0 -6
  150. package/dist/types/linux/mtab.d.ts +0 -47
  151. package/dist/types/mount_point.d.ts +0 -2
  152. package/dist/types/number.d.ts +0 -3
  153. package/dist/types/object.d.ts +0 -18
  154. package/dist/types/options.d.ts +0 -33
  155. package/dist/types/path.d.ts +0 -17
  156. package/dist/types/platform.d.ts +0 -4
  157. package/dist/types/random.d.ts +0 -12
  158. package/dist/types/remote_info.d.ts +0 -6
  159. package/dist/types/stack_path.d.ts +0 -2
  160. package/dist/types/string.d.ts +0 -37
  161. package/dist/types/string_enum.d.ts +0 -19
  162. package/dist/types/system_volume.d.ts +0 -14
  163. package/dist/types/types/hidden_metadata.d.ts +0 -32
  164. package/dist/types/types/mount_point.d.ts +0 -46
  165. package/dist/types/types/native_bindings.d.ts +0 -51
  166. package/dist/types/types/options.d.ts +0 -47
  167. package/dist/types/types/remote_info.d.ts +0 -33
  168. package/dist/types/types/volume_metadata.d.ts +0 -46
  169. package/dist/types/unc.d.ts +0 -11
  170. package/dist/types/units.d.ts +0 -38
  171. package/dist/types/uuid.d.ts +0 -16
  172. package/dist/types/volume_health_status.d.ts +0 -24
  173. package/dist/types/volume_metadata.d.ts +0 -8
  174. package/dist/types/volume_mount_points.d.ts +0 -6
  175. package/jest.config.base.cjs +0 -63
  176. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  177. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  178. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  179. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Cross-platform memory checking script
5
+ * Runs JavaScript memory tests on all platforms
6
+ * Runs valgrind and ASAN tests only on Linux
7
+ */
8
+
9
+ import { execSync } from "child_process";
10
+ import { writeFileSync } from "fs";
11
+ import os from "os";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+
18
+ // Colors for output
19
+ const colors = {
20
+ RED: "\x1b[31m",
21
+ GREEN: "\x1b[32m",
22
+ YELLOW: "\x1b[33m",
23
+ BLUE: "\x1b[34m",
24
+ RESET: "\x1b[0m",
25
+ };
26
+
27
+ // Use colors only if not on Windows
28
+ const isWindows = os.platform() === "win32";
29
+ const color = (colorCode, text) =>
30
+ isWindows ? text : `${colorCode}${text}${colors.RESET}`;
31
+
32
+ console.log(color(colors.BLUE, "=== Memory Leak Detection Suite ==="));
33
+
34
+ let exitCode = 0;
35
+
36
+ // 1. Run JavaScript memory tests (all platforms)
37
+ console.log(color(colors.YELLOW, "\nRunning JavaScript memory tests..."));
38
+ try {
39
+ execSync("npm run test:memory", { stdio: "inherit" });
40
+ console.log(color(colors.GREEN, "✓ JavaScript memory tests passed"));
41
+ } catch {
42
+ console.log(color(colors.RED, "✗ JavaScript memory tests failed"));
43
+ exitCode = 1;
44
+ }
45
+
46
+ // 2. Run valgrind if available and on Linux
47
+ if (os.platform() === "linux") {
48
+ try {
49
+ execSync("which valgrind", { stdio: "ignore" });
50
+ console.log(color(colors.YELLOW, "\nRunning valgrind memory analysis..."));
51
+ try {
52
+ const valgrindScript = path.join(__dirname, "valgrind.sh");
53
+ execSync(valgrindScript, { stdio: "inherit" });
54
+ console.log(color(colors.GREEN, "✓ Valgrind tests passed"));
55
+ } catch {
56
+ console.log(color(colors.RED, "✗ Valgrind tests failed"));
57
+ exitCode = 1;
58
+ }
59
+ } catch {
60
+ console.log(color(colors.YELLOW, "\nValgrind not available. Skipping."));
61
+ }
62
+ } else {
63
+ console.log(
64
+ color(colors.YELLOW, "\nValgrind tests only run on Linux. Skipping."),
65
+ );
66
+ }
67
+
68
+ // 3. Run Address Sanitizer if requested (Linux only for now)
69
+ if (process.env.ENABLE_ASAN) {
70
+ if (os.platform() === "linux") {
71
+ console.log(color(colors.YELLOW, "\nBuilding with AddressSanitizer..."));
72
+ try {
73
+ // Check if clang is available
74
+ execSync("which clang", { stdio: "ignore" });
75
+
76
+ const env = {
77
+ ...process.env,
78
+ CC: "clang",
79
+ CXX: "clang++",
80
+ CFLAGS: "-fsanitize=address -fno-omit-frame-pointer -g -O1",
81
+ CXXFLAGS: "-fsanitize=address -fno-omit-frame-pointer -g -O1",
82
+ LDFLAGS: "-fsanitize=address",
83
+ ASAN_OPTIONS: "detect_leaks=1:halt_on_error=0:print_stats=1",
84
+ LSAN_OPTIONS: `suppressions=${path.join(process.cwd(), ".lsan-suppressions.txt")}:print_suppressions=0`,
85
+ };
86
+
87
+ // Find ASan runtime library using clang
88
+ try {
89
+ const asanLib = execSync(
90
+ "clang -print-file-name=libclang_rt.asan-x86_64.so",
91
+ { encoding: "utf8" },
92
+ ).trim();
93
+ if (asanLib && !asanLib.includes("not found")) {
94
+ env.LD_PRELOAD = asanLib;
95
+ console.log(color(colors.BLUE, `Using ASan library: ${asanLib}`));
96
+ }
97
+ } catch {
98
+ // Try common paths as fallback
99
+ const asanLibPaths = [
100
+ "/usr/lib/x86_64-linux-gnu/libasan.so.8",
101
+ "/usr/lib/x86_64-linux-gnu/libasan.so.6",
102
+ "/usr/lib64/libasan.so.8",
103
+ "/usr/lib64/libasan.so.6",
104
+ ];
105
+
106
+ for (const libPath of asanLibPaths) {
107
+ try {
108
+ execSync(`test -f ${libPath}`, { stdio: "ignore" });
109
+ env.LD_PRELOAD = libPath;
110
+ console.log(color(colors.BLUE, `Using ASan library: ${libPath}`));
111
+ break;
112
+ } catch {
113
+ // Try next path
114
+ }
115
+ }
116
+ }
117
+
118
+ execSync("npm run node-gyp-rebuild", { stdio: "inherit", env });
119
+
120
+ console.log(
121
+ color(colors.YELLOW, "Running tests with AddressSanitizer..."),
122
+ );
123
+
124
+ // Capture ASAN output for analysis
125
+ let asanOutput = "";
126
+ try {
127
+ asanOutput = execSync("npm test -- --no-coverage 2>&1", {
128
+ env,
129
+ }).toString();
130
+ console.log(asanOutput);
131
+ } catch (error) {
132
+ asanOutput = error.stdout ? error.stdout.toString() : "";
133
+ asanOutput += error.stderr ? error.stderr.toString() : "";
134
+ console.log(asanOutput);
135
+ }
136
+
137
+ // Save full output to file
138
+ const outputFile = path.join(process.cwd(), "asan-output.log");
139
+ writeFileSync(outputFile, asanOutput);
140
+ console.log(
141
+ color(colors.BLUE, `\nFull ASAN output saved to: ${outputFile}`),
142
+ );
143
+
144
+ // Check for ASAN errors in our code (not V8/Node internals)
145
+ const lines = asanOutput.split("\n");
146
+ const hasOurErrors = lines.some(
147
+ (line) =>
148
+ (line.includes("ERROR: AddressSanitizer") ||
149
+ line.includes("ERROR: LeakSanitizer")) &&
150
+ (line.includes("fs_metadata.node") || line.includes("/src/")),
151
+ );
152
+
153
+ const hasOurLeaks = lines.some(
154
+ (line) =>
155
+ (line.includes("Direct leak") || line.includes("Indirect leak")) &&
156
+ lines
157
+ .slice(
158
+ Math.max(0, lines.indexOf(line) - 5),
159
+ lines.indexOf(line) + 10,
160
+ )
161
+ .some(
162
+ (context) =>
163
+ context.includes("fs_metadata.node") ||
164
+ context.includes("/src/"),
165
+ ),
166
+ );
167
+
168
+ // Count V8/Node internal leaks for information
169
+ const internalLeaks = (asanOutput.match(/leak.*\/usr\/bin\/node/g) || [])
170
+ .length;
171
+
172
+ if (hasOurErrors || hasOurLeaks) {
173
+ console.log(
174
+ color(
175
+ colors.RED,
176
+ "\n✗ AddressSanitizer found issues in fs-metadata code:",
177
+ ),
178
+ );
179
+
180
+ // Extract relevant error lines
181
+ const relevantErrors = lines.filter((line, idx) => {
182
+ if (line.includes("ERROR:") || line.includes("leak")) {
183
+ // Check context around this line for our code
184
+ const context = lines
185
+ .slice(Math.max(0, idx - 5), idx + 10)
186
+ .join("\n");
187
+ return (
188
+ context.includes("fs_metadata.node") || context.includes("/src/")
189
+ );
190
+ }
191
+ return false;
192
+ });
193
+
194
+ relevantErrors.forEach((line) => console.log(color(colors.RED, line)));
195
+ exitCode = 1;
196
+ } else {
197
+ console.log(
198
+ color(
199
+ colors.GREEN,
200
+ "✓ AddressSanitizer tests passed (no issues in fs-metadata code)",
201
+ ),
202
+ );
203
+ if (internalLeaks > 0) {
204
+ console.log(
205
+ color(
206
+ colors.YELLOW,
207
+ ` Note: ${internalLeaks} V8/Node.js internal leaks detected (suppressed)`,
208
+ ),
209
+ );
210
+ }
211
+ }
212
+ } catch (error) {
213
+ if (error && error.code === 1) {
214
+ console.log(
215
+ color(
216
+ colors.YELLOW,
217
+ "clang not found. Skipping AddressSanitizer tests.",
218
+ ),
219
+ );
220
+ } else {
221
+ console.log(color(colors.RED, "✗ AddressSanitizer tests failed"));
222
+ exitCode = 1;
223
+ }
224
+ }
225
+ } else {
226
+ console.log(
227
+ color(
228
+ colors.YELLOW,
229
+ "\nAddressSanitizer tests are currently only supported on Linux. Skipping.",
230
+ ),
231
+ );
232
+ }
233
+ }
234
+
235
+ if (exitCode === 0) {
236
+ console.log(
237
+ color(colors.GREEN, "\n=== All memory tests completed successfully! ==="),
238
+ );
239
+ } else {
240
+ console.log(color(colors.RED, "\n=== Some memory tests failed ==="));
241
+ }
242
+
243
+ process.exit(exitCode);
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import { execSync, spawn } from "child_process";
3
+ import { platform } from "os";
4
+
5
+ // Skip clang-tidy on Windows
6
+ if (platform() === "win32") {
7
+ console.log("Skipping clang-tidy on Windows platform");
8
+ process.exit(0);
9
+ }
10
+
11
+ // Check for required tools
12
+ function checkCommand(command, installHint) {
13
+ try {
14
+ execSync(`which ${command}`, { stdio: "ignore" });
15
+ return true;
16
+ } catch {
17
+ console.error(`Error: '${command}' not found in PATH.`);
18
+ console.error(`To install: ${installHint}`);
19
+ return false;
20
+ }
21
+ }
22
+
23
+ const isMacOS = platform() === "darwin";
24
+ const isLinux = platform() === "linux";
25
+
26
+ let hasAllTools = true;
27
+
28
+ if (
29
+ !checkCommand(
30
+ "bear",
31
+ isLinux
32
+ ? "sudo apt-get install bear"
33
+ : isMacOS
34
+ ? "brew install bear"
35
+ : "see https://github.com/rizsotto/Bear",
36
+ )
37
+ ) {
38
+ hasAllTools = false;
39
+ }
40
+
41
+ if (
42
+ !checkCommand(
43
+ "clang-tidy",
44
+ isLinux
45
+ ? "sudo apt-get install clang-tidy"
46
+ : isMacOS
47
+ ? "brew install llvm && alias clang-tidy=$(brew --prefix llvm)/bin/clang-tidy"
48
+ : "see https://clang.llvm.org/extra/clang-tidy/",
49
+ )
50
+ ) {
51
+ hasAllTools = false;
52
+ }
53
+
54
+ if (!hasAllTools) {
55
+ process.exit(1);
56
+ }
57
+
58
+ // Run the clang-tidy command on Unix platforms
59
+ const command = `npm run configure && bear -- npm run node-gyp-rebuild && find src -name '*.cpp' -o -name '*.h' | grep -E '\\.(cpp|h)$' | grep -v -E '(windows|darwin)/' | xargs clang-tidy`;
60
+
61
+ const shell = spawn("sh", ["-c", command], {
62
+ stdio: "inherit",
63
+ shell: false,
64
+ });
65
+
66
+ shell.on("exit", (code) => {
67
+ process.exit(code || 0);
68
+ });
69
+
70
+ shell.on("error", (err) => {
71
+ console.error("Failed to run clang-tidy:", err);
72
+ process.exit(1);
73
+ });
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { platform } from "os";
3
+
4
+ const targetPlatform = process.argv[2];
5
+ if (!targetPlatform) {
6
+ console.error("Usage: is-platform.mjs <platform>");
7
+ console.error("Example: is-platform.mjs win32");
8
+ process.exit(2);
9
+ }
10
+
11
+ // Exit with 0 if current platform matches target, 1 otherwise
12
+ process.exit(platform() === targetPlatform ? 0 : 1);
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { copyFile } from "fs/promises";
4
+ import { dirname, join } from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const distDir = join(__dirname, "..", "dist");
9
+
10
+ // Copy .d.ts to .d.cts for CommonJS type safety
11
+ async function createCjsTypes() {
12
+ try {
13
+ await copyFile(join(distDir, "index.d.ts"), join(distDir, "index.d.cts"));
14
+ console.log("Created index.d.cts for CommonJS type safety");
15
+ } catch (error) {
16
+ console.error("Error creating .d.cts file:", error);
17
+ process.exit(1);
18
+ }
19
+ }
20
+
21
+ createCjsTypes();
@@ -0,0 +1,92 @@
1
+ #!/bin/bash
2
+ # AddressSanitizer test runner for @photostructure/fs-metadata
3
+
4
+ set -euo pipefail
5
+
6
+ # Check if we're on Linux
7
+ if [[ "$OSTYPE" != "linux-gnu"* ]]; then
8
+ echo "AddressSanitizer tests are only supported on Linux"
9
+ exit 0
10
+ fi
11
+
12
+ # Check for clang
13
+ if ! command -v clang &> /dev/null; then
14
+ echo "Error: clang is required for AddressSanitizer tests"
15
+ echo "Install with: sudo apt-get install clang"
16
+ exit 1
17
+ fi
18
+
19
+ # Colors for output
20
+ RED='\033[0;31m'
21
+ GREEN='\033[0;32m'
22
+ YELLOW='\033[1;33m'
23
+ NC='\033[0m' # No Color
24
+
25
+ echo -e "${YELLOW}Running AddressSanitizer tests...${NC}"
26
+
27
+ # Clean previous builds
28
+ echo "Cleaning previous builds..."
29
+ rm -rf build/
30
+ rm -f asan-output.log
31
+
32
+ # Set up ASan environment
33
+ export CC=clang
34
+ export CXX=clang++
35
+ export CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1"
36
+ export CXXFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1"
37
+ export LDFLAGS="-fsanitize=address"
38
+
39
+ # Find ASan runtime library
40
+ ASAN_LIB=$(clang -print-file-name=libclang_rt.asan-x86_64.so 2>/dev/null || true)
41
+ if [[ -f "$ASAN_LIB" ]]; then
42
+ export LD_PRELOAD="$ASAN_LIB"
43
+ echo "Using ASan library: $ASAN_LIB"
44
+ fi
45
+
46
+ # Set ASan options
47
+ export ASAN_OPTIONS="detect_leaks=1:check_initialization_order=1:strict_init_order=1:print_stats=1:print_module_map=1"
48
+ export LSAN_OPTIONS="suppressions=$(pwd)/.lsan-suppressions.txt:print_suppressions=0"
49
+
50
+ # Set Node.js options to increase memory for ASan overhead
51
+ export NODE_OPTIONS="--max-old-space-size=8192"
52
+
53
+ # Build the native module
54
+ echo "Building with AddressSanitizer..."
55
+ npm run node-gyp-rebuild
56
+
57
+ # Run tests and capture output
58
+ echo "Running tests..."
59
+ npm run test -- --no-coverage 2>&1 | tee asan-output.log
60
+
61
+ # Analyze results
62
+ echo -e "\n${YELLOW}Analyzing AddressSanitizer output...${NC}"
63
+
64
+ # Check for errors in our code (excluding V8/Node internals)
65
+ if grep -E "ERROR: AddressSanitizer" asan-output.log | grep -v "/usr/bin/node" > /dev/null 2>&1; then
66
+ echo -e "${RED}AddressSanitizer detected errors in fs-metadata code!${NC}"
67
+ grep -A5 -B5 "ERROR: AddressSanitizer" asan-output.log | grep -v "/usr/bin/node" || true
68
+ exit 1
69
+ fi
70
+
71
+ # Check for leaks in our code
72
+ if grep -E "Direct leak.*photostructure|Indirect leak.*photostructure" asan-output.log > /dev/null 2>&1; then
73
+ echo -e "${RED}Memory leaks detected in fs-metadata code!${NC}"
74
+ grep -A5 -B5 "leak.*photostructure" asan-output.log || true
75
+ exit 1
76
+ fi
77
+
78
+ # Check for any leaks from our native module
79
+ if grep -E "leak.*build/Release/fs_metadata.node" asan-output.log > /dev/null 2>&1; then
80
+ echo -e "${RED}Memory leaks detected in native module!${NC}"
81
+ grep -A5 -B5 "leak.*fs_metadata.node" asan-output.log || true
82
+ exit 1
83
+ fi
84
+
85
+ # Report on V8/Node.js internal leaks (informational only)
86
+ INTERNAL_LEAKS=$(grep -c "leak.*node" asan-output.log 2>/dev/null || echo "0")
87
+ if [[ "$INTERNAL_LEAKS" -gt 0 ]]; then
88
+ echo -e "${YELLOW}Note: Found $INTERNAL_LEAKS V8/Node.js internal leaks (suppressed)${NC}"
89
+ fi
90
+
91
+ echo -e "${GREEN}✓ AddressSanitizer tests passed! No memory errors in fs-metadata code.${NC}"
92
+ echo "Full output saved to: asan-output.log"
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Valgrind test runner for @photostructure/fs-metadata
5
+ *
6
+ * This script exercises the native module's core functionality to detect
7
+ * memory leaks. It's designed to be run under valgrind via npm run test:valgrind
8
+ *
9
+ * The test performs multiple iterations of each operation to help detect
10
+ * memory leaks that might only appear after repeated use.
11
+ */
12
+
13
+ import {
14
+ getAllVolumeMetadata,
15
+ getVolumeMetadata,
16
+ getVolumeMountPoints,
17
+ isHidden,
18
+ } from "../dist/index.js";
19
+
20
+ async function runTests() {
21
+ console.log("Starting valgrind memory leak tests...");
22
+
23
+ // Test 1: Exercise getVolumeMountPoints multiple times
24
+ console.log("Test 1: getVolumeMountPoints");
25
+ for (let i = 0; i < 10; i++) {
26
+ await getVolumeMountPoints();
27
+ if (i % 5 === 0) console.log(` Iteration ${i + 1}/10`);
28
+ }
29
+
30
+ // Test 2: Exercise getVolumeMetadata with valid and invalid paths
31
+ console.log("Test 2: getVolumeMetadata");
32
+ for (let i = 0; i < 10; i++) {
33
+ try {
34
+ await getVolumeMetadata("/");
35
+ } catch {
36
+ // Expected for some platforms
37
+ }
38
+
39
+ try {
40
+ await getVolumeMetadata("/nonexistent-path-" + i);
41
+ } catch {
42
+ // Expected - testing error paths
43
+ }
44
+
45
+ if (i % 5 === 0) console.log(` Iteration ${i + 1}/10`);
46
+ }
47
+
48
+ // Test 3: Exercise getAllVolumeMetadata
49
+ console.log("Test 3: getAllVolumeMetadata");
50
+ try {
51
+ await getAllVolumeMetadata();
52
+ console.log(" Completed successfully");
53
+ } catch (e) {
54
+ console.log(" Completed with expected error:", e.message);
55
+ }
56
+
57
+ // Test 4: Exercise hidden file operations
58
+ console.log("Test 4: isHidden");
59
+ const testPaths = ["/tmp", "/var", "/nonexistent"];
60
+ for (let i = 0; i < 10; i++) {
61
+ for (const path of testPaths) {
62
+ try {
63
+ await isHidden(path);
64
+ } catch {
65
+ // Expected for some paths
66
+ }
67
+ }
68
+ if (i % 5 === 0) console.log(` Iteration ${i + 1}/10`);
69
+ }
70
+
71
+ console.log("Valgrind tests completed");
72
+ }
73
+
74
+ // Run tests and exit
75
+ runTests()
76
+ .then(() => {
77
+ console.log("All tests completed successfully");
78
+ process.exit(0);
79
+ })
80
+ .catch((err) => {
81
+ console.error("Test error:", err);
82
+ process.exit(1);
83
+ });
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+
3
+ # Valgrind memory leak detection script for CI/CD
4
+ set -e
5
+
6
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
7
+ ROOT_DIR="$(dirname "$SCRIPT_DIR")"
8
+
9
+ # Colors for output
10
+ RED='\033[0;31m'
11
+ GREEN='\033[0;32m'
12
+ YELLOW='\033[1;33m'
13
+ NC='\033[0m' # No Color
14
+
15
+ # Check if valgrind is available
16
+ if ! command -v valgrind &> /dev/null; then
17
+ echo -e "${YELLOW}Warning: valgrind not found. Skipping memory leak tests.${NC}"
18
+ exit 0
19
+ fi
20
+
21
+ # Only run on Linux
22
+ if [[ "$OSTYPE" != "linux-gnu"* ]]; then
23
+ echo -e "${YELLOW}Valgrind tests only run on Linux. Skipping.${NC}"
24
+ exit 0
25
+ fi
26
+
27
+ echo -e "${GREEN}Running valgrind memory leak detection...${NC}"
28
+
29
+ # Path to the dedicated valgrind test script
30
+ VALGRIND_TEST="$SCRIPT_DIR/valgrind-test.mjs"
31
+
32
+ # Ensure the test script exists
33
+ if [ ! -f "$VALGRIND_TEST" ]; then
34
+ echo -e "${RED}Error: Valgrind test script not found at $VALGRIND_TEST${NC}"
35
+ exit 1
36
+ fi
37
+
38
+ # Use the committed suppressions file
39
+ SUPP_FILE="$ROOT_DIR/.valgrind.supp"
40
+
41
+ # Ensure suppressions file exists
42
+ if [ ! -f "$SUPP_FILE" ]; then
43
+ echo -e "${RED}Error: Valgrind suppressions file not found at $SUPP_FILE${NC}"
44
+ exit 1
45
+ fi
46
+
47
+ # Run valgrind with appropriate options
48
+ VALGRIND_OPTS="--leak-check=full --show-leak-kinds=definite,indirect,possible --track-origins=yes --suppressions=$SUPP_FILE"
49
+
50
+ echo "Running valgrind tests..."
51
+ if valgrind $VALGRIND_OPTS node "$VALGRIND_TEST" 2>&1 | tee "$ROOT_DIR/valgrind.log"; then
52
+ # Check the log for actual leaks
53
+ if grep -q "definitely lost: 0 bytes in 0 blocks" "$ROOT_DIR/valgrind.log" && \
54
+ grep -q "indirectly lost: 0 bytes in 0 blocks" "$ROOT_DIR/valgrind.log"; then
55
+ echo -e "${GREEN}✓ No memory leaks detected${NC}"
56
+ RESULT=0
57
+ else
58
+ echo -e "${RED}✗ Memory leaks detected${NC}"
59
+ grep -A 5 "LEAK SUMMARY" "$ROOT_DIR/valgrind.log"
60
+ RESULT=1
61
+ fi
62
+ else
63
+ echo -e "${RED}✗ Valgrind error${NC}"
64
+ RESULT=1
65
+ fi
66
+
67
+ # Cleanup log file only (keep the committed suppression file)
68
+ rm -f "$ROOT_DIR/valgrind.log"
69
+
70
+ exit $RESULT
package/src/async.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { availableParallelism } from "node:os";
2
2
  import { env } from "node:process";
3
- import { gt0, isNumber } from "./number.js";
4
- import { isBlank } from "./string.js";
5
- import { DayMs } from "./units.js";
3
+ import { gt0, isNumber } from "./number";
4
+ import { isBlank } from "./string";
5
+ import { DayMs } from "./units";
6
6
 
7
7
  /**
8
8
  * An error that is thrown when a promise does not resolve within the specified
package/src/binding.cpp CHANGED
@@ -20,7 +20,7 @@
20
20
  namespace {
21
21
 
22
22
  Napi::Value SetDebugLogging(const Napi::CallbackInfo &info) {
23
- Napi::Env env = info.Env();
23
+ const Napi::Env env = info.Env();
24
24
 
25
25
  if (info.Length() < 1 || !info[0].IsBoolean()) {
26
26
  throw Napi::TypeError::New(env, "Boolean argument expected");
@@ -31,7 +31,7 @@ Napi::Value SetDebugLogging(const Napi::CallbackInfo &info) {
31
31
  }
32
32
 
33
33
  Napi::Value SetDebugPrefix(const Napi::CallbackInfo &info) {
34
- Napi::Env env = info.Env();
34
+ const Napi::Env env = info.Env();
35
35
 
36
36
  if (info.Length() < 1 || !info[0].IsString()) {
37
37
  throw Napi::TypeError::New(env, "String argument expected");
@@ -44,7 +44,7 @@ Napi::Value SetDebugPrefix(const Napi::CallbackInfo &info) {
44
44
 
45
45
  #ifdef ENABLE_GIO
46
46
  Napi::Value GetGioMountPoints(const Napi::CallbackInfo &info) {
47
- Napi::Env env = info.Env();
47
+ const Napi::Env env = info.Env();
48
48
  return FSMeta::gio::GetMountPoints(env);
49
49
  }
50
50
  #endif
package/src/error.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/error.ts
2
2
 
3
- import { isNumber } from "./number.js";
4
- import { compactValues, map, omit } from "./object.js";
5
- import { isBlank, isNotBlank } from "./string.js";
3
+ import { isNumber } from "./number";
4
+ import { compactValues, map, omit } from "./object";
5
+ import { isBlank, isNotBlank } from "./string";
6
6
 
7
7
  function toMessage(context: string, cause: unknown): string {
8
8
  const causeStr =
package/src/fs.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { type PathLike, type StatOptions, Stats, statSync } from "node:fs";
4
4
  import { opendir, stat } from "node:fs/promises";
5
5
  import { join, resolve } from "node:path";
6
- import { withTimeout } from "./async.js";
6
+ import { withTimeout } from "./async";
7
7
 
8
8
  /**
9
9
  * Wrapping node:fs/promises.stat() so we can mock it in tests.
package/src/glob.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/glob.ts
2
2
 
3
- import { isWindows } from "./platform.js";
4
- import { isNotBlank } from "./string.js";
3
+ import { isWindows } from "./platform";
4
+ import { isNotBlank } from "./string";
5
5
 
6
6
  const cache = new Map<string, RegExp>();
7
7
 
package/src/hidden.ts CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  import { rename } from "node:fs/promises";
4
4
  import { basename, dirname, join } from "node:path";
5
- import { WrappedError } from "./error.js";
6
- import { canStatAsync, statAsync } from "./fs.js";
7
- import { isRootDirectory, normalizePath } from "./path.js";
8
- import { isWindows } from "./platform.js";
9
- import type { HiddenMetadata } from "./types/hidden_metadata.js";
10
- import type { NativeBindingsFn } from "./types/native_bindings.js";
5
+ import { WrappedError } from "./error";
6
+ import { canStatAsync, statAsync } from "./fs";
7
+ import { isRootDirectory, normalizePath } from "./path";
8
+ import { isWindows } from "./platform";
9
+ import type { HiddenMetadata } from "./types/hidden_metadata";
10
+ import type { NativeBindingsFn } from "./types/native_bindings";
11
11
 
12
12
  const HiddenSupportByPlatform: Partial<
13
13
  Record<NodeJS.Platform, Pick<HiddenMetadata, "supported">>