@photostructure/fs-metadata 0.6.1 → 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.
- 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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom install script that handles Windows architecture defines
|
|
5
|
+
* when node-gyp-build needs to compile from source
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { spawn } = require("child_process");
|
|
9
|
+
const { platform, arch } = require("os");
|
|
10
|
+
|
|
11
|
+
// If in CI and on Windows, set architecture defines
|
|
12
|
+
if (process.env.CI && platform() === "win32") {
|
|
13
|
+
const currentArch = arch();
|
|
14
|
+
|
|
15
|
+
// Set architecture-specific defines for Windows
|
|
16
|
+
if (currentArch === "x64") {
|
|
17
|
+
process.env.CL = "/D_M_X64 /D_WIN64 /D_AMD64_";
|
|
18
|
+
} else if (currentArch === "arm64") {
|
|
19
|
+
process.env.CL = "/D_M_ARM64 /D_WIN64";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`Windows CI detected: arch=${currentArch}, CL=${process.env.CL}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Run node-gyp-build
|
|
26
|
+
const child = spawn("npx", ["node-gyp-build"], {
|
|
27
|
+
stdio: "inherit",
|
|
28
|
+
shell: true,
|
|
29
|
+
env: process.env,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
child.on("error", (error) => {
|
|
33
|
+
console.error("Failed to run node-gyp-build:", error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
child.on("exit", (code) => {
|
|
38
|
+
if (code !== 0) {
|
|
39
|
+
console.error(`node-gyp-build exited with code ${code}`);
|
|
40
|
+
process.exit(code || 1);
|
|
41
|
+
}
|
|
42
|
+
});
|
package/scripts/is-platform.mjs
CHANGED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# macOS AddressSanitizer test script
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
# Colors for output
|
|
7
|
+
RED='\033[0;31m'
|
|
8
|
+
GREEN='\033[0;32m'
|
|
9
|
+
YELLOW='\033[1;33m'
|
|
10
|
+
NC='\033[0m' # No Color
|
|
11
|
+
|
|
12
|
+
echo -e "${YELLOW}=== macOS AddressSanitizer Memory Test ===${NC}"
|
|
13
|
+
|
|
14
|
+
# Check if we're on macOS
|
|
15
|
+
if [[ "$(uname)" != "Darwin" ]]; then
|
|
16
|
+
echo -e "${YELLOW}Not on macOS. Skipping macOS-specific memory tests.${NC}"
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Clean and rebuild with AddressSanitizer
|
|
21
|
+
echo -e "${YELLOW}Cleaning previous builds...${NC}"
|
|
22
|
+
npm run clean:native
|
|
23
|
+
|
|
24
|
+
# Configure build with ASan flags
|
|
25
|
+
echo -e "${YELLOW}Building with AddressSanitizer enabled...${NC}"
|
|
26
|
+
export CFLAGS="-fsanitize=address -g -O1 -fno-omit-frame-pointer"
|
|
27
|
+
export CXXFLAGS="-fsanitize=address -g -O1 -fno-omit-frame-pointer"
|
|
28
|
+
export LDFLAGS="-fsanitize=address"
|
|
29
|
+
|
|
30
|
+
# Set ASan options
|
|
31
|
+
export ASAN_OPTIONS="detect_leaks=1:check_initialization_order=1:strict_init_order=1:print_stats=1:halt_on_error=0"
|
|
32
|
+
export MallocScribble=1
|
|
33
|
+
export MallocGuardEdges=1
|
|
34
|
+
|
|
35
|
+
# Find and set the ASan library path for macOS
|
|
36
|
+
# First try to find the most recent version
|
|
37
|
+
ASAN_LIB=$(find /Library/Developer/CommandLineTools/usr/lib/clang/*/lib/darwin -name "libclang_rt.asan_osx_dynamic.dylib" 2>/dev/null | sort -V | tail -1)
|
|
38
|
+
if [[ -z "$ASAN_LIB" ]]; then
|
|
39
|
+
# Try alternative location
|
|
40
|
+
ASAN_LIB=$(find /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/*/lib/darwin -name "libclang_rt.asan_osx_dynamic.dylib" 2>/dev/null | sort -V | tail -1)
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [[ -n "$ASAN_LIB" ]]; then
|
|
44
|
+
export DYLD_INSERT_LIBRARIES="$ASAN_LIB"
|
|
45
|
+
echo -e "${GREEN}Using ASan library: $ASAN_LIB${NC}"
|
|
46
|
+
else
|
|
47
|
+
echo -e "${RED}Warning: Could not find ASan library. Tests may not run properly.${NC}"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Build the native module
|
|
51
|
+
npm run node-gyp-rebuild
|
|
52
|
+
|
|
53
|
+
# Run tests with ASan
|
|
54
|
+
echo -e "${YELLOW}Running tests with AddressSanitizer...${NC}"
|
|
55
|
+
|
|
56
|
+
# Note: On macOS with SIP enabled, DYLD_INSERT_LIBRARIES is stripped from
|
|
57
|
+
# child processes. Jest uses worker processes, so we need to run tests
|
|
58
|
+
# in a single process to ensure ASAN works correctly.
|
|
59
|
+
|
|
60
|
+
# Run the test and capture output
|
|
61
|
+
TEST_OUTPUT=$(npm test -- --runInBand 2>&1)
|
|
62
|
+
TEST_EXIT_CODE=$?
|
|
63
|
+
|
|
64
|
+
if [[ $TEST_EXIT_CODE -eq 0 ]]; then
|
|
65
|
+
echo -e "${GREEN}✓ Tests passed with AddressSanitizer${NC}"
|
|
66
|
+
else
|
|
67
|
+
# Check if the failure is due to SIP interceptor issues
|
|
68
|
+
if echo "$TEST_OUTPUT" | grep -q "interceptors not installed"; then
|
|
69
|
+
echo -e "${YELLOW}⚠ Tests completed but AddressSanitizer interceptors not installed${NC}"
|
|
70
|
+
echo -e "${YELLOW} This is due to macOS System Integrity Protection (SIP) stripping${NC}"
|
|
71
|
+
echo -e "${YELLOW} DYLD_INSERT_LIBRARIES from child processes. This is expected behavior.${NC}"
|
|
72
|
+
echo -e "${YELLOW} To run ASAN tests properly, you may need to disable SIP temporarily.${NC}"
|
|
73
|
+
# Don't treat this as a failure
|
|
74
|
+
else
|
|
75
|
+
echo -e "${RED}✗ Tests failed with AddressSanitizer${NC}"
|
|
76
|
+
echo "$TEST_OUTPUT"
|
|
77
|
+
# This is a real failure, exit with error
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Run memory leak check using leaks tool
|
|
83
|
+
echo -e "${YELLOW}Running macOS leaks tool...${NC}"
|
|
84
|
+
if command -v leaks >/dev/null 2>&1; then
|
|
85
|
+
# Get the project root directory
|
|
86
|
+
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
87
|
+
|
|
88
|
+
# Check if the native module exists
|
|
89
|
+
if [ ! -f "${PROJECT_ROOT}/build/Release/fs_metadata.node" ]; then
|
|
90
|
+
echo -e "${RED}Native module not found. Skipping leaks test.${NC}"
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Create a simple test script
|
|
95
|
+
cat > /tmp/test-leaks.js << EOF
|
|
96
|
+
const fs = require('fs');
|
|
97
|
+
const binding = require('${PROJECT_ROOT}/build/Release/fs_metadata.node');
|
|
98
|
+
|
|
99
|
+
async function testVolumeMountPoints() {
|
|
100
|
+
for (let i = 0; i < 100; i++) {
|
|
101
|
+
await binding.getVolumeMountPoints();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function testVolumeMetadata() {
|
|
106
|
+
const mountPoints = await binding.getVolumeMountPoints();
|
|
107
|
+
if (mountPoints.length > 0) {
|
|
108
|
+
for (let i = 0; i < 10; i++) {
|
|
109
|
+
await binding.getVolumeMetadata({ mountPoint: mountPoints[0].mountPoint });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function runTests() {
|
|
115
|
+
await testVolumeMountPoints();
|
|
116
|
+
await testVolumeMetadata();
|
|
117
|
+
|
|
118
|
+
// Force garbage collection if available
|
|
119
|
+
if (global.gc) {
|
|
120
|
+
global.gc();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
runTests().then(() => {
|
|
125
|
+
console.log('Tests completed');
|
|
126
|
+
// Give time for cleanup
|
|
127
|
+
setTimeout(() => process.exit(0), 1000);
|
|
128
|
+
}).catch(err => {
|
|
129
|
+
console.error('Test failed:', err);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
132
|
+
EOF
|
|
133
|
+
|
|
134
|
+
# Run with leaks detection
|
|
135
|
+
echo -e "${YELLOW}Executing memory leak test...${NC}"
|
|
136
|
+
if leaks --atExit -- node --expose-gc /tmp/test-leaks.js > /tmp/leaks-output.txt 2>&1; then
|
|
137
|
+
echo -e "${GREEN}✓ No memory leaks detected${NC}"
|
|
138
|
+
# Show summary if available
|
|
139
|
+
if grep -q "Process" /tmp/leaks-output.txt; then
|
|
140
|
+
echo -e "${YELLOW}Summary:${NC}"
|
|
141
|
+
grep -E "(Process|leaks for|total leaked bytes)" /tmp/leaks-output.txt || true
|
|
142
|
+
fi
|
|
143
|
+
else
|
|
144
|
+
echo -e "${RED}✗ Memory leaks detected or leaks tool failed:${NC}"
|
|
145
|
+
cat /tmp/leaks-output.txt
|
|
146
|
+
# Don't exit with failure - leaks tool can have false positives
|
|
147
|
+
echo -e "${YELLOW}Note: The leaks tool may report false positives from Node.js internals.${NC}"
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
rm -f /tmp/test-leaks.js /tmp/leaks-output.txt
|
|
151
|
+
else
|
|
152
|
+
echo -e "${YELLOW}leaks tool not available. Skipping native leak detection.${NC}"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
echo -e "${GREEN}=== All macOS memory tests passed! ===${NC}"
|
package/scripts/post-build.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { copyFile } from "fs/promises";
|
|
4
|
-
import { dirname, join } from "path";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
3
|
+
import { copyFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const distDir = join(__dirname, "..", "dist");
|
|
@@ -97,11 +97,22 @@ docker exec "$CONTAINER_NAME" sh -c "
|
|
|
97
97
|
$BUILD_CMD
|
|
98
98
|
"
|
|
99
99
|
|
|
100
|
-
# Copy artifacts back
|
|
100
|
+
# Copy artifacts back
|
|
101
101
|
docker cp "$CONTAINER_NAME:/tmp/project/prebuilds" . 2>/dev/null || true
|
|
102
102
|
docker cp "$CONTAINER_NAME:/tmp/project/build" . 2>/dev/null || true
|
|
103
103
|
docker cp "$CONTAINER_NAME:/tmp/project/config.gypi" . 2>/dev/null || true
|
|
104
104
|
|
|
105
|
+
# Fix ownership (docker cp preserves container's root ownership)
|
|
106
|
+
if [ -d prebuilds ]; then
|
|
107
|
+
chown -R "$(id -u):$(id -g)" prebuilds
|
|
108
|
+
fi
|
|
109
|
+
if [ -d build ]; then
|
|
110
|
+
chown -R "$(id -u):$(id -g)" build
|
|
111
|
+
fi
|
|
112
|
+
if [ -f config.gypi ]; then
|
|
113
|
+
chown "$(id -u):$(id -g)" config.gypi
|
|
114
|
+
fi
|
|
115
|
+
|
|
105
116
|
# Clean up container
|
|
106
117
|
docker rm -f "$CONTAINER_NAME" >/dev/null
|
|
107
118
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { arch, platform } from "node:os";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wrapper for prebuildify to ensure architecture is explicitly passed This
|
|
8
|
+
* works around the issue where prebuildify doesn't properly evaluate
|
|
9
|
+
* binding.gyp conditions for Windows architecture defines
|
|
10
|
+
*
|
|
11
|
+
* NOTE: if you don't include <windows.h> in your binding.gyp, this script is
|
|
12
|
+
* unnecessary.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Get the current architecture and platform
|
|
16
|
+
const currentArch = arch(); // 'x64', 'arm64', etc.
|
|
17
|
+
const currentPlatform = platform(); // 'win32', 'darwin', 'linux'
|
|
18
|
+
|
|
19
|
+
console.log(`Building for platform: ${currentPlatform}, arch: ${currentArch}`);
|
|
20
|
+
|
|
21
|
+
// Set up environment variables to help node-gyp
|
|
22
|
+
const env = { ...process.env };
|
|
23
|
+
|
|
24
|
+
// Set architecture-specific defines for Windows
|
|
25
|
+
if (currentPlatform === "win32") {
|
|
26
|
+
// Try various environment variables that might work
|
|
27
|
+
env.npm_config_arch = currentArch;
|
|
28
|
+
env.npm_config_target_arch = currentArch;
|
|
29
|
+
env.PREBUILD_ARCH = currentArch;
|
|
30
|
+
|
|
31
|
+
// Try setting compiler flags directly
|
|
32
|
+
if (currentArch === "x64") {
|
|
33
|
+
env.CL = "/D_M_X64 /D_WIN64 /D_AMD64_";
|
|
34
|
+
} else if (currentArch === "arm64") {
|
|
35
|
+
env.CL = "/D_M_ARM64 /D_WIN64";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Build the prebuildify command with explicit architecture
|
|
40
|
+
const args = [
|
|
41
|
+
"--napi",
|
|
42
|
+
"--tag-libc",
|
|
43
|
+
"--strip",
|
|
44
|
+
"--arch",
|
|
45
|
+
currentArch,
|
|
46
|
+
"--platform",
|
|
47
|
+
currentPlatform,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Add any additional arguments passed to this script
|
|
51
|
+
if (process.argv.length > 2) {
|
|
52
|
+
args.push(...process.argv.slice(2));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(`Running: prebuildify ${args.join(" ")}`);
|
|
56
|
+
if (currentPlatform === "win32" && env.CL) {
|
|
57
|
+
console.log(`CL environment variable: ${env.CL}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Spawn prebuildify with the arguments
|
|
61
|
+
const child = spawn("prebuildify", args, {
|
|
62
|
+
stdio: "inherit",
|
|
63
|
+
shell: true,
|
|
64
|
+
env,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
child.on("error", (error) => {
|
|
68
|
+
console.error("Failed to start prebuildify:", error);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
child.on("exit", (code) => {
|
|
73
|
+
if (code !== 0) {
|
|
74
|
+
console.error(`prebuildify exited with code ${code}`);
|
|
75
|
+
process.exit(code || 1);
|
|
76
|
+
}
|
|
77
|
+
});
|
package/scripts/precommit.ts
CHANGED
|
@@ -1,45 +1,70 @@
|
|
|
1
|
-
import { familySync } from "detect-libc";
|
|
2
1
|
import { execSync } from "node:child_process";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
3
|
import { platform } from "node:os";
|
|
4
|
+
import { exit } from "node:process";
|
|
4
5
|
|
|
5
6
|
const isLinux = platform() === "linux";
|
|
6
7
|
const isMacOS = platform() === "darwin";
|
|
7
|
-
const isGlibc = isLinux && familySync() === "glibc";
|
|
8
8
|
|
|
9
|
-
function run(
|
|
10
|
-
|
|
9
|
+
function run({
|
|
10
|
+
cmd,
|
|
11
|
+
desc,
|
|
12
|
+
exitOnFail: shouldExit = true,
|
|
13
|
+
}: {
|
|
14
|
+
cmd: string;
|
|
15
|
+
desc: string;
|
|
16
|
+
exitOnFail?: boolean;
|
|
17
|
+
}) {
|
|
18
|
+
console.log(`\n▶ ${desc ?? cmd}`);
|
|
11
19
|
try {
|
|
12
|
-
execSync(
|
|
20
|
+
execSync(cmd, { stdio: "inherit" });
|
|
13
21
|
} catch (error) {
|
|
14
|
-
console.error(`✗ Failed: ${
|
|
15
|
-
|
|
22
|
+
console.error(`✗ Failed: ${desc ?? cmd}: ` + error);
|
|
23
|
+
if (shouldExit) exit(1);
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
run("npm run
|
|
21
|
-
|
|
22
|
-
run("npm
|
|
23
|
-
run("npm run
|
|
27
|
+
run({ cmd: "npm install", desc: "Installing dependencies" });
|
|
28
|
+
run({ cmd: "npm run update", desc: "Updating dependencies" });
|
|
29
|
+
rmSync("package-lock.json", { force: true });
|
|
30
|
+
run({ cmd: "npm install", desc: "Updating dependencies" });
|
|
31
|
+
run({ cmd: "npm run clean", desc: "Start fresh" });
|
|
32
|
+
run({ cmd: "npm run fmt", desc: "Formatting code" });
|
|
33
|
+
run({ cmd: "npm run lint", desc: "Running linting checks" });
|
|
34
|
+
run({ cmd: "npm run docs", desc: "TypeDoc generation" });
|
|
35
|
+
run({ cmd: "npm run build:dist", desc: "Building distribution files" });
|
|
36
|
+
|
|
37
|
+
// Detect if we're using glibc (vs musl)
|
|
38
|
+
// Check process.report for musl loader - if not found, assume glibc
|
|
39
|
+
const isGlibc = (() => {
|
|
40
|
+
if (!isLinux) return false;
|
|
41
|
+
const report = process.report?.getReport() as any;
|
|
42
|
+
return !report?.sharedObjects?.some((lib: string) => /ld-musl/.test(lib));
|
|
43
|
+
})();
|
|
24
44
|
|
|
25
45
|
// Build native module with portable GLIBC
|
|
26
46
|
if (isLinux && isGlibc) {
|
|
27
|
-
run(
|
|
28
|
-
"npm run build:linux-glibc",
|
|
29
|
-
"Building native project with portable GLIBC",
|
|
30
|
-
);
|
|
47
|
+
run({
|
|
48
|
+
cmd: "npm run build:linux-glibc",
|
|
49
|
+
desc: "Building native project with portable GLIBC",
|
|
50
|
+
});
|
|
31
51
|
} else {
|
|
32
|
-
|
|
52
|
+
// Clean old native builds to ensure fresh compilation
|
|
53
|
+
run({ cmd: "npm run clean:native", desc: "Cleaning old native builds" });
|
|
54
|
+
run({ cmd: "npm run build:native", desc: "Building native module" });
|
|
33
55
|
}
|
|
34
56
|
|
|
35
|
-
run("npm run tests", "Running tests in ESM & CJS mode");
|
|
57
|
+
run({ cmd: "npm run tests", desc: "Running tests in ESM & CJS mode" });
|
|
36
58
|
|
|
37
59
|
// Platform-specific checks
|
|
38
60
|
if (isLinux || isMacOS) {
|
|
39
|
-
|
|
61
|
+
// Remove stale compile_commands.json to ensure it's regenerated with current settings
|
|
62
|
+
rmSync("compile_commands.json", { force: true });
|
|
63
|
+
run({ cmd: "npm run lint:native", desc: "Running clang-tidy" });
|
|
40
64
|
}
|
|
41
65
|
|
|
42
66
|
// Run comprehensive memory tests (cross-platform)
|
|
43
|
-
|
|
67
|
+
// This includes Windows debug memory check on Windows
|
|
68
|
+
run({ cmd: "npm run check:memory", desc: "Comprehensive memory tests" });
|
|
44
69
|
|
|
45
70
|
console.log("\n✅ All precommit checks passed!");
|
|
@@ -8,6 +8,8 @@ struct VolumeMetadataOptions {
|
|
|
8
8
|
std::string mountPoint; // Required mount point path
|
|
9
9
|
uint32_t timeoutMs = 5000; // Optional timeout with default
|
|
10
10
|
std::string device; // Optional device path
|
|
11
|
+
bool skipNetworkVolumes =
|
|
12
|
+
false; // Skip detailed info for network volumes to avoid blocking
|
|
11
13
|
|
|
12
14
|
static VolumeMetadataOptions FromObject(const Napi::Object &obj) {
|
|
13
15
|
VolumeMetadataOptions options;
|
|
@@ -25,6 +27,10 @@ struct VolumeMetadataOptions {
|
|
|
25
27
|
if (obj.Has("device")) {
|
|
26
28
|
options.device = obj.Get("device").As<Napi::String>();
|
|
27
29
|
}
|
|
30
|
+
if (obj.Has("skipNetworkVolumes")) {
|
|
31
|
+
options.skipNetworkVolumes =
|
|
32
|
+
obj.Get("skipNetworkVolumes").As<Napi::Boolean>().Value();
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
return options;
|
|
30
36
|
}
|
package/src/darwin/hidden.cpp
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
#include "hidden.h"
|
|
3
3
|
#include "../common/debug_log.h"
|
|
4
4
|
#include "../common/error_utils.h"
|
|
5
|
+
#include "path_security.h"
|
|
6
|
+
#include <string.h> // for strcmp
|
|
7
|
+
#include <sys/mount.h>
|
|
5
8
|
#include <sys/stat.h>
|
|
6
9
|
#include <unistd.h>
|
|
7
10
|
|
|
@@ -9,35 +12,51 @@ namespace FSMeta {
|
|
|
9
12
|
|
|
10
13
|
GetHiddenWorker::GetHiddenWorker(std::string path,
|
|
11
14
|
Napi::Promise::Deferred deferred)
|
|
12
|
-
: Napi::AsyncWorker(deferred.Env()), path_(path)
|
|
13
|
-
is_hidden_(false) {
|
|
15
|
+
: Napi::AsyncWorker(deferred.Env()), path_(std::move(path)),
|
|
16
|
+
deferred_(deferred), is_hidden_(false) {
|
|
14
17
|
DEBUG_LOG("[GetHiddenWorker] created for path: %s", path_.c_str());
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
void GetHiddenWorker::Execute() {
|
|
18
21
|
DEBUG_LOG("[GetHiddenWorker] checking hidden status for: %s", path_.c_str());
|
|
19
22
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
// Validate and canonicalize path using realpath() to prevent directory
|
|
24
|
+
// traversal This follows Apple's Secure Coding Guide recommendations For
|
|
25
|
+
// isHidden(), we allow non-existent paths (they will fail stat() below)
|
|
26
|
+
std::string error;
|
|
27
|
+
std::string validated_path = ValidateAndCanonicalizePath(path_, error, true);
|
|
28
|
+
if (validated_path.empty()) {
|
|
29
|
+
// If validation failed, check if it's because the path doesn't exist
|
|
30
|
+
// In that case, return the expected "Path not found" error for TypeScript
|
|
31
|
+
// layer
|
|
32
|
+
if (error.find("realpath") != std::string::npos &&
|
|
33
|
+
error.find("No such file or directory") != std::string::npos) {
|
|
34
|
+
SetError("Path not found: '" + path_ + "'");
|
|
35
|
+
} else {
|
|
36
|
+
SetError(error);
|
|
37
|
+
}
|
|
23
38
|
return;
|
|
24
39
|
}
|
|
25
40
|
|
|
41
|
+
// Use the validated path for all subsequent operations
|
|
42
|
+
DEBUG_LOG("[GetHiddenWorker] Using validated path: %s",
|
|
43
|
+
validated_path.c_str());
|
|
44
|
+
|
|
26
45
|
struct stat statbuf;
|
|
27
|
-
if (stat(
|
|
46
|
+
if (stat(validated_path.c_str(), &statbuf) != 0) {
|
|
28
47
|
int error = errno;
|
|
29
48
|
if (error == ENOENT) {
|
|
30
|
-
DEBUG_LOG("[GetHiddenWorker] path not found: %s",
|
|
31
|
-
SetError("Path not found: '" +
|
|
49
|
+
DEBUG_LOG("[GetHiddenWorker] path not found: %s", validated_path.c_str());
|
|
50
|
+
SetError("Path not found: '" + validated_path + "'");
|
|
32
51
|
} else {
|
|
33
52
|
DEBUG_LOG("[GetHiddenWorker] failed to stat path %s: %s (%d)",
|
|
34
|
-
|
|
35
|
-
SetError(CreatePathErrorMessage("stat",
|
|
53
|
+
validated_path.c_str(), strerror(error), error);
|
|
54
|
+
SetError(CreatePathErrorMessage("stat", validated_path, error));
|
|
36
55
|
}
|
|
37
56
|
return;
|
|
38
57
|
}
|
|
39
58
|
is_hidden_ = (statbuf.st_flags & UF_HIDDEN) != 0;
|
|
40
|
-
DEBUG_LOG("[GetHiddenWorker] path %s is %s",
|
|
59
|
+
DEBUG_LOG("[GetHiddenWorker] path %s is %s", validated_path.c_str(),
|
|
41
60
|
is_hidden_ ? "hidden" : "not hidden");
|
|
42
61
|
}
|
|
43
62
|
|
|
@@ -74,8 +93,8 @@ Napi::Promise GetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
|
74
93
|
|
|
75
94
|
SetHiddenWorker::SetHiddenWorker(std::string path, bool hidden,
|
|
76
95
|
Napi::Promise::Deferred deferred)
|
|
77
|
-
: Napi::AsyncWorker(deferred.Env()), path_(path)
|
|
78
|
-
deferred_(deferred) {
|
|
96
|
+
: Napi::AsyncWorker(deferred.Env()), path_(std::move(path)),
|
|
97
|
+
hidden_(hidden), deferred_(deferred) {
|
|
79
98
|
DEBUG_LOG("[SetHiddenWorker] created for path: %s, hidden: %d", path_.c_str(),
|
|
80
99
|
hidden_);
|
|
81
100
|
}
|
|
@@ -84,22 +103,34 @@ void SetHiddenWorker::Execute() {
|
|
|
84
103
|
DEBUG_LOG("[SetHiddenWorker] setting hidden=%d for: %s", hidden_,
|
|
85
104
|
path_.c_str());
|
|
86
105
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
// macOS uses BSD file flags (UF_HIDDEN) to control file visibility.
|
|
107
|
+
// This is different from the dot-prefix convention used on Unix systems.
|
|
108
|
+
// The chflags() system call modifies these BSD-specific file flags.
|
|
109
|
+
|
|
110
|
+
// Validate and canonicalize path using realpath() to prevent directory
|
|
111
|
+
// traversal This follows Apple's Secure Coding Guide recommendations For
|
|
112
|
+
// setHidden, the file must exist, so we use ValidatePathForRead
|
|
113
|
+
std::string error;
|
|
114
|
+
std::string validated_path = ValidatePathForRead(path_, error);
|
|
115
|
+
if (validated_path.empty()) {
|
|
116
|
+
SetError(error);
|
|
90
117
|
return;
|
|
91
118
|
}
|
|
92
119
|
|
|
120
|
+
// Use the validated path for all subsequent operations
|
|
121
|
+
DEBUG_LOG("[SetHiddenWorker] Using validated path: %s",
|
|
122
|
+
validated_path.c_str());
|
|
123
|
+
|
|
93
124
|
struct stat statbuf;
|
|
94
|
-
if (stat(
|
|
125
|
+
if (stat(validated_path.c_str(), &statbuf) != 0) {
|
|
95
126
|
int error = errno;
|
|
96
127
|
if (error == ENOENT) {
|
|
97
|
-
DEBUG_LOG("[SetHiddenWorker] path not found: %s",
|
|
98
|
-
SetError("Path not found: '" +
|
|
128
|
+
DEBUG_LOG("[SetHiddenWorker] path not found: %s", validated_path.c_str());
|
|
129
|
+
SetError("Path not found: '" + validated_path + "'");
|
|
99
130
|
} else {
|
|
100
131
|
DEBUG_LOG("[SetHiddenWorker] failed to stat path %s: %s (%d)",
|
|
101
|
-
|
|
102
|
-
SetError(CreatePathErrorMessage("stat",
|
|
132
|
+
validated_path.c_str(), strerror(error), error);
|
|
133
|
+
SetError(CreatePathErrorMessage("stat", validated_path, error));
|
|
103
134
|
}
|
|
104
135
|
return;
|
|
105
136
|
}
|
|
@@ -111,15 +142,32 @@ void SetHiddenWorker::Execute() {
|
|
|
111
142
|
new_flags = statbuf.st_flags & ~UF_HIDDEN;
|
|
112
143
|
}
|
|
113
144
|
|
|
114
|
-
if (chflags(
|
|
145
|
+
if (chflags(validated_path.c_str(), new_flags) != 0) {
|
|
115
146
|
int error = errno;
|
|
116
147
|
DEBUG_LOG("[SetHiddenWorker] failed to set flags for %s: %s (%d)",
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
validated_path.c_str(), strerror(error), error);
|
|
149
|
+
|
|
150
|
+
// Check if this is an APFS filesystem issue
|
|
151
|
+
struct statfs fs;
|
|
152
|
+
bool is_apfs = false;
|
|
153
|
+
if (statfs(validated_path.c_str(), &fs) == 0) {
|
|
154
|
+
is_apfs = (strcmp(fs.f_fstypename, "apfs") == 0);
|
|
155
|
+
DEBUG_LOG("[SetHiddenWorker] filesystem type: %s", fs.f_fstypename);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Provide more detailed error message for APFS
|
|
159
|
+
if (is_apfs && (error == EPERM || error == ENOTSUP)) {
|
|
160
|
+
SetError("Setting hidden attribute failed on APFS filesystem. "
|
|
161
|
+
"This is a known issue with some APFS volumes. "
|
|
162
|
+
"Error: " +
|
|
163
|
+
CreatePathErrorMessage("chflags", validated_path, error));
|
|
164
|
+
} else {
|
|
165
|
+
SetError(CreatePathErrorMessage("chflags", validated_path, error));
|
|
166
|
+
}
|
|
119
167
|
return;
|
|
120
168
|
}
|
|
121
169
|
DEBUG_LOG("[SetHiddenWorker] successfully set hidden=%d for: %s", hidden_,
|
|
122
|
-
|
|
170
|
+
validated_path.c_str());
|
|
123
171
|
}
|
|
124
172
|
|
|
125
173
|
void SetHiddenWorker::OnOK() {
|